analyzthis_design 2.0.1 → 2.1.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/README.md +10 -5
- package/agents/cards/evolve-check.md +38 -0
- package/agents/manifests/evolve-check.json +16 -0
- package/dist/README.md +10 -5
- package/dist/agents/cards/evolve-check.md +38 -0
- package/dist/agents/manifests/evolve-check.json +16 -0
- package/dist/bin/cli.js +1225 -1
- package/dist/lib/cache.js +111 -1
- package/dist/lib/chunk-executor.js +219 -1
- package/dist/lib/chunk-models.js +228 -1
- package/dist/lib/chunk-planner.js +328 -1
- package/dist/lib/chunk-router.js +66 -1
- package/dist/lib/chunk-run.js +199 -1
- package/dist/lib/chunk-synthesis.js +176 -1
- package/dist/lib/chunk-telemetry.js +88 -1
- package/dist/lib/collect.js +858 -1
- package/dist/lib/cost.js +119 -1
- package/dist/lib/dedup.js +167 -1
- package/dist/lib/deliberation.js +721 -1
- package/dist/lib/design-spec.js +236 -1
- package/dist/lib/evolution-metrics.js +197 -0
- package/dist/lib/evolve.js +361 -1
- package/dist/lib/export.js +77 -1
- package/dist/lib/feedback-submit.js +324 -1
- package/dist/lib/feedback.js +182 -1
- package/dist/lib/host-llm.js +251 -1
- package/dist/lib/install.js +301 -1
- package/dist/lib/knowledge.js +384 -1
- package/dist/lib/lessons.js +217 -1
- package/dist/lib/moodboard.js +563 -1
- package/dist/lib/orchestrator/run.js +935 -1
- package/dist/lib/outcome.js +193 -1
- package/dist/lib/platforms.js +166 -1
- package/dist/lib/provider.js +57 -1
- package/dist/lib/query-expander.js +83 -1
- package/dist/lib/ranker.js +105 -1
- package/dist/lib/reference-pack.js +221 -1
- package/dist/lib/research.js +143 -1
- package/dist/lib/retrieve.js +131 -1
- package/dist/lib/session.js +185 -1
- package/dist/lib/source-discovery.js +486 -1
- package/dist/lib/synthesis.js +155 -1
- package/dist/lib/token-gate.js +46 -1
- package/dist/skills/evolve-check/SKILL.md +106 -0
- package/package.json +3 -6
- package/skills/evolve-check/SKILL.md +106 -0
package/dist/lib/design-spec.js
CHANGED
|
@@ -1,2 +1,237 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
'use strict';
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* DesignSpec — parse, validate, and persist machine-readable design contracts.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const session = require('./session');
|
|
11
|
+
|
|
12
|
+
const SCHEMA_PATH = path.join(__dirname, '..', 'agents', 'design-spec-schema.json');
|
|
13
|
+
|
|
14
|
+
function loadSchema() {
|
|
15
|
+
try {
|
|
16
|
+
return JSON.parse(fs.readFileSync(SCHEMA_PATH, 'utf8'));
|
|
17
|
+
} catch {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Extract ```design-spec ... ``` block from markdown/text. */
|
|
23
|
+
function parseFromText(text) {
|
|
24
|
+
if (!text) return null;
|
|
25
|
+
const m = String(text).match(/```design-spec\s*\n([\s\S]*?)```/i);
|
|
26
|
+
if (!m) return null;
|
|
27
|
+
try {
|
|
28
|
+
return JSON.parse(m[1].trim());
|
|
29
|
+
} catch {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function validateDesignSpec(spec) {
|
|
35
|
+
const errors = [];
|
|
36
|
+
if (!spec || typeof spec !== 'object') {
|
|
37
|
+
return { valid: false, errors: ['Spec must be a JSON object'] };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const required = [
|
|
41
|
+
'version', 'screen_name', 'status', 'intent', 'information_hierarchy',
|
|
42
|
+
'layout', 'tokens', 'components', 'states',
|
|
43
|
+
];
|
|
44
|
+
for (const key of required) {
|
|
45
|
+
if (spec[key] === undefined || spec[key] === null) errors.push(`Missing required field: ${key}`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (spec.version && spec.version !== '1.0') {
|
|
49
|
+
errors.push(`Unsupported version "${spec.version}" — use "1.0"`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (spec.status && !['draft', 'spec_review', 'ship', 'revise'].includes(spec.status)) {
|
|
53
|
+
errors.push(`Invalid status "${spec.status}"`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (Array.isArray(spec.information_hierarchy) && spec.information_hierarchy.length === 0) {
|
|
57
|
+
errors.push('information_hierarchy must have at least one ranked item');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (spec.intent && !spec.intent.primary_action) {
|
|
61
|
+
errors.push('intent.primary_action is required');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (spec.layout && (!Array.isArray(spec.layout.regions) || spec.layout.regions.length === 0)) {
|
|
65
|
+
errors.push('layout.regions must be a non-empty array');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (spec.tokens) {
|
|
69
|
+
for (const group of ['colors', 'typography', 'spacing']) {
|
|
70
|
+
if (!spec.tokens[group] || typeof spec.tokens[group] !== 'object') {
|
|
71
|
+
errors.push(`tokens.${group} is required`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
// Flag invented hex when DS gate expects tokens
|
|
75
|
+
if (spec.tokens.colors) {
|
|
76
|
+
for (const [k, v] of Object.entries(spec.tokens.colors)) {
|
|
77
|
+
if (/^#[0-9a-f]{3,8}$/i.test(String(v)) && !spec.tokens.source) {
|
|
78
|
+
errors.push(`tokens.colors.${k} uses raw hex "${v}" — prefer CSS vars or Tailwind tokens from knowledge bank / tailwind.config`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (!Array.isArray(spec.components) || spec.components.length === 0) {
|
|
85
|
+
errors.push('components must be a non-empty array');
|
|
86
|
+
} else {
|
|
87
|
+
for (const [i, c] of spec.components.entries()) {
|
|
88
|
+
if (!c.component) errors.push(`components[${i}].component is required`);
|
|
89
|
+
if (!c.region) errors.push(`components[${i}].region is required`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (spec.states) {
|
|
94
|
+
for (const s of ['empty', 'loading', 'error', 'success']) {
|
|
95
|
+
if (!spec.states[s] || !String(spec.states[s]).trim()) {
|
|
96
|
+
errors.push(`states.${s} is required`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Rank #1 component should exist with hierarchy_rank 1
|
|
102
|
+
if (Array.isArray(spec.components) && Array.isArray(spec.information_hierarchy) && spec.information_hierarchy.length) {
|
|
103
|
+
const hasRank1 = spec.components.some((c) => c.hierarchy_rank === 1);
|
|
104
|
+
if (!hasRank1) {
|
|
105
|
+
errors.push('At least one component must set hierarchy_rank: 1 matching information_hierarchy rank #1');
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return { valid: errors.length === 0, errors };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function saveToSession(spec, { project, merge = true } = {}) {
|
|
113
|
+
const projectId = project || session.getProjectId();
|
|
114
|
+
const validation = validateDesignSpec(spec);
|
|
115
|
+
const state = session.show({ project: projectId }) || session.init({ project: projectId });
|
|
116
|
+
const patch = {
|
|
117
|
+
design_spec: merge && state.design_spec
|
|
118
|
+
? { ...state.design_spec, ...spec, updated_at: new Date().toISOString() }
|
|
119
|
+
: { ...spec, updated_at: new Date().toISOString() },
|
|
120
|
+
digest: {
|
|
121
|
+
...(state.digest || {}),
|
|
122
|
+
design_spec_status: spec.status || 'draft',
|
|
123
|
+
hierarchy_top3: (spec.information_hierarchy || []).slice(0, 3),
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
session.update({ project: projectId, patch });
|
|
127
|
+
return { projectId, validation, design_spec: patch.design_spec };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function showFromSession({ project } = {}) {
|
|
131
|
+
const projectId = project || session.getProjectId();
|
|
132
|
+
const state = session.show({ project: projectId });
|
|
133
|
+
if (!state || !state.design_spec) return null;
|
|
134
|
+
return { projectId, design_spec: state.design_spec };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function validateFile(filePath) {
|
|
138
|
+
const raw = fs.readFileSync(path.resolve(filePath), 'utf8');
|
|
139
|
+
let spec = null;
|
|
140
|
+
if (filePath.endsWith('.json')) {
|
|
141
|
+
spec = JSON.parse(raw);
|
|
142
|
+
} else {
|
|
143
|
+
spec = parseFromText(raw) || JSON.parse(raw);
|
|
144
|
+
}
|
|
145
|
+
return { spec, ...validateDesignSpec(spec) };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Markdown template for host LLM to fill. */
|
|
149
|
+
function templateMarkdown() {
|
|
150
|
+
return `\`\`\`design-spec
|
|
151
|
+
{
|
|
152
|
+
"version": "1.0",
|
|
153
|
+
"screen_name": "",
|
|
154
|
+
"status": "draft",
|
|
155
|
+
"intent": {
|
|
156
|
+
"primary_user_task": "",
|
|
157
|
+
"north_star_metric": "",
|
|
158
|
+
"primary_action": "",
|
|
159
|
+
"business_framing": ""
|
|
160
|
+
},
|
|
161
|
+
"information_hierarchy": [
|
|
162
|
+
"1. [most important — primary action or data]",
|
|
163
|
+
"2. [supporting context]",
|
|
164
|
+
"3. [secondary]"
|
|
165
|
+
],
|
|
166
|
+
"layout": {
|
|
167
|
+
"grid": "12-col",
|
|
168
|
+
"max_width": "max-w-7xl",
|
|
169
|
+
"breakpoints": ["mobile", "desktop"],
|
|
170
|
+
"nav_level": "L2",
|
|
171
|
+
"regions": [
|
|
172
|
+
{ "name": "header", "span": "full", "content": "Page title + primary CTA" },
|
|
173
|
+
{ "name": "main", "span": "8/12", "content": "Primary content" },
|
|
174
|
+
{ "name": "aside", "span": "4/12", "content": "Secondary panel" }
|
|
175
|
+
]
|
|
176
|
+
},
|
|
177
|
+
"tokens": {
|
|
178
|
+
"source": "tailwind.config | css-vars | knowledge-bank",
|
|
179
|
+
"colors": {
|
|
180
|
+
"primary": "bg-primary text-primary-foreground",
|
|
181
|
+
"background": "bg-background",
|
|
182
|
+
"muted": "text-muted-foreground"
|
|
183
|
+
},
|
|
184
|
+
"typography": {
|
|
185
|
+
"page_title": "text-2xl font-semibold tracking-tight",
|
|
186
|
+
"body": "text-sm leading-relaxed",
|
|
187
|
+
"label": "text-xs font-medium uppercase tracking-wide"
|
|
188
|
+
},
|
|
189
|
+
"spacing": {
|
|
190
|
+
"page_padding": "p-6 md:p-8",
|
|
191
|
+
"section_gap": "gap-6",
|
|
192
|
+
"stack_gap": "space-y-4"
|
|
193
|
+
}
|
|
194
|
+
},
|
|
195
|
+
"components": [
|
|
196
|
+
{
|
|
197
|
+
"region": "header",
|
|
198
|
+
"component": "Button",
|
|
199
|
+
"import_path": "@/components/ui/button",
|
|
200
|
+
"variant": "default",
|
|
201
|
+
"hierarchy_rank": 1,
|
|
202
|
+
"props": { "children": "Primary action label" }
|
|
203
|
+
}
|
|
204
|
+
],
|
|
205
|
+
"states": {
|
|
206
|
+
"empty": "EmptyState with one CTA",
|
|
207
|
+
"loading": "Skeleton rows matching final layout",
|
|
208
|
+
"error": "Inline alert + retry",
|
|
209
|
+
"success": "Toast + updated primary data"
|
|
210
|
+
},
|
|
211
|
+
"motion": { "enabled": false, "notes": "" },
|
|
212
|
+
"do": ["Single primary CTA above the fold", "Use existing shadcn components only"],
|
|
213
|
+
"dont": ["Invent hex colors", "Add second primary button"],
|
|
214
|
+
"wireframe_ref": "synthesized",
|
|
215
|
+
"delight_moment": "",
|
|
216
|
+
"effort_estimate": "M",
|
|
217
|
+
"citations": [],
|
|
218
|
+
"spec_verdict": {
|
|
219
|
+
"arjun_visual": "pending",
|
|
220
|
+
"ds_gate": "pending",
|
|
221
|
+
"hierarchy_gate": "pending",
|
|
222
|
+
"notes": ""
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
\`\`\``;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
module.exports = {
|
|
229
|
+
loadSchema,
|
|
230
|
+
parseFromText,
|
|
231
|
+
validateDesignSpec,
|
|
232
|
+
saveToSession,
|
|
233
|
+
showFromSession,
|
|
234
|
+
validateFile,
|
|
235
|
+
templateMarkdown,
|
|
236
|
+
SCHEMA_PATH,
|
|
237
|
+
};
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Evolution metrics — track how much each persona has evolved.
|
|
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
|
|
12
|
+
*
|
|
13
|
+
* CommonJS, 'use strict', var.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
var fs = require('fs');
|
|
17
|
+
var path = require('path');
|
|
18
|
+
var os = require('os');
|
|
19
|
+
var session = require('./session');
|
|
20
|
+
|
|
21
|
+
var LESSONS_ROOT = path.join(os.homedir(), '.analyzthis_design', 'lessons');
|
|
22
|
+
var EVOLUTION_ROOT = path.join(os.homedir(), '.analyzthis_design', 'evolution');
|
|
23
|
+
|
|
24
|
+
function readJsonl(filePath) {
|
|
25
|
+
if (!fs.existsSync(filePath)) return [];
|
|
26
|
+
var content = fs.readFileSync(filePath, 'utf8').trim();
|
|
27
|
+
if (!content) return [];
|
|
28
|
+
return content.split('\n').map(function(line) {
|
|
29
|
+
try { return JSON.parse(line); } catch (e) { return null; }
|
|
30
|
+
}).filter(Boolean);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function listJsonFiles(dir) {
|
|
34
|
+
if (!fs.existsSync(dir)) return [];
|
|
35
|
+
return fs.readdirSync(dir).filter(function(f) { return f.endsWith('.json'); });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Compute evolution metrics for all personas in a project.
|
|
40
|
+
* @param {string} projectId
|
|
41
|
+
* @returns {object} Per-persona evolution metrics + aggregate
|
|
42
|
+
*/
|
|
43
|
+
function computeEvolutionMetrics(projectId) {
|
|
44
|
+
var state = session.show({ project: projectId }) || {};
|
|
45
|
+
var personas = ['arjun', 'meera', 'priya', 'zara', 'noor', 'anuj', 'raj'];
|
|
46
|
+
var metrics = {};
|
|
47
|
+
var totalLessons = 0;
|
|
48
|
+
var totalOutcomes = 0;
|
|
49
|
+
var totalPatches = 0;
|
|
50
|
+
var totalApplied = 0;
|
|
51
|
+
|
|
52
|
+
for (var i = 0; i < personas.length; i++) {
|
|
53
|
+
var persona = personas[i];
|
|
54
|
+
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
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
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 */ }
|
|
84
|
+
}
|
|
85
|
+
|
|
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
|
|
90
|
+
|
|
91
|
+
var totalScore = Math.min(lessonScore + outcomeScore + patchScore + appliedBonus, 100);
|
|
92
|
+
|
|
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';
|
|
98
|
+
|
|
99
|
+
metrics[persona] = {
|
|
100
|
+
score: totalScore,
|
|
101
|
+
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,
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
totalLessons += lessonsCount;
|
|
113
|
+
totalOutcomes += outcomesCount;
|
|
114
|
+
totalPatches += patchesCount;
|
|
115
|
+
totalApplied += appliedCount;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
var avgScore = 0;
|
|
119
|
+
for (var p in metrics) avgScore += metrics[p].score;
|
|
120
|
+
avgScore = personas.length ? Math.round(avgScore / personas.length) : 0;
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
personas: metrics,
|
|
124
|
+
aggregate: {
|
|
125
|
+
average_score: avgScore,
|
|
126
|
+
total_lessons: totalLessons,
|
|
127
|
+
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',
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Format evolution metrics for Devi's prompt.
|
|
137
|
+
* @param {object} metrics - Output of computeEvolutionMetrics
|
|
138
|
+
* @returns {string} Human-readable summary
|
|
139
|
+
*/
|
|
140
|
+
function formatEvolutionSummary(metrics) {
|
|
141
|
+
var lines = [];
|
|
142
|
+
lines.push('-- Team Evolution Status --');
|
|
143
|
+
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');
|
|
146
|
+
lines.push('');
|
|
147
|
+
|
|
148
|
+
for (var persona in metrics.personas) {
|
|
149
|
+
var m = metrics.personas[persona];
|
|
150
|
+
var filled = Math.floor(m.score / 10);
|
|
151
|
+
var bar = '';
|
|
152
|
+
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)');
|
|
155
|
+
}
|
|
156
|
+
lines.push('');
|
|
157
|
+
return lines.join('\n');
|
|
158
|
+
lines.push('');
|
|
159
|
+
return lines.join('\n');
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Check if evolution is ready to run (has enough data).
|
|
164
|
+
* @param {string} projectId
|
|
165
|
+
* @returns {object} { ready: boolean, reason: string, metrics: object }
|
|
166
|
+
*/
|
|
167
|
+
function checkEvolutionReady(projectId) {
|
|
168
|
+
var metrics = computeEvolutionMetrics(projectId);
|
|
169
|
+
var totalLessons = metrics.aggregate.total_lessons;
|
|
170
|
+
var totalOutcomes = metrics.aggregate.total_outcomes;
|
|
171
|
+
|
|
172
|
+
// Need at least 5 lessons for a prompt patch, 10 outcomes for router patch
|
|
173
|
+
var minLessons = 5;
|
|
174
|
+
var minOutcomes = 10;
|
|
175
|
+
|
|
176
|
+
if (totalLessons >= minLessons || totalOutcomes >= minOutcomes) {
|
|
177
|
+
return {
|
|
178
|
+
ready: true,
|
|
179
|
+
reason: 'Enough data accumulated (' + totalLessons + ' lessons, ' + totalOutcomes + ' outcomes). Evolution can propose patches.',
|
|
180
|
+
metrics: metrics,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
ready: false,
|
|
186
|
+
reason: 'Need more data: ' + totalLessons + '/' + minLessons + ' lessons, ' + totalOutcomes + '/' + minOutcomes + ' outcomes.',
|
|
187
|
+
metrics: metrics,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
module.exports = {
|
|
192
|
+
computeEvolutionMetrics: computeEvolutionMetrics,
|
|
193
|
+
formatEvolutionSummary: formatEvolutionSummary,
|
|
194
|
+
checkEvolutionReady: checkEvolutionReady,
|
|
195
|
+
LESSONS_ROOT: LESSONS_ROOT,
|
|
196
|
+
EVOLUTION_ROOT: EVOLUTION_ROOT,
|
|
197
|
+
};
|