chekk 0.3.0 → 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
@@ -11,6 +11,14 @@ import { computeDebugCycles } from './metrics/debug-cycles.js';
11
11
  import { computeAILeverage } from './metrics/ai-leverage.js';
12
12
  import { computeSessionStructure } from './metrics/session-structure.js';
13
13
  import { computeOverallScore } from './scorer.js';
14
+ import {
15
+ computeSignatures,
16
+ computeWatchPoints,
17
+ computeTrajectory,
18
+ computeProjectComplexity,
19
+ generateAssessment,
20
+ computeConfidence,
21
+ } from './insights.js';
14
22
  import {
15
23
  displayHeader,
16
24
  displayScan,
@@ -147,6 +155,7 @@ export async function run(options = {}) {
147
155
  overall: result.overall,
148
156
  tier: result.tier,
149
157
  archetype: result.archetype.name,
158
+ scores: result.scores,
150
159
  date: new Date().toISOString(),
151
160
  });
152
161
  // Keep last 20 scans
@@ -158,12 +167,23 @@ export async function run(options = {}) {
158
167
  totalExchanges,
159
168
  projectCount: projects.length,
160
169
  dateRange: dateRangeFull,
170
+ dateRangeShort,
161
171
  tools: tools.map(t => t.tool),
162
172
  };
163
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
+
164
184
  // ── JSON output ──
165
185
  if (options.json) {
166
- console.log(JSON.stringify({ metrics, result, sessionStats, perToolScores, scoreDelta }, null, 2));
186
+ console.log(JSON.stringify({ metrics, result, sessionStats, perToolScores, scoreDelta, insights }, null, 2));
167
187
  return;
168
188
  }
169
189
 
@@ -180,10 +200,11 @@ export async function run(options = {}) {
180
200
  }
181
201
 
182
202
  // ── Step 5: Display results ──
203
+ const extra = { scoreDelta, perToolScores, insights, sessionStats };
183
204
  if (options.offline) {
184
- displayOffline(result, metrics, { scoreDelta, perToolScores });
205
+ displayOffline(result, metrics, extra);
185
206
  } else {
186
- displayFull(result, metrics, prose, { scoreDelta, perToolScores });
207
+ displayFull(result, metrics, prose, extra);
187
208
  }
188
209
 
189
210
  // ── Step 6: Verbose prompt (interactive) ──
@@ -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
+ }