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.
- package/HOW-TO-USE.md +26 -8
- package/README.md +48 -4
- package/agents/cards/anuj.md +13 -0
- package/agents/cards/arjun.md +13 -0
- package/agents/cards/devi.md +22 -0
- package/agents/cards/kavi.md +13 -0
- package/agents/cards/meera.md +13 -0
- package/agents/cards/noor.md +13 -0
- package/agents/cards/priya.md +13 -0
- package/agents/cards/raj.md +13 -0
- package/agents/cards/zara.md +13 -0
- package/dist/HOW-TO-USE.md +26 -8
- package/dist/README.md +48 -4
- package/dist/agents/cards/anuj.md +13 -0
- package/dist/agents/cards/arjun.md +13 -0
- package/dist/agents/cards/devi.md +22 -0
- package/dist/agents/cards/kavi.md +13 -0
- package/dist/agents/cards/meera.md +13 -0
- package/dist/agents/cards/noor.md +13 -0
- package/dist/agents/cards/priya.md +13 -0
- package/dist/agents/cards/raj.md +13 -0
- package/dist/agents/cards/zara.md +13 -0
- package/dist/bin/cli.js +184 -0
- package/dist/lib/accept.js +108 -0
- package/dist/lib/evolution-metrics.js +303 -87
- package/dist/lib/evolve.js +5 -2
- package/dist/lib/feedback-submit.js +99 -17
- package/dist/lib/host-llm.js +16 -0
- package/dist/lib/install.js +7 -0
- package/dist/lib/lessons.js +48 -5
- package/dist/lib/mcp-server.js +90 -11
- package/dist/lib/receipt.js +102 -0
- package/dist/lib/session.js +52 -1
- package/dist/lib/share.js +131 -0
- package/dist/skills/accept/SKILL.md +64 -0
- package/dist/skills/devi/SKILL.md +22 -0
- package/dist/skills/evolve-check/SKILL.md +29 -13
- package/dist/skills/getting-started/SKILL.md +3 -0
- package/dist/skills/share/SKILL.md +54 -0
- package/package.json +6 -2
- package/scripts/validate-csvs.js +20 -0
- package/skills/accept/SKILL.md +64 -0
- package/skills/devi/SKILL.md +22 -0
- package/skills/evolve-check/SKILL.md +29 -13
- package/skills/getting-started/SKILL.md +3 -0
- package/skills/share/SKILL.md +54 -0
|
@@ -1,14 +1,22 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Evolution metrics —
|
|
4
|
+
* Evolution metrics — how much each persona is worth listening to.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* -
|
|
10
|
-
* -
|
|
11
|
-
* -
|
|
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
|
-
*
|
|
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
|
|
44
|
-
var
|
|
45
|
-
var
|
|
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
|
-
|
|
53
|
-
|
|
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
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
71
|
-
var
|
|
72
|
-
|
|
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
|
|
87
|
-
var
|
|
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
|
-
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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:
|
|
235
|
+
score: score,
|
|
236
|
+
raw_score: Math.round(rawScore),
|
|
101
237
|
level: level,
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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 +=
|
|
113
|
-
totalOutcomes +=
|
|
114
|
-
totalPatches +=
|
|
115
|
-
totalApplied +=
|
|
260
|
+
totalLessons += lessons.length;
|
|
261
|
+
totalOutcomes += signals.outcomes.length;
|
|
262
|
+
totalPatches += personaPatches.length;
|
|
263
|
+
totalApplied += appliedPatches.length;
|
|
116
264
|
}
|
|
117
265
|
|
|
118
|
-
var
|
|
119
|
-
for (var p in metrics)
|
|
120
|
-
avgScore =
|
|
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
|
-
|
|
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
|
-
*
|
|
137
|
-
*
|
|
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
|
|
145
|
-
lines.push('
|
|
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
|
-
|
|
154
|
-
|
|
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
|
-
|
|
159
|
-
|
|
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
|
+
};
|
package/dist/lib/evolve.js
CHANGED
|
@@ -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
|
-
|
|
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],
|