chekk 0.2.5 → 0.4.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/src/index.js CHANGED
@@ -1,4 +1,7 @@
1
1
  import chalk from 'chalk';
2
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
3
+ import { join } from 'path';
4
+ import { homedir } from 'os';
2
5
  import { detectTools } from './detect.js';
3
6
  import { parseAllProjects } from './parsers/claude-code.js';
4
7
  import { parseAllWorkspaces } from './parsers/cursor.js';
@@ -8,6 +11,14 @@ import { computeDebugCycles } from './metrics/debug-cycles.js';
8
11
  import { computeAILeverage } from './metrics/ai-leverage.js';
9
12
  import { computeSessionStructure } from './metrics/session-structure.js';
10
13
  import { computeOverallScore } from './scorer.js';
14
+ import {
15
+ computeSignatures,
16
+ computeWatchPoints,
17
+ computeTrajectory,
18
+ computeProjectComplexity,
19
+ generateAssessment,
20
+ computeConfidence,
21
+ } from './insights.js';
11
22
  import {
12
23
  displayHeader,
13
24
  displayScan,
@@ -19,6 +30,52 @@ import {
19
30
  } from './display.js';
20
31
  import { generateProse, askVerbose, askClaim, uploadAndClaim } from './upload.js';
21
32
 
33
+ // ── Score history ──
34
+ const HISTORY_DIR = join(homedir(), '.chekk');
35
+ const HISTORY_FILE = join(HISTORY_DIR, 'history.json');
36
+
37
+ function loadHistory() {
38
+ try {
39
+ if (existsSync(HISTORY_FILE)) {
40
+ return JSON.parse(readFileSync(HISTORY_FILE, 'utf-8'));
41
+ }
42
+ } catch {}
43
+ return { scans: [] };
44
+ }
45
+
46
+ function saveHistory(history) {
47
+ try {
48
+ if (!existsSync(HISTORY_DIR)) mkdirSync(HISTORY_DIR, { recursive: true });
49
+ writeFileSync(HISTORY_FILE, JSON.stringify(history, null, 2));
50
+ } catch {}
51
+ }
52
+
53
+ function computePerToolScores(allSessions) {
54
+ const toolSessions = {};
55
+ for (const s of allSessions) {
56
+ const t = s.sourceTool || 'Unknown';
57
+ if (!toolSessions[t]) toolSessions[t] = [];
58
+ toolSessions[t].push(s);
59
+ }
60
+
61
+ const perTool = {};
62
+ for (const [tool, sessions] of Object.entries(toolSessions)) {
63
+ if (sessions.length < 2) {
64
+ perTool[tool] = { sessions: sessions.length, score: null, label: 'limited data' };
65
+ continue;
66
+ }
67
+ const m = {
68
+ decomposition: computeDecomposition(sessions),
69
+ debugCycles: computeDebugCycles(sessions),
70
+ aiLeverage: computeAILeverage(sessions),
71
+ sessionStructure: computeSessionStructure(sessions),
72
+ };
73
+ const r = computeOverallScore(m);
74
+ perTool[tool] = { sessions: sessions.length, score: r.overall, label: null };
75
+ }
76
+ return perTool;
77
+ }
78
+
22
79
  export async function run(options = {}) {
23
80
  // ── Header ──
24
81
  displayHeader();
@@ -34,16 +91,21 @@ export async function run(options = {}) {
34
91
 
35
92
  displayScan(tools);
36
93
 
37
- // ── Step 2: Parse sessions ──
94
+ // ── Step 2: Parse sessions (tag each with sourceTool) ──
38
95
  let allSessions = [];
39
96
  for (const tool of tools) {
97
+ let parsed = [];
40
98
  if (tool.tool === 'Claude Code') {
41
- allSessions.push(...parseAllProjects(tool.basePath));
99
+ parsed = parseAllProjects(tool.basePath);
42
100
  } else if (tool.tool === 'Cursor') {
43
- allSessions.push(...parseAllWorkspaces(tool.basePath));
101
+ parsed = parseAllWorkspaces(tool.basePath);
44
102
  } else if (tool.tool === 'Codex') {
45
- allSessions.push(...parseCodexSessions(tool.basePath));
103
+ parsed = parseCodexSessions(tool.basePath);
46
104
  }
105
+ for (const s of parsed) {
106
+ s.sourceTool = tool.tool;
107
+ }
108
+ allSessions.push(...parsed);
47
109
  }
48
110
 
49
111
  if (allSessions.length === 0) {
@@ -80,23 +142,52 @@ export async function run(options = {}) {
80
142
 
81
143
  const result = computeOverallScore(metrics);
82
144
 
145
+ // ── Cross-platform scores ──
146
+ const perToolScores = tools.length > 1 ? computePerToolScores(allSessions) : null;
147
+
148
+ // ── Score delta (compare to last scan) ──
149
+ const history = loadHistory();
150
+ const lastScan = history.scans.length > 0 ? history.scans[history.scans.length - 1] : null;
151
+ const scoreDelta = lastScan ? result.overall - lastScan.overall : null;
152
+
153
+ // Save current scan
154
+ history.scans.push({
155
+ overall: result.overall,
156
+ tier: result.tier,
157
+ archetype: result.archetype.name,
158
+ scores: result.scores,
159
+ date: new Date().toISOString(),
160
+ });
161
+ // Keep last 20 scans
162
+ if (history.scans.length > 20) history.scans = history.scans.slice(-20);
163
+ saveHistory(history);
164
+
83
165
  const sessionStats = {
84
166
  totalSessions: allSessions.length,
85
167
  totalExchanges,
86
168
  projectCount: projects.length,
87
169
  dateRange: dateRangeFull,
170
+ dateRangeShort,
88
171
  tools: tools.map(t => t.tool),
89
172
  };
90
173
 
174
+ // ── Step 3b: Compute insights ──
175
+ const signatures = computeSignatures(allSessions, metrics);
176
+ const watchPoints = computeWatchPoints(allSessions, metrics);
177
+ const trajectory = computeTrajectory(allSessions);
178
+ const projectComplexity = computeProjectComplexity(allSessions);
179
+ const assessment = generateAssessment(result, metrics, signatures, watchPoints);
180
+ const confidence = computeConfidence(sessionStats);
181
+
182
+ const insights = { signatures, watchPoints, trajectory, projectComplexity, assessment, confidence };
183
+
91
184
  // ── JSON output ──
92
185
  if (options.json) {
93
- console.log(JSON.stringify({ metrics, result, sessionStats }, null, 2));
186
+ console.log(JSON.stringify({ metrics, result, sessionStats, perToolScores, scoreDelta, insights }, null, 2));
94
187
  return;
95
188
  }
96
189
 
97
190
  // ── Step 4: Progress bar + API call in parallel ──
98
- // Run prose generation alongside the progress bar so score appears
99
- // immediately when the bar hits 100% — no lag.
100
191
  let prose = null;
101
192
  if (!options.offline) {
102
193
  const [, proseResult] = await Promise.all([
@@ -109,14 +200,14 @@ export async function run(options = {}) {
109
200
  }
110
201
 
111
202
  // ── Step 5: Display results ──
203
+ const extra = { scoreDelta, perToolScores, insights, sessionStats };
112
204
  if (options.offline) {
113
- displayOffline(result, metrics);
205
+ displayOffline(result, metrics, extra);
114
206
  } else {
115
- displayFull(result, metrics, prose);
207
+ displayFull(result, metrics, prose, extra);
116
208
  }
117
209
 
118
210
  // ── Step 6: Verbose prompt (interactive) ──
119
- // If --verbose flag was passed, show immediately. Otherwise prompt.
120
211
  if (options.verbose) {
121
212
  displayVerbose(metrics, allSessions);
122
213
  } else {
@@ -0,0 +1,503 @@
1
+ /**
2
+ * Insights Engine
3
+ *
4
+ * Computes higher-order analysis from raw metrics and sessions:
5
+ * - Signatures: distinctive patterns that make an engineer unique
6
+ * - Watch Points: anti-patterns and areas for improvement
7
+ * - Trajectory: weekly score evolution over time
8
+ * - Project Complexity: classification of project sophistication
9
+ * - Assessment: narrative paragraph for the engineer's profile
10
+ * - Confidence: statistical confidence based on data volume
11
+ */
12
+
13
+ import { computeDecomposition } from './metrics/decomposition.js';
14
+ import { computeDebugCycles } from './metrics/debug-cycles.js';
15
+ import { computeAILeverage } from './metrics/ai-leverage.js';
16
+ import { computeSessionStructure } from './metrics/session-structure.js';
17
+ import { computeOverallScore } from './scorer.js';
18
+
19
+ // ── Benchmarks (early-stage estimates, refined as data grows) ──
20
+ export const BENCHMARKS = {
21
+ avgExchangesPerSession: 34.2,
22
+ avgPromptLength: 187,
23
+ avgTurnsToResolve: 3.8,
24
+ specificReportRatio: 62,
25
+ highLevelRatio: 18,
26
+ contextSetRatio: 35,
27
+ refinementRatio: 15,
28
+ reviewEndRatio: 28,
29
+ };
30
+
31
+ // ── Dimension score ranges (observed distribution) ──
32
+ export const DIM_RANGES = {
33
+ decomposition: { min: 15, max: 95 },
34
+ debugCycles: { min: 20, max: 98 },
35
+ aiLeverage: { min: 10, max: 92 },
36
+ sessionStructure: { min: 12, max: 88 },
37
+ };
38
+
39
+ // ══════════════════════════════════════════════
40
+ // SIGNATURES — Distinctive patterns
41
+ // ══════════════════════════════════════════════
42
+
43
+ const constraintPatterns = /\b(don'?t|do not|never|avoid|without|no |not |shouldn'?t|must not|skip|exclude)\b/i;
44
+ const preflightPatterns = /^(before (we|you|i)|don'?t code|review (first|this|my|the plan)|let'?s (think|plan|discuss)|check my (approach|plan|thinking))/i;
45
+ const testFirstPatterns = /\b(write (the )?tests? (first|before)|test.?driven|TDD|spec first|start with (tests?|specs?))\b/i;
46
+ const negativeConstraintPatterns = /\b(don'?t|do not|never|avoid|must not|shouldn'?t)\b.*\b(add|create|use|include|change|modify|touch|remove)\b/i;
47
+
48
+ export function computeSignatures(allSessions, metrics) {
49
+ const signatures = [];
50
+ const d = metrics.decomposition.details;
51
+ const db = metrics.debugCycles.details;
52
+ const ai = metrics.aiLeverage.details;
53
+ const ss = metrics.sessionStructure.details;
54
+
55
+ let totalPrompts = 0;
56
+ let constraintPrompts = 0;
57
+ let preflightSessions = 0;
58
+ let testFirstSessions = 0;
59
+ let modificationCount = 0;
60
+ let acceptCount = 0;
61
+
62
+ for (const session of allSessions) {
63
+ const { exchanges } = session;
64
+ if (exchanges.length === 0) continue;
65
+
66
+ // Check first prompt for preflight review
67
+ const firstPrompt = exchanges[0].userPrompt || '';
68
+ if (preflightPatterns.test(firstPrompt)) {
69
+ preflightSessions++;
70
+ }
71
+
72
+ let hasTestFirst = false;
73
+ for (let i = 0; i < exchanges.length; i++) {
74
+ const prompt = exchanges[i].userPrompt || '';
75
+ totalPrompts++;
76
+
77
+ if (constraintPatterns.test(prompt) && negativeConstraintPatterns.test(prompt)) {
78
+ constraintPrompts++;
79
+ }
80
+
81
+ if (testFirstPatterns.test(prompt)) {
82
+ hasTestFirst = true;
83
+ }
84
+
85
+ // Track modification vs acceptance
86
+ if (i > 0 && /\b(actually|wait|instead|change|no,?|not quite|modify|tweak)\b/i.test(prompt)) {
87
+ modificationCount++;
88
+ } else if (i > 0) {
89
+ acceptCount++;
90
+ }
91
+ }
92
+ if (hasTestFirst) testFirstSessions++;
93
+ }
94
+
95
+ const sessionsWithExchanges = allSessions.filter(s => s.exchanges.length > 0).length;
96
+
97
+ // Pre-flight reviews
98
+ const preflightRatio = sessionsWithExchanges > 0 ? preflightSessions / sessionsWithExchanges : 0;
99
+ if (preflightRatio > 0.15 && preflightSessions >= 3) {
100
+ signatures.push({
101
+ name: 'Pre-flight reviews',
102
+ detail: `You ask AI to review your plan before coding in ${Math.round(preflightRatio * 100)}% of sessions. Only 8% of engineers do this consistently. This correlates with fewer debug cycles.`,
103
+ });
104
+ }
105
+
106
+ // Constraint-first prompting
107
+ const constraintRatio = totalPrompts > 0 ? constraintPrompts / totalPrompts : 0;
108
+ if (constraintRatio > 0.1 && constraintPrompts >= 5) {
109
+ signatures.push({
110
+ name: 'Constraint-first prompting',
111
+ detail: `You specify what NOT to do in ${Math.round(constraintRatio * 100)}% of prompts. This is a hallmark of senior architectural thinking that prevents scope creep.`,
112
+ });
113
+ }
114
+
115
+ // Test-driven AI usage
116
+ const testFirstRatio = sessionsWithExchanges > 0 ? testFirstSessions / sessionsWithExchanges : 0;
117
+ if (testFirstRatio > 0.05 && testFirstSessions >= 2) {
118
+ signatures.push({
119
+ name: 'Test-driven AI usage',
120
+ detail: `You request tests before implementation in ${Math.round(testFirstRatio * 100)}% of sessions. Engineers who do this ship fewer bugs post-merge.`,
121
+ });
122
+ }
123
+
124
+ // Deep session marathons
125
+ if (d.avgExchangesPerSession > BENCHMARKS.avgExchangesPerSession * 2) {
126
+ signatures.push({
127
+ name: 'Marathon sessions',
128
+ detail: `Avg session depth of ${d.avgExchangesPerSession} exchanges is ${Math.round(d.avgExchangesPerSession / BENCHMARKS.avgExchangesPerSession)}x the benchmark (${BENCHMARKS.avgExchangesPerSession}). You sustain deep, focused work.`,
129
+ });
130
+ }
131
+
132
+ // Zero vague debugging
133
+ if (db.vagueReports === 0 && db.totalDebugSequences > 5) {
134
+ signatures.push({
135
+ name: 'Precision debugging',
136
+ detail: `Zero vague error reports across ${db.totalDebugSequences} debug sequences. Every bug report includes specific context. This is rare.`,
137
+ });
138
+ }
139
+
140
+ // High architectural ratio
141
+ if (ai.highLevelRatio > 30) {
142
+ signatures.push({
143
+ name: 'Strategic AI usage',
144
+ detail: `${ai.highLevelRatio}% of prompts are architectural or planning-level (benchmark: ${BENCHMARKS.highLevelRatio}%). You use AI as a thinking partner, not just a code generator.`,
145
+ });
146
+ }
147
+
148
+ // Critical reviewer
149
+ const totalFollowups = modificationCount + acceptCount;
150
+ const modRatio = totalFollowups > 0 ? modificationCount / totalFollowups : 0;
151
+ if (modRatio > 0.25 && modificationCount > 10) {
152
+ signatures.push({
153
+ name: 'Critical reviewer',
154
+ detail: `You modify or redirect AI output in ${Math.round(modRatio * 100)}% of follow-up prompts. This indicates active evaluation rather than passive acceptance.`,
155
+ });
156
+ }
157
+
158
+ return signatures.slice(0, 4); // Max 4 signatures
159
+ }
160
+
161
+ // ══════════════════════════════════════════════
162
+ // WATCH POINTS — Anti-patterns
163
+ // ══════════════════════════════════════════════
164
+
165
+ export function computeWatchPoints(allSessions, metrics) {
166
+ const watchPoints = [];
167
+ const d = metrics.decomposition.details;
168
+ const db = metrics.debugCycles.details;
169
+ const ai = metrics.aiLeverage.details;
170
+ const ss = metrics.sessionStructure.details;
171
+
172
+ // Context amnesia — restarting from scratch on same project
173
+ const projectSessions = {};
174
+ for (const s of allSessions) {
175
+ const p = s.project || 'unknown';
176
+ if (!projectSessions[p]) projectSessions[p] = [];
177
+ projectSessions[p].push(s);
178
+ }
179
+ let contextRestarts = 0;
180
+ let multiSessionProjects = 0;
181
+ for (const [, sessions] of Object.entries(projectSessions)) {
182
+ if (sessions.length < 2) continue;
183
+ multiSessionProjects++;
184
+ for (let i = 1; i < sessions.length; i++) {
185
+ const firstPrompt = sessions[i].exchanges[0]?.userPrompt || '';
186
+ // If first prompt doesn't reference previous work, it's a context restart
187
+ if (firstPrompt.length > 50 && !/\b(continuing|following up|as discussed|last time|previously|where we left|earlier)\b/i.test(firstPrompt)) {
188
+ contextRestarts++;
189
+ }
190
+ }
191
+ }
192
+ const totalFollowupSessions = Object.values(projectSessions).reduce((sum, s) => sum + Math.max(0, s.length - 1), 0);
193
+ if (totalFollowupSessions > 3 && contextRestarts / totalFollowupSessions > 0.5) {
194
+ watchPoints.push({
195
+ name: 'Context amnesia',
196
+ detail: `You restart context from scratch in ${Math.round(contextRestarts / totalFollowupSessions * 100)}% of follow-up sessions on the same project. Engineers who maintain context across sessions are more efficient.`,
197
+ });
198
+ }
199
+
200
+ // Low modification rate — accepting AI output without review
201
+ let modCount = 0;
202
+ let followupCount = 0;
203
+ for (const session of allSessions) {
204
+ for (let i = 1; i < session.exchanges.length; i++) {
205
+ followupCount++;
206
+ const prompt = session.exchanges[i].userPrompt || '';
207
+ if (/\b(actually|wait|instead|change|no,?|not quite|modify|tweak|hmm|but )\b/i.test(prompt)) {
208
+ modCount++;
209
+ }
210
+ }
211
+ }
212
+ const modRatio = followupCount > 10 ? modCount / followupCount : 0.5;
213
+ if (modRatio < 0.15 && followupCount > 20) {
214
+ watchPoints.push({
215
+ name: 'Acceptance without review',
216
+ detail: `You accept AI output without modification in ${Math.round((1 - modRatio) * 100)}% of cases. Top engineers modify or redirect 30%+ of initial suggestions.`,
217
+ });
218
+ }
219
+
220
+ // Monologue prompting — excessively long first prompts
221
+ if (d.avgPromptLength > 2000) {
222
+ watchPoints.push({
223
+ name: 'Monologue prompting',
224
+ detail: `Avg prompt length of ${d.avgPromptLength} chars is ${Math.round(d.avgPromptLength / BENCHMARKS.avgPromptLength)}x the benchmark. Breaking complex requests into 2-3 shorter prompts typically yields better AI output.`,
225
+ });
226
+ }
227
+
228
+ // Low context-setting
229
+ if (ss.contextSetRatio < 20) {
230
+ watchPoints.push({
231
+ name: 'Missing context',
232
+ detail: `Only ${ss.contextSetRatio}% of sessions start with context-setting (benchmark: ${BENCHMARKS.contextSetRatio}%). Upfront context leads to better first responses and fewer corrections.`,
233
+ });
234
+ }
235
+
236
+ // Extended debug spirals
237
+ if (db.longLoops > 2) {
238
+ watchPoints.push({
239
+ name: 'Debug spirals',
240
+ detail: `${db.longLoops} extended debug loops (>5 turns) detected. When stuck, try providing more specific error context or breaking the problem differently.`,
241
+ });
242
+ }
243
+
244
+ return watchPoints.slice(0, 3); // Max 3 watch points
245
+ }
246
+
247
+ // ══════════════════════════════════════════════
248
+ // TRAJECTORY — Weekly score evolution
249
+ // ══════════════════════════════════════════════
250
+
251
+ export function computeTrajectory(allSessions) {
252
+ // Group sessions by week
253
+ const sessionsWithTime = allSessions.filter(s => s.startTime);
254
+ if (sessionsWithTime.length < 5) return null;
255
+
256
+ sessionsWithTime.sort((a, b) => new Date(a.startTime) - new Date(b.startTime));
257
+
258
+ const firstDate = new Date(sessionsWithTime[0].startTime);
259
+ const lastDate = new Date(sessionsWithTime[sessionsWithTime.length - 1].startTime);
260
+
261
+ // Need at least 2 weeks of data
262
+ const daySpan = (lastDate - firstDate) / (1000 * 60 * 60 * 24);
263
+ if (daySpan < 10) return null;
264
+
265
+ // Create weekly buckets
266
+ const weeks = [];
267
+ let weekStart = new Date(firstDate);
268
+ weekStart.setHours(0, 0, 0, 0);
269
+ // Align to Monday
270
+ weekStart.setDate(weekStart.getDate() - weekStart.getDay() + 1);
271
+
272
+ while (weekStart <= lastDate) {
273
+ const weekEnd = new Date(weekStart);
274
+ weekEnd.setDate(weekEnd.getDate() + 7);
275
+ const weekSessions = sessionsWithTime.filter(s => {
276
+ const t = new Date(s.startTime);
277
+ return t >= weekStart && t < weekEnd;
278
+ });
279
+
280
+ if (weekSessions.length >= 2) {
281
+ // Compute score for this week
282
+ const m = {
283
+ decomposition: computeDecomposition(weekSessions),
284
+ debugCycles: computeDebugCycles(weekSessions),
285
+ aiLeverage: computeAILeverage(weekSessions),
286
+ sessionStructure: computeSessionStructure(weekSessions),
287
+ };
288
+ const r = computeOverallScore(m);
289
+ const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
290
+ weeks.push({
291
+ label: `${months[weekStart.getMonth()]} ${weekStart.getDate()}-${weekEnd.getDate() - 1}`,
292
+ score: r.overall,
293
+ sessions: weekSessions.length,
294
+ });
295
+ }
296
+
297
+ weekStart = new Date(weekStart);
298
+ weekStart.setDate(weekStart.getDate() + 7);
299
+ }
300
+
301
+ if (weeks.length < 2) return null;
302
+
303
+ // Compute learning velocity
304
+ const firstScore = weeks[0].score;
305
+ const lastScore = weeks[weeks.length - 1].score;
306
+ const delta = lastScore - firstScore;
307
+ const weeksCount = weeks.length;
308
+ const velocityPerWeek = delta / weeksCount;
309
+
310
+ let velocityLabel;
311
+ if (velocityPerWeek > 3) velocityLabel = 'FAST';
312
+ else if (velocityPerWeek > 1) velocityLabel = 'STEADY';
313
+ else if (velocityPerWeek > -1) velocityLabel = 'STABLE';
314
+ else velocityLabel = 'DECLINING';
315
+
316
+ return {
317
+ weeks,
318
+ delta,
319
+ daysSpan: Math.round(daySpan),
320
+ velocityLabel,
321
+ velocityDetail: delta !== 0
322
+ ? `${Math.abs(delta)} point ${delta > 0 ? 'improvement' : 'change'} over ${Math.round(daySpan)} days`
323
+ : `Stable over ${Math.round(daySpan)} days`,
324
+ };
325
+ }
326
+
327
+ // ══════════════════════════════════════════════
328
+ // PROJECT COMPLEXITY — What did they build?
329
+ // ══════════════════════════════════════════════
330
+
331
+ const complexitySignals = {
332
+ high: /\b(pipeline|distributed|real.?time|analytics|classification|machine learning|ml |auth|oauth|websocket|streaming|queue|worker|migration|microservice|kubernetes|docker|deployment|ci.?cd|infrastructure|database design|data model|schema design|api design|caching|rate limit)\b/i,
333
+ medium: /\b(api|crud|component|feature|integration|testing|refactor|database|query|endpoint|route|middleware|hook|state management|responsive|animation|chart|graph|dashboard)\b/i,
334
+ };
335
+
336
+ export function computeProjectComplexity(allSessions) {
337
+ const projectData = {};
338
+
339
+ for (const s of allSessions) {
340
+ const p = s.project || 'unknown';
341
+ if (!projectData[p]) {
342
+ projectData[p] = { sessions: 0, exchanges: 0, daysActive: new Set(), highSignals: new Set(), medSignals: new Set(), prompts: [] };
343
+ }
344
+ projectData[p].sessions++;
345
+ projectData[p].exchanges += s.exchangeCount;
346
+ if (s.startTime) {
347
+ projectData[p].daysActive.add(new Date(s.startTime).toISOString().split('T')[0]);
348
+ }
349
+
350
+ for (const ex of s.exchanges) {
351
+ const prompt = ex.userPrompt || '';
352
+ projectData[p].prompts.push(prompt);
353
+
354
+ // Extract complexity signals
355
+ const highMatches = prompt.match(complexitySignals.high);
356
+ const medMatches = prompt.match(complexitySignals.medium);
357
+ if (highMatches) {
358
+ for (const m of highMatches) projectData[p].highSignals.add(m.toLowerCase().trim());
359
+ }
360
+ if (medMatches) {
361
+ for (const m of medMatches) projectData[p].medSignals.add(m.toLowerCase().trim());
362
+ }
363
+ }
364
+ }
365
+
366
+ const projects = [];
367
+ for (const [name, data] of Object.entries(projectData)) {
368
+ if (data.exchanges < 3) continue; // Skip trivial projects
369
+
370
+ let complexity;
371
+ const signals = [...data.highSignals, ...data.medSignals].slice(0, 5);
372
+ if (data.highSignals.size >= 3 || (data.highSignals.size >= 1 && data.exchanges > 50)) {
373
+ complexity = 'HIGH';
374
+ } else if (data.medSignals.size >= 3 || data.highSignals.size >= 1 || data.exchanges > 30) {
375
+ complexity = 'MEDIUM';
376
+ } else {
377
+ complexity = 'LOW';
378
+ }
379
+
380
+ const shortName = name.length > 28 ? '...' + name.slice(-25) : name;
381
+
382
+ projects.push({
383
+ name: shortName,
384
+ complexity,
385
+ sessions: data.sessions,
386
+ exchanges: data.exchanges,
387
+ daysActive: data.daysActive.size,
388
+ signals,
389
+ });
390
+ }
391
+
392
+ // Sort by exchanges descending
393
+ projects.sort((a, b) => b.exchanges - a.exchanges);
394
+ return projects.slice(0, 5); // Top 5 projects
395
+ }
396
+
397
+ // ══════════════════════════════════════════════
398
+ // ASSESSMENT — Narrative paragraph
399
+ // ══════════════════════════════════════════════
400
+
401
+ export function generateAssessment(result, metrics, signatures, watchPoints) {
402
+ const { overall, scores, archetype, tier } = result;
403
+ const d = metrics.decomposition.details;
404
+ const db = metrics.debugCycles.details;
405
+ const ai = metrics.aiLeverage.details;
406
+ const ss = metrics.sessionStructure.details;
407
+
408
+ // Find strongest and weakest dimensions
409
+ const dims = [
410
+ { key: 'decomposition', label: 'problem decomposition', score: scores.decomposition },
411
+ { key: 'debugCycles', label: 'debugging efficiency', score: scores.debugCycles },
412
+ { key: 'aiLeverage', label: 'AI leverage', score: scores.aiLeverage },
413
+ { key: 'sessionStructure', label: 'workflow discipline', score: scores.sessionStructure },
414
+ ];
415
+ dims.sort((a, b) => b.score - a.score);
416
+ const strongest = dims[0];
417
+ const weakest = dims[dims.length - 1];
418
+
419
+ // Build assessment parts
420
+ let assessment = `This engineer demonstrates ${dimQualitative(strongest.score)} ${strongest.label}`;
421
+
422
+ // Add signature mention if available
423
+ if (signatures.length > 0) {
424
+ assessment += ` with a distinctive pattern of ${signatures[0].name.toLowerCase()}`;
425
+ }
426
+ assessment += '.';
427
+
428
+ // Second sentence — second strength or debugging detail
429
+ if (dims[1].score >= 65) {
430
+ assessment += ` Their ${dims[1].label} is also ${dimQualitative(dims[1].score).toLowerCase()}`;
431
+ if (db.avgTurnsToResolve <= 2 && dims[1].key === 'debugCycles') {
432
+ assessment += ' \u2014 surgical and specific with ' + (db.longLoops === 0 ? 'zero' : 'minimal') + ' extended loops';
433
+ }
434
+ assessment += '.';
435
+ }
436
+
437
+ // Third sentence — growth area
438
+ if (weakest.score < 65) {
439
+ assessment += ` Primary growth opportunity is in ${weakest.label}`;
440
+ if (weakest.key === 'sessionStructure') {
441
+ assessment += ': context-setting and upfront planning are below benchmark';
442
+ if (ss.refinementRatio > 15) {
443
+ assessment += ', though iterative refinement partially compensates';
444
+ }
445
+ } else if (weakest.key === 'decomposition') {
446
+ assessment += ': more task breakdown and structured thinking would yield significant score improvement';
447
+ } else if (weakest.key === 'aiLeverage') {
448
+ assessment += ': using AI for architecture and planning, not just code generation, would increase impact';
449
+ } else {
450
+ assessment += ': stronger error reporting and systematic resolution would improve efficiency';
451
+ }
452
+ assessment += '.';
453
+ }
454
+
455
+ // Fourth sentence — best for
456
+ assessment += ' ' + archetype.bestFor;
457
+
458
+ return assessment;
459
+ }
460
+
461
+ function dimQualitative(score) {
462
+ if (score >= 80) return 'Exceptional';
463
+ if (score >= 65) return 'Strong';
464
+ if (score >= 50) return 'Solid';
465
+ if (score >= 35) return 'Developing';
466
+ return 'Early-stage';
467
+ }
468
+
469
+ // ══════════════════════════════════════════════
470
+ // CONFIDENCE — Data volume indicator
471
+ // ══════════════════════════════════════════════
472
+
473
+ export function computeConfidence(sessionStats) {
474
+ const { totalSessions, totalExchanges, tools } = sessionStats;
475
+ const toolCount = tools.length;
476
+
477
+ // Score confidence on sessions, exchanges, and tool diversity
478
+ let score = 0;
479
+ if (totalSessions >= 50) score += 40;
480
+ else if (totalSessions >= 20) score += 30;
481
+ else if (totalSessions >= 10) score += 20;
482
+ else score += 10;
483
+
484
+ if (totalExchanges >= 500) score += 30;
485
+ else if (totalExchanges >= 200) score += 20;
486
+ else if (totalExchanges >= 50) score += 10;
487
+
488
+ if (toolCount >= 3) score += 20;
489
+ else if (toolCount >= 2) score += 15;
490
+ else score += 10;
491
+
492
+ // Bonus for enough data
493
+ if (totalSessions >= 30 && totalExchanges >= 300) score += 10;
494
+
495
+ score = Math.min(100, score);
496
+
497
+ let level;
498
+ if (score >= 80) level = 'HIGH';
499
+ else if (score >= 50) level = 'MODERATE';
500
+ else level = 'LOW';
501
+
502
+ return { score, level };
503
+ }