analyzthis_design 2.4.0 → 2.5.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.
Files changed (46) hide show
  1. package/HOW-TO-USE.md +26 -8
  2. package/README.md +48 -4
  3. package/agents/cards/anuj.md +13 -0
  4. package/agents/cards/arjun.md +13 -0
  5. package/agents/cards/devi.md +22 -0
  6. package/agents/cards/kavi.md +13 -0
  7. package/agents/cards/meera.md +13 -0
  8. package/agents/cards/noor.md +13 -0
  9. package/agents/cards/priya.md +13 -0
  10. package/agents/cards/raj.md +13 -0
  11. package/agents/cards/zara.md +13 -0
  12. package/dist/HOW-TO-USE.md +26 -8
  13. package/dist/README.md +48 -4
  14. package/dist/agents/cards/anuj.md +13 -0
  15. package/dist/agents/cards/arjun.md +13 -0
  16. package/dist/agents/cards/devi.md +22 -0
  17. package/dist/agents/cards/kavi.md +13 -0
  18. package/dist/agents/cards/meera.md +13 -0
  19. package/dist/agents/cards/noor.md +13 -0
  20. package/dist/agents/cards/priya.md +13 -0
  21. package/dist/agents/cards/raj.md +13 -0
  22. package/dist/agents/cards/zara.md +13 -0
  23. package/dist/bin/cli.js +184 -0
  24. package/dist/lib/accept.js +108 -0
  25. package/dist/lib/evolution-metrics.js +303 -87
  26. package/dist/lib/evolve.js +5 -2
  27. package/dist/lib/feedback-submit.js +99 -17
  28. package/dist/lib/host-llm.js +16 -0
  29. package/dist/lib/install.js +7 -0
  30. package/dist/lib/lessons.js +48 -5
  31. package/dist/lib/mcp-server.js +90 -11
  32. package/dist/lib/receipt.js +102 -0
  33. package/dist/lib/session.js +52 -1
  34. package/dist/lib/share.js +131 -0
  35. package/dist/skills/accept/SKILL.md +64 -0
  36. package/dist/skills/devi/SKILL.md +22 -0
  37. package/dist/skills/evolve-check/SKILL.md +29 -13
  38. package/dist/skills/getting-started/SKILL.md +3 -0
  39. package/dist/skills/share/SKILL.md +54 -0
  40. package/package.json +6 -2
  41. package/scripts/validate-csvs.js +20 -0
  42. package/skills/accept/SKILL.md +64 -0
  43. package/skills/devi/SKILL.md +22 -0
  44. package/skills/evolve-check/SKILL.md +29 -13
  45. package/skills/getting-started/SKILL.md +3 -0
  46. package/skills/share/SKILL.md +54 -0
@@ -1,14 +1,22 @@
1
1
  'use strict';
2
2
 
3
3
  /**
4
- * Evolution metrics — track how much each persona has evolved.
4
+ * Evolution metrics — how much each persona is worth listening to.
5
5
  *
6
- * Computes per-persona evolution scores based on:
7
- * - Lessons extracted
8
- * - Outcomes confirmed (shipped/revised/blocked/missed)
9
- * - Prompt patches applied
10
- * - Reference rows added
11
- * - Router patches proposed/applied
6
+ * Scores are SIGNED. A persona starts at BASELINE (50) and moves in both
7
+ * directions, so a rejection actually costs something. Contributions:
8
+ *
9
+ * outcome shipped +15 | blocked_correctly +10 | revised -5 | missed -15
10
+ * feedback (rating - 3) * 4 → 5* = +8 ... 1* = -8
11
+ * lessons +10 each, positive-polarity only
12
+ * patches +20 proposed, +25 applied
13
+ *
14
+ * Scores are DERIVED, never stored — changing the weights re-scores history
15
+ * on the next read with no migration.
16
+ *
17
+ * Scope is GLOBAL per persona by default: lessons and patches are already
18
+ * global on disk, and outcomes are aggregated across every project. Pass
19
+ * { project } to scope down for debugging.
12
20
  *
13
21
  * CommonJS, 'use strict', var.
14
22
  */
@@ -21,128 +29,273 @@ var session = require('./session');
21
29
  var LESSONS_ROOT = path.join(os.homedir(), '.analyzthis_design', 'lessons');
22
30
  var EVOLUTION_ROOT = path.join(os.homedir(), '.analyzthis_design', 'evolution');
23
31
 
32
+ // Must match session.PERSONA_IDS — accept.js will happily record a note for
33
+ // any of them, and anything scored-but-not-accepted (or vice versa) silently
34
+ // drops designer feedback on the floor.
35
+ var PERSONAS = require('./session').PERSONA_IDS.slice();
36
+
37
+ var BASELINE = 50;
38
+
39
+ // Minimum signals (outcomes + feedback) before a score may leave the
40
+ // Baseline band. Without this, one bad note brands a persona "At risk"
41
+ // and Devi over-corrects on noise.
42
+ var MIN_EVIDENCE = 5;
43
+
44
+ // Ceiling on the lessons/patches bonus, so accumulated history can never
45
+ // make a persona immune to recent failures.
46
+ var BONUS_CAP = 12;
47
+
48
+ var OUTCOME_POINTS = {
49
+ shipped: 15,
50
+ blocked_correctly: 10,
51
+ revised: -5,
52
+ missed: -15,
53
+ };
54
+
55
+ var BANDS = [
56
+ { min: 80, name: 'Trusted' },
57
+ { min: 60, name: 'Reliable' },
58
+ { min: 40, name: 'Baseline' },
59
+ { min: 20, name: 'Developing' },
60
+ { min: 0, name: 'At risk' },
61
+ ];
62
+
24
63
  function readJsonl(filePath) {
25
64
  if (!fs.existsSync(filePath)) return [];
26
65
  var content = fs.readFileSync(filePath, 'utf8').trim();
27
66
  if (!content) return [];
28
- return content.split('\n').map(function(line) {
67
+ return content.split('\n').map(function (line) {
29
68
  try { return JSON.parse(line); } catch (e) { return null; }
30
69
  }).filter(Boolean);
31
70
  }
32
71
 
33
72
  function listJsonFiles(dir) {
34
73
  if (!fs.existsSync(dir)) return [];
35
- return fs.readdirSync(dir).filter(function(f) { return f.endsWith('.json'); });
74
+ return fs.readdirSync(dir).filter(function (f) { return f.endsWith('.json'); });
75
+ }
76
+
77
+ function clamp(n, lo, hi) {
78
+ return Math.max(lo, Math.min(hi, n));
79
+ }
80
+
81
+ function bandFor(score) {
82
+ for (var i = 0; i < BANDS.length; i++) {
83
+ if (score >= BANDS[i].min) return BANDS[i].name;
84
+ }
85
+ return 'At risk';
86
+ }
87
+
88
+ /**
89
+ * Load every patch once, so callers don't re-read the directory per persona.
90
+ */
91
+ function loadPatches() {
92
+ var files = listJsonFiles(EVOLUTION_ROOT);
93
+ var patches = [];
94
+ for (var i = 0; i < files.length; i++) {
95
+ try {
96
+ patches.push(JSON.parse(fs.readFileSync(path.join(EVOLUTION_ROOT, files[i]), 'utf8')));
97
+ } catch (e) { /* skip unreadable patch */ }
98
+ }
99
+ return patches;
100
+ }
101
+
102
+ function patchIsApplied(patch) {
103
+ return patch.applied === true || patch.dry_run === false;
36
104
  }
37
105
 
38
106
  /**
39
- * Compute evolution metrics for all personas in a project.
40
- * @param {string} projectId
41
- * @returns {object} Per-persona evolution metrics + aggregate
107
+ * Collect outcome + feedback signals for one persona across the given projects.
42
108
  */
43
- function computeEvolutionMetrics(projectId) {
44
- var state = session.show({ project: projectId }) || {};
45
- var personas = ['arjun', 'meera', 'priya', 'zara', 'noor', 'anuj', 'raj'];
109
+ function collectSignals(projectIds, persona) {
110
+ var outcomes = []; // { value, source }
111
+ var ratings = []; // numbers
112
+
113
+ for (var i = 0; i < projectIds.length; i++) {
114
+ var state = session.show({ project: projectIds[i] });
115
+ if (!state) continue;
116
+
117
+ var confirmed = (state.outcome && state.outcome.confirmed) || {};
118
+ var inferred = (state.outcome && state.outcome.inferred) || {};
119
+
120
+ // A confirmed outcome supersedes an inferred one for the same persona.
121
+ if (confirmed[persona] && confirmed[persona].value) {
122
+ outcomes.push({ value: confirmed[persona].value, source: 'confirmed' });
123
+ } else if (inferred[persona] && inferred[persona].value) {
124
+ outcomes.push({ value: inferred[persona].value, source: 'inferred' });
125
+ }
126
+
127
+ var log = state.feedback_log || [];
128
+ for (var j = 0; j < log.length; j++) {
129
+ if (log[j].persona !== persona) continue;
130
+ if (log[j].rating == null) continue;
131
+ ratings.push(Number(log[j].rating));
132
+ }
133
+ }
134
+
135
+ return { outcomes: outcomes, ratings: ratings };
136
+ }
137
+
138
+ /**
139
+ * Compute evolution metrics.
140
+ * @param {object|string} [opts] - { project, global } or a bare projectId (legacy)
141
+ * @returns {object} { personas, aggregate }
142
+ */
143
+ function computeEvolutionMetrics(opts) {
144
+ // Legacy call shape: computeEvolutionMetrics('project-id')
145
+ if (typeof opts === 'string') opts = { project: opts, global: false };
146
+ opts = opts || {};
147
+
148
+ var useGlobal = opts.global !== false && !opts.project;
149
+ var projectIds;
150
+ if (useGlobal) {
151
+ projectIds = session.listProjects();
152
+ } else {
153
+ projectIds = [opts.project || session.getProjectId()].filter(Boolean);
154
+ }
155
+
156
+ var patches = loadPatches();
46
157
  var metrics = {};
47
158
  var totalLessons = 0;
48
159
  var totalOutcomes = 0;
49
160
  var totalPatches = 0;
50
161
  var totalApplied = 0;
51
162
 
52
- for (var i = 0; i < personas.length; i++) {
53
- var persona = personas[i];
163
+ // Team-level patches (router) belong to nobody in particular.
164
+ var teamPatches = patches.filter(function (p) { return !p.persona; });
165
+
166
+ for (var i = 0; i < PERSONAS.length; i++) {
167
+ var persona = PERSONAS[i];
168
+
54
169
  var lessons = readJsonl(path.join(LESSONS_ROOT, persona + '.jsonl'));
55
- var lessonsCount = lessons.length;
56
-
57
- // Count confirmed outcomes for this persona
58
- var outcomesCount = 0;
59
- if (state.outcome && state.outcome.confirmed) {
60
- for (var pid in state.outcome.confirmed) {
61
- if (pid === persona) outcomesCount++;
62
- }
63
- }
64
- if (state.outcome && state.outcome.inferred) {
65
- for (var pid2 in state.outcome.inferred) {
66
- if (pid2 === persona) outcomesCount++;
67
- }
170
+ var positiveLessons = lessons.filter(function (l) {
171
+ if (l.polarity) return l.polarity === 'positive';
172
+ // Legacy lessons carry no polarity — fall back to the outcome value.
173
+ return l.outcome === 'shipped' || l.outcome === 'blocked_correctly';
174
+ });
175
+
176
+ var signals = collectSignals(projectIds, persona);
177
+
178
+ var personaPatches = patches.filter(function (p) { return p.persona === persona; });
179
+ var appliedPatches = personaPatches.filter(patchIsApplied);
180
+
181
+ // ── Signed contributions ──
182
+ var lessonPoints = positiveLessons.length * 10;
183
+
184
+ var outcomePoints = 0;
185
+ var outcomeBreakdown = { shipped: 0, blocked_correctly: 0, revised: 0, missed: 0 };
186
+ for (var o = 0; o < signals.outcomes.length; o++) {
187
+ var val = signals.outcomes[o].value;
188
+ if (OUTCOME_POINTS[val] === undefined) continue;
189
+ outcomePoints += OUTCOME_POINTS[val];
190
+ outcomeBreakdown[val] = (outcomeBreakdown[val] || 0) + 1;
68
191
  }
69
192
 
70
- // Count patches for this persona
71
- var patchesCount = 0;
72
- var appliedCount = 0;
73
- var patchFiles = listJsonFiles(EVOLUTION_ROOT);
74
- for (var j = 0; j < patchFiles.length; j++) {
75
- try {
76
- var patch = JSON.parse(fs.readFileSync(path.join(EVOLUTION_ROOT, patchFiles[j]), 'utf8'));
77
- if (patch.persona === persona) {
78
- patchesCount++;
79
- if (patch.applied === true || (patch.dry_run === false && patch.type !== 'router')) {
80
- appliedCount++;
81
- }
82
- }
83
- } catch (e) { /* ignore */ }
193
+ var feedbackPoints = 0;
194
+ for (var r = 0; r < signals.ratings.length; r++) {
195
+ feedbackPoints += (signals.ratings[r] - 3) * 4;
84
196
  }
197
+ var avgRating = signals.ratings.length
198
+ ? Math.round((signals.ratings.reduce(function (a, b) { return a + b; }, 0) / signals.ratings.length) * 10) / 10
199
+ : null;
85
200
 
86
- var lessonScore = Math.min(lessonsCount * 10, 100); // 10 pts per lesson, cap 100
87
- var outcomeScore = Math.min(outcomesCount * 15, 100); // 15 pts per outcome
88
- var patchScore = Math.min(patchesCount * 20, 100); // 20 pts per patch
89
- var appliedBonus = appliedCount * 25; // bonus for applied patches
201
+ var patchPoints = personaPatches.length * 20;
202
+ var appliedPoints = appliedPatches.length * 25;
90
203
 
91
- var totalScore = Math.min(lessonScore + outcomeScore + patchScore + appliedBonus, 100);
204
+ // Score on the RATE of good outcomes, not an unbounded sum. A running total
205
+ // saturates: bank enough early wins and later failures cannot move a
206
+ // persona off 100. Averaging per signal keeps recent behaviour visible
207
+ // forever, which is the whole point of punishing a bad note.
208
+ var signalCount = signals.outcomes.length + signals.ratings.length;
209
+ var signalPoints = outcomePoints + feedbackPoints;
210
+ var avgSignal = signalCount ? (signalPoints / signalCount) : 0;
92
211
 
93
- var level = 'Novice';
94
- if (totalScore >= 80) level = 'Expert';
95
- else if (totalScore >= 60) level = 'Advanced';
96
- else if (totalScore >= 40) level = 'Proficient';
97
- else if (totalScore >= 20) level = 'Developing';
212
+ // avgSignal spans about [-15, +15]; map onto 0-100 around the baseline.
213
+ var base = BASELINE + (avgSignal / 15) * 50;
214
+
215
+ // Accumulated lessons and patches are a real but secondary signal — cap
216
+ // their contribution so they can never re-introduce saturation.
217
+ var bonus = Math.min((lessonPoints + patchPoints + appliedPoints) * 0.25, BONUS_CAP);
218
+
219
+ var rawScore = base + bonus;
220
+ var score = clamp(Math.round(rawScore), 0, 100);
221
+
222
+ // ── Evidence gating ──
223
+ var evidenceCount = signals.outcomes.length + signals.ratings.length;
224
+ var sufficient = evidenceCount >= MIN_EVIDENCE;
225
+ var level;
226
+ if (sufficient) {
227
+ level = bandFor(score);
228
+ } else {
229
+ // Not enough signal to make a claim either way.
230
+ level = 'Baseline';
231
+ score = clamp(score, 40, 59);
232
+ }
98
233
 
99
234
  metrics[persona] = {
100
- score: totalScore,
235
+ score: score,
236
+ raw_score: Math.round(rawScore),
101
237
  level: level,
102
- lessons: lessonsCount,
103
- outcomes: outcomesCount,
104
- patches_proposed: patchesCount,
105
- patches_applied: appliedCount,
106
- lesson_score: lessonScore,
107
- outcome_score: outcomeScore,
108
- patch_score: patchScore,
109
- applied_bonus: appliedBonus,
238
+ sufficient_evidence: sufficient,
239
+ evidence_count: evidenceCount,
240
+ lessons: lessons.length,
241
+ lessons_positive: positiveLessons.length,
242
+ outcomes: signals.outcomes.length,
243
+ outcome_breakdown: outcomeBreakdown,
244
+ ratings: signals.ratings.length,
245
+ avg_rating: avgRating,
246
+ patches_proposed: personaPatches.length,
247
+ patches_applied: appliedPatches.length,
248
+ avg_signal: Math.round(avgSignal * 10) / 10,
249
+ contributions: {
250
+ baseline: BASELINE,
251
+ avg_signal: Math.round(avgSignal * 10) / 10,
252
+ from_rate: Math.round((base - BASELINE) * 10) / 10,
253
+ bonus: Math.round(bonus * 10) / 10,
254
+ raw_outcomes: outcomePoints,
255
+ raw_feedback: feedbackPoints,
256
+ raw_lessons: lessonPoints,
257
+ },
110
258
  };
111
259
 
112
- totalLessons += lessonsCount;
113
- totalOutcomes += outcomesCount;
114
- totalPatches += patchesCount;
115
- totalApplied += appliedCount;
260
+ totalLessons += lessons.length;
261
+ totalOutcomes += signals.outcomes.length;
262
+ totalPatches += personaPatches.length;
263
+ totalApplied += appliedPatches.length;
116
264
  }
117
265
 
118
- var avgScore = 0;
119
- for (var p in metrics) avgScore += metrics[p].score;
120
- avgScore = personas.length ? Math.round(avgScore / personas.length) : 0;
266
+ var sum = 0;
267
+ for (var p in metrics) sum += metrics[p].score;
268
+ var avgScore = PERSONAS.length ? Math.round(sum / PERSONAS.length) : 0;
121
269
 
122
270
  return {
123
271
  personas: metrics,
124
272
  aggregate: {
125
273
  average_score: avgScore,
274
+ evolution_level: bandFor(avgScore),
126
275
  total_lessons: totalLessons,
127
276
  total_outcomes: totalOutcomes,
128
- total_patches_proposed: totalPatches,
129
- total_patches_applied: totalApplied,
130
- evolution_level: avgScore >= 80 ? 'Expert Team' : avgScore >= 60 ? 'Advanced Team' : avgScore >= 40 ? 'Proficient Team' : avgScore >= 20 ? 'Developing Team' : 'Novice Team',
277
+ total_patches_proposed: totalPatches + teamPatches.length,
278
+ total_patches_applied: totalApplied + teamPatches.filter(patchIsApplied).length,
279
+ team_patches: teamPatches.length,
280
+ scope: useGlobal ? 'global' : projectIds.join(', '),
281
+ projects_scanned: projectIds.length,
131
282
  },
132
283
  };
133
284
  }
134
285
 
135
286
  /**
136
- * Format evolution metrics for Devi's prompt.
137
- * @param {object} metrics - Output of computeEvolutionMetrics
138
- * @returns {string} Human-readable summary
287
+ * Human-readable dashboard. Shows the signed breakdown so a designer can see
288
+ * WHY a persona fell, not just that it did.
139
289
  */
140
290
  function formatEvolutionSummary(metrics) {
141
291
  var lines = [];
142
292
  lines.push('-- Team Evolution Status --');
143
293
  lines.push('');
144
- lines.push('Overall: ' + metrics.aggregate.evolution_level + ' (avg score: ' + metrics.aggregate.average_score + '/100)');
145
- lines.push(' Lessons: ' + metrics.aggregate.total_lessons + ' | Outcomes: ' + metrics.aggregate.total_outcomes + ' | Patches: ' + metrics.aggregate.total_patches_proposed + ' proposed, ' + metrics.aggregate.total_patches_applied + ' applied');
294
+ lines.push('Overall: ' + metrics.aggregate.evolution_level + ' (avg ' + metrics.aggregate.average_score + '/100)');
295
+ lines.push(' Scope: ' + metrics.aggregate.scope + ' | Lessons: ' + metrics.aggregate.total_lessons +
296
+ ' | Outcomes: ' + metrics.aggregate.total_outcomes +
297
+ ' | Patches: ' + metrics.aggregate.total_patches_proposed + ' proposed, ' +
298
+ metrics.aggregate.total_patches_applied + ' applied');
146
299
  lines.push('');
147
300
 
148
301
  for (var persona in metrics.personas) {
@@ -150,26 +303,83 @@ function formatEvolutionSummary(metrics) {
150
303
  var filled = Math.floor(m.score / 10);
151
304
  var bar = '';
152
305
  for (var b = 0; b < 10; b++) { bar += b < filled ? '#' : '.'; }
153
- lines.push(' ' + persona + ': ' + m.level + ' (' + m.score + '/100) [' + bar + ']');
154
- lines.push(' Lessons: ' + m.lessons + ' | Outcomes: ' + m.outcomes + ' | Patches: ' + m.patches_proposed + ' (' + m.patches_applied + ' applied)');
306
+
307
+ var label = m.level + (m.sufficient_evidence ? '' : ' (insufficient evidence)');
308
+ var scoreCell = pad(m.score + '/100', 8);
309
+ lines.push(' ' + pad(persona, 9) + scoreCell + pad(label, 33) + '[' + bar + ']');
310
+
311
+ var parts = [];
312
+ var ob = m.outcome_breakdown;
313
+ if (ob.shipped) parts.push('shipped ' + ob.shipped + ' (+' + (ob.shipped * 15) + ')');
314
+ if (ob.blocked_correctly) parts.push('blocked ' + ob.blocked_correctly + ' (+' + (ob.blocked_correctly * 10) + ')');
315
+ if (ob.revised) parts.push('revised ' + ob.revised + ' (-' + (ob.revised * 5) + ')');
316
+ if (ob.missed) parts.push('missed ' + ob.missed + ' (-' + (ob.missed * 15) + ')');
317
+ if (m.avg_rating != null) {
318
+ parts.push('ratings avg ' + m.avg_rating + ' (' + signed(m.contributions.raw_feedback) + ')');
319
+ }
320
+ if (m.lessons_positive) parts.push('lessons ' + m.lessons_positive);
321
+ if (m.patches_proposed) parts.push('patches ' + m.patches_proposed);
322
+
323
+ lines.push(' ' + (parts.length ? parts.join(' · ') : 'no signals yet'));
324
+ lines.push(' ' + m.evidence_count + ' signal' + (m.evidence_count === 1 ? '' : 's') +
325
+ (m.sufficient_evidence ? ' — enough to act on' : ' — need ' + MIN_EVIDENCE + ' to act on'));
155
326
  }
156
327
  lines.push('');
157
328
  return lines.join('\n');
158
- lines.push('');
159
- return lines.join('\n');
329
+ }
330
+
331
+ function pad(s, n) {
332
+ s = String(s);
333
+ while (s.length < n) s += ' ';
334
+ return s;
335
+ }
336
+
337
+ function signed(n) {
338
+ return (n >= 0 ? '+' : '') + n;
339
+ }
340
+
341
+ /**
342
+ * Compact scoreboard injected into Devi's system prompt. Advisory only.
343
+ * Personas without enough evidence are omitted rather than guessed at.
344
+ */
345
+ function formatScoreboardForDevi(metrics) {
346
+ var rows = [];
347
+ for (var persona in metrics.personas) {
348
+ var m = metrics.personas[persona];
349
+ if (!m.sufficient_evidence) continue;
350
+ if (m.level === 'Baseline') continue;
351
+
352
+ var why;
353
+ var ob = m.outcome_breakdown;
354
+ if (m.level === 'Trusted' || m.level === 'Reliable') {
355
+ why = ob.shipped ? ob.shipped + ' shipped' : 'positive feedback';
356
+ } else {
357
+ why = (ob.missed ? ob.missed + ' missed' : '') +
358
+ (ob.missed && ob.revised ? ', ' : '') +
359
+ (ob.revised ? ob.revised + ' revised' : '') ||
360
+ 'low ratings';
361
+ }
362
+ rows.push(' ' + pad(persona, 8) + pad(m.level, 12) + why);
363
+ }
364
+
365
+ if (!rows.length) return '';
366
+
367
+ return [
368
+ 'Team scoreboard (advisory — from designer feedback on past runs):',
369
+ rows.join('\n'),
370
+ 'Lean on Trusted/Reliable personas and discount At risk ones when you synthesise,',
371
+ 'and say so in one line. Do NOT drop any persona from the run.',
372
+ ].join('\n');
160
373
  }
161
374
 
162
375
  /**
163
376
  * Check if evolution is ready to run (has enough data).
164
- * @param {string} projectId
165
- * @returns {object} { ready: boolean, reason: string, metrics: object }
166
377
  */
167
378
  function checkEvolutionReady(projectId) {
168
- var metrics = computeEvolutionMetrics(projectId);
379
+ var metrics = computeEvolutionMetrics(projectId ? { project: projectId } : undefined);
169
380
  var totalLessons = metrics.aggregate.total_lessons;
170
381
  var totalOutcomes = metrics.aggregate.total_outcomes;
171
382
 
172
- // Need at least 5 lessons for a prompt patch, 10 outcomes for router patch
173
383
  var minLessons = 5;
174
384
  var minOutcomes = 10;
175
385
 
@@ -191,7 +401,13 @@ function checkEvolutionReady(projectId) {
191
401
  module.exports = {
192
402
  computeEvolutionMetrics: computeEvolutionMetrics,
193
403
  formatEvolutionSummary: formatEvolutionSummary,
404
+ formatScoreboardForDevi: formatScoreboardForDevi,
194
405
  checkEvolutionReady: checkEvolutionReady,
406
+ PERSONAS: PERSONAS,
407
+ BASELINE: BASELINE,
408
+ MIN_EVIDENCE: MIN_EVIDENCE,
409
+ BONUS_CAP: BONUS_CAP,
410
+ OUTCOME_POINTS: OUTCOME_POINTS,
195
411
  LESSONS_ROOT: LESSONS_ROOT,
196
412
  EVOLUTION_ROOT: EVOLUTION_ROOT,
197
- };
413
+ };
@@ -176,9 +176,11 @@ function proposeRouterPatch(outcomes, dryRun) {
176
176
  var o = outcomes[i];
177
177
  if (!byTaskType[o.task_type]) byTaskType[o.task_type] = {};
178
178
  if (!byTaskType[o.task_type][o.persona]) byTaskType[o.task_type][o.persona] = { good: 0, bad: 0 };
179
- if (o.outcome === 'shipped' || o.outcome === 'blocked_correctly' || o.outcome === 'revised') {
179
+ // 'revised' is NOT a win the advice needed rework. Counting it as good
180
+ // was the same defect that let a rejection raise a persona's score.
181
+ if (o.outcome === 'shipped' || o.outcome === 'blocked_correctly') {
180
182
  byTaskType[o.task_type][o.persona].good++;
181
- } else if (o.outcome === 'missed') {
183
+ } else if (o.outcome === 'missed' || o.outcome === 'revised') {
182
184
  byTaskType[o.task_type][o.persona].bad++;
183
185
  }
184
186
  }
@@ -202,6 +204,7 @@ function proposeRouterPatch(outcomes, dryRun) {
202
204
  var patch = {
203
205
  id: generatePatchId(),
204
206
  type: 'router',
207
+ persona: null,
205
208
  task_type: tt,
206
209
  description: 'Route ' + tt + ' primarily to ' + best + ' based on outcomes',
207
210
  suggested_route_to: [best],