popeye-cli 1.0.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/.env.example +25 -0
- package/.prettierrc +8 -0
- package/README.md +320 -0
- package/dist/adapters/claude.d.ts +82 -0
- package/dist/adapters/claude.d.ts.map +1 -0
- package/dist/adapters/claude.js +230 -0
- package/dist/adapters/claude.js.map +1 -0
- package/dist/adapters/openai.d.ts +48 -0
- package/dist/adapters/openai.d.ts.map +1 -0
- package/dist/adapters/openai.js +257 -0
- package/dist/adapters/openai.js.map +1 -0
- package/dist/auth/claude.d.ts +44 -0
- package/dist/auth/claude.d.ts.map +1 -0
- package/dist/auth/claude.js +139 -0
- package/dist/auth/claude.js.map +1 -0
- package/dist/auth/index.d.ts +61 -0
- package/dist/auth/index.d.ts.map +1 -0
- package/dist/auth/index.js +141 -0
- package/dist/auth/index.js.map +1 -0
- package/dist/auth/keychain.d.ts +66 -0
- package/dist/auth/keychain.d.ts.map +1 -0
- package/dist/auth/keychain.js +125 -0
- package/dist/auth/keychain.js.map +1 -0
- package/dist/auth/openai-entry.d.ts +9 -0
- package/dist/auth/openai-entry.d.ts.map +1 -0
- package/dist/auth/openai-entry.js +410 -0
- package/dist/auth/openai-entry.js.map +1 -0
- package/dist/auth/openai.d.ts +71 -0
- package/dist/auth/openai.d.ts.map +1 -0
- package/dist/auth/openai.js +212 -0
- package/dist/auth/openai.js.map +1 -0
- package/dist/auth/server.d.ts +32 -0
- package/dist/auth/server.d.ts.map +1 -0
- package/dist/auth/server.js +213 -0
- package/dist/auth/server.js.map +1 -0
- package/dist/cli/commands/auth.d.ts +10 -0
- package/dist/cli/commands/auth.d.ts.map +1 -0
- package/dist/cli/commands/auth.js +162 -0
- package/dist/cli/commands/auth.js.map +1 -0
- package/dist/cli/commands/config.d.ts +10 -0
- package/dist/cli/commands/config.d.ts.map +1 -0
- package/dist/cli/commands/config.js +215 -0
- package/dist/cli/commands/config.js.map +1 -0
- package/dist/cli/commands/create.d.ts +10 -0
- package/dist/cli/commands/create.d.ts.map +1 -0
- package/dist/cli/commands/create.js +240 -0
- package/dist/cli/commands/create.js.map +1 -0
- package/dist/cli/commands/index.d.ts +10 -0
- package/dist/cli/commands/index.d.ts.map +1 -0
- package/dist/cli/commands/index.js +10 -0
- package/dist/cli/commands/index.js.map +1 -0
- package/dist/cli/commands/resume.d.ts +18 -0
- package/dist/cli/commands/resume.d.ts.map +1 -0
- package/dist/cli/commands/resume.js +241 -0
- package/dist/cli/commands/resume.js.map +1 -0
- package/dist/cli/commands/status.d.ts +18 -0
- package/dist/cli/commands/status.d.ts.map +1 -0
- package/dist/cli/commands/status.js +154 -0
- package/dist/cli/commands/status.js.map +1 -0
- package/dist/cli/index.d.ts +17 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +71 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/interactive.d.ts +9 -0
- package/dist/cli/interactive.d.ts.map +1 -0
- package/dist/cli/interactive.js +330 -0
- package/dist/cli/interactive.js.map +1 -0
- package/dist/cli/output.d.ts +182 -0
- package/dist/cli/output.d.ts.map +1 -0
- package/dist/cli/output.js +355 -0
- package/dist/cli/output.js.map +1 -0
- package/dist/config/defaults.d.ts +57 -0
- package/dist/config/defaults.d.ts.map +1 -0
- package/dist/config/defaults.js +103 -0
- package/dist/config/defaults.js.map +1 -0
- package/dist/config/index.d.ts +138 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +244 -0
- package/dist/config/index.js.map +1 -0
- package/dist/config/schema.d.ts +220 -0
- package/dist/config/schema.d.ts.map +1 -0
- package/dist/config/schema.js +141 -0
- package/dist/config/schema.js.map +1 -0
- package/dist/generators/index.d.ts +101 -0
- package/dist/generators/index.d.ts.map +1 -0
- package/dist/generators/index.js +200 -0
- package/dist/generators/index.js.map +1 -0
- package/dist/generators/python.d.ts +48 -0
- package/dist/generators/python.d.ts.map +1 -0
- package/dist/generators/python.js +262 -0
- package/dist/generators/python.js.map +1 -0
- package/dist/generators/templates/index.d.ts +6 -0
- package/dist/generators/templates/index.d.ts.map +1 -0
- package/dist/generators/templates/index.js +6 -0
- package/dist/generators/templates/index.js.map +1 -0
- package/dist/generators/templates/python.d.ts +53 -0
- package/dist/generators/templates/python.d.ts.map +1 -0
- package/dist/generators/templates/python.js +454 -0
- package/dist/generators/templates/python.js.map +1 -0
- package/dist/generators/templates/typescript.d.ts +53 -0
- package/dist/generators/templates/typescript.d.ts.map +1 -0
- package/dist/generators/templates/typescript.js +394 -0
- package/dist/generators/templates/typescript.js.map +1 -0
- package/dist/generators/typescript.d.ts +64 -0
- package/dist/generators/typescript.d.ts.map +1 -0
- package/dist/generators/typescript.js +271 -0
- package/dist/generators/typescript.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/dist/state/index.d.ts +168 -0
- package/dist/state/index.d.ts.map +1 -0
- package/dist/state/index.js +338 -0
- package/dist/state/index.js.map +1 -0
- package/dist/state/persistence.d.ts +91 -0
- package/dist/state/persistence.d.ts.map +1 -0
- package/dist/state/persistence.js +201 -0
- package/dist/state/persistence.js.map +1 -0
- package/dist/types/cli.d.ts +132 -0
- package/dist/types/cli.d.ts.map +1 -0
- package/dist/types/cli.js +17 -0
- package/dist/types/cli.js.map +1 -0
- package/dist/types/consensus.d.ts +111 -0
- package/dist/types/consensus.d.ts.map +1 -0
- package/dist/types/consensus.js +29 -0
- package/dist/types/consensus.js.map +1 -0
- package/dist/types/index.d.ts +9 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +13 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/project.d.ts +73 -0
- package/dist/types/project.d.ts.map +1 -0
- package/dist/types/project.js +55 -0
- package/dist/types/project.js.map +1 -0
- package/dist/types/workflow.d.ts +236 -0
- package/dist/types/workflow.d.ts.map +1 -0
- package/dist/types/workflow.js +74 -0
- package/dist/types/workflow.js.map +1 -0
- package/dist/workflow/consensus.d.ts +89 -0
- package/dist/workflow/consensus.d.ts.map +1 -0
- package/dist/workflow/consensus.js +220 -0
- package/dist/workflow/consensus.js.map +1 -0
- package/dist/workflow/execution-mode.d.ts +82 -0
- package/dist/workflow/execution-mode.d.ts.map +1 -0
- package/dist/workflow/execution-mode.js +346 -0
- package/dist/workflow/execution-mode.js.map +1 -0
- package/dist/workflow/index.d.ts +110 -0
- package/dist/workflow/index.d.ts.map +1 -0
- package/dist/workflow/index.js +283 -0
- package/dist/workflow/index.js.map +1 -0
- package/dist/workflow/plan-mode.d.ts +83 -0
- package/dist/workflow/plan-mode.d.ts.map +1 -0
- package/dist/workflow/plan-mode.js +241 -0
- package/dist/workflow/plan-mode.js.map +1 -0
- package/dist/workflow/test-runner.d.ts +87 -0
- package/dist/workflow/test-runner.d.ts.map +1 -0
- package/dist/workflow/test-runner.js +273 -0
- package/dist/workflow/test-runner.js.map +1 -0
- package/eslint.config.js +25 -0
- package/package.json +66 -0
- package/src/adapters/claude.ts +298 -0
- package/src/adapters/openai.ts +300 -0
- package/src/auth/claude.ts +166 -0
- package/src/auth/index.ts +171 -0
- package/src/auth/keychain.ts +138 -0
- package/src/auth/openai-entry.ts +410 -0
- package/src/auth/openai.ts +260 -0
- package/src/auth/server.ts +252 -0
- package/src/cli/commands/auth.ts +194 -0
- package/src/cli/commands/config.ts +241 -0
- package/src/cli/commands/create.ts +308 -0
- package/src/cli/commands/index.ts +10 -0
- package/src/cli/commands/resume.ts +304 -0
- package/src/cli/commands/status.ts +189 -0
- package/src/cli/index.ts +90 -0
- package/src/cli/interactive.ts +418 -0
- package/src/cli/output.ts +410 -0
- package/src/config/defaults.ts +114 -0
- package/src/config/index.ts +315 -0
- package/src/config/schema.ts +164 -0
- package/src/generators/index.ts +251 -0
- package/src/generators/python.ts +318 -0
- package/src/generators/templates/index.ts +6 -0
- package/src/generators/templates/python.ts +465 -0
- package/src/generators/templates/typescript.ts +417 -0
- package/src/generators/typescript.ts +340 -0
- package/src/index.ts +13 -0
- package/src/state/index.ts +454 -0
- package/src/state/persistence.ts +230 -0
- package/src/types/cli.ts +146 -0
- package/src/types/consensus.ts +116 -0
- package/src/types/index.ts +64 -0
- package/src/types/project.ts +85 -0
- package/src/types/workflow.ts +149 -0
- package/src/workflow/consensus.ts +299 -0
- package/src/workflow/execution-mode.ts +517 -0
- package/src/workflow/index.ts +396 -0
- package/src/workflow/plan-mode.ts +356 -0
- package/src/workflow/test-runner.ts +345 -0
- package/tests/adapters/openai.test.ts +145 -0
- package/tests/config/config.test.ts +208 -0
- package/tests/generators/generators.test.ts +185 -0
- package/tests/types/consensus.test.ts +152 -0
- package/tests/types/project.test.ts +134 -0
- package/tests/workflow/consensus.test.ts +221 -0
- package/tests/workflow/test-runner.test.ts +214 -0
- package/tsconfig.json +25 -0
- package/vitest.config.ts +22 -0
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Consensus workflow module
|
|
3
|
+
* Handles the iterative consensus-building process between Claude and OpenAI
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { ConsensusResult, ConsensusIteration, ConsensusConfig } from '../types/consensus.js';
|
|
7
|
+
import { DEFAULT_CONSENSUS_CONFIG } from '../types/consensus.js';
|
|
8
|
+
import { requestConsensus } from '../adapters/openai.js';
|
|
9
|
+
import { revisePlan } from '../adapters/claude.js';
|
|
10
|
+
import { recordConsensusIteration } from '../state/index.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Options for consensus iteration
|
|
14
|
+
*/
|
|
15
|
+
export interface ConsensusOptions {
|
|
16
|
+
projectDir: string;
|
|
17
|
+
config?: Partial<ConsensusConfig>;
|
|
18
|
+
onIteration?: (iteration: number, result: ConsensusResult) => void;
|
|
19
|
+
onRevision?: (iteration: number, revisedPlan: string) => void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Result of the consensus process
|
|
24
|
+
*/
|
|
25
|
+
export interface ConsensusProcessResult {
|
|
26
|
+
approved: boolean;
|
|
27
|
+
finalPlan: string;
|
|
28
|
+
finalScore: number;
|
|
29
|
+
iterations: ConsensusIteration[];
|
|
30
|
+
totalIterations: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Format a plan for consensus review
|
|
35
|
+
* Structures the plan in a way that's optimal for review
|
|
36
|
+
*
|
|
37
|
+
* @param plan - The raw plan content
|
|
38
|
+
* @param context - Project context
|
|
39
|
+
* @returns Formatted plan string
|
|
40
|
+
*/
|
|
41
|
+
export function formatPlanForReview(plan: string, context: string): string {
|
|
42
|
+
return `
|
|
43
|
+
## Development Plan
|
|
44
|
+
|
|
45
|
+
${plan}
|
|
46
|
+
|
|
47
|
+
## Project Context
|
|
48
|
+
|
|
49
|
+
${context}
|
|
50
|
+
`.trim();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Extract concerns from a consensus result for revision
|
|
55
|
+
*
|
|
56
|
+
* @param result - The consensus result
|
|
57
|
+
* @returns Array of concerns to address
|
|
58
|
+
*/
|
|
59
|
+
export function extractConcerns(result: ConsensusResult): string[] {
|
|
60
|
+
const concerns: string[] = [];
|
|
61
|
+
|
|
62
|
+
// Add explicit concerns
|
|
63
|
+
if (result.concerns && result.concerns.length > 0) {
|
|
64
|
+
concerns.push(...result.concerns);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Add recommendations as concerns to address
|
|
68
|
+
if (result.recommendations && result.recommendations.length > 0) {
|
|
69
|
+
concerns.push(...result.recommendations.map((r) => `Consider: ${r}`));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return concerns;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Check if consensus threshold is met
|
|
77
|
+
*
|
|
78
|
+
* @param score - The consensus score
|
|
79
|
+
* @param threshold - The threshold to meet (default from config)
|
|
80
|
+
* @returns True if threshold is met
|
|
81
|
+
*/
|
|
82
|
+
export function meetsThreshold(
|
|
83
|
+
score: number,
|
|
84
|
+
threshold: number = DEFAULT_CONSENSUS_CONFIG.threshold
|
|
85
|
+
): boolean {
|
|
86
|
+
return score >= threshold;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Iterate until consensus is reached
|
|
91
|
+
*
|
|
92
|
+
* @param initialPlan - The initial plan to review
|
|
93
|
+
* @param context - Project context
|
|
94
|
+
* @param options - Consensus options
|
|
95
|
+
* @returns The consensus process result
|
|
96
|
+
*/
|
|
97
|
+
export async function iterateUntilConsensus(
|
|
98
|
+
initialPlan: string,
|
|
99
|
+
context: string,
|
|
100
|
+
options: ConsensusOptions
|
|
101
|
+
): Promise<ConsensusProcessResult> {
|
|
102
|
+
const {
|
|
103
|
+
projectDir,
|
|
104
|
+
config = {},
|
|
105
|
+
onIteration,
|
|
106
|
+
onRevision,
|
|
107
|
+
} = options;
|
|
108
|
+
|
|
109
|
+
const {
|
|
110
|
+
threshold = DEFAULT_CONSENSUS_CONFIG.threshold,
|
|
111
|
+
maxIterations = DEFAULT_CONSENSUS_CONFIG.maxIterations,
|
|
112
|
+
} = config;
|
|
113
|
+
|
|
114
|
+
const iterations: ConsensusIteration[] = [];
|
|
115
|
+
let currentPlan = initialPlan;
|
|
116
|
+
let iteration = 0;
|
|
117
|
+
|
|
118
|
+
while (iteration < maxIterations) {
|
|
119
|
+
iteration++;
|
|
120
|
+
|
|
121
|
+
// Request consensus review from OpenAI
|
|
122
|
+
const consensusResult = await requestConsensus(currentPlan, context, config);
|
|
123
|
+
|
|
124
|
+
// Record the iteration
|
|
125
|
+
const iterationRecord: ConsensusIteration = {
|
|
126
|
+
iteration,
|
|
127
|
+
plan: currentPlan,
|
|
128
|
+
result: consensusResult,
|
|
129
|
+
timestamp: new Date().toISOString(),
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
iterations.push(iterationRecord);
|
|
133
|
+
|
|
134
|
+
// Save to project state
|
|
135
|
+
await recordConsensusIteration(projectDir, iterationRecord);
|
|
136
|
+
|
|
137
|
+
// Notify callback
|
|
138
|
+
if (onIteration) {
|
|
139
|
+
onIteration(iteration, consensusResult);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Check if we've reached consensus
|
|
143
|
+
if (meetsThreshold(consensusResult.score, threshold)) {
|
|
144
|
+
return {
|
|
145
|
+
approved: true,
|
|
146
|
+
finalPlan: currentPlan,
|
|
147
|
+
finalScore: consensusResult.score,
|
|
148
|
+
iterations,
|
|
149
|
+
totalIterations: iteration,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// If not at max iterations, revise the plan
|
|
154
|
+
if (iteration < maxIterations) {
|
|
155
|
+
const concerns = extractConcerns(consensusResult);
|
|
156
|
+
|
|
157
|
+
// Use Claude to revise the plan
|
|
158
|
+
const revisionResult = await revisePlan(
|
|
159
|
+
currentPlan,
|
|
160
|
+
consensusResult.analysis,
|
|
161
|
+
concerns
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
if (revisionResult.success && revisionResult.response) {
|
|
165
|
+
currentPlan = revisionResult.response;
|
|
166
|
+
|
|
167
|
+
if (onRevision) {
|
|
168
|
+
onRevision(iteration, currentPlan);
|
|
169
|
+
}
|
|
170
|
+
} else {
|
|
171
|
+
// If revision fails, try to continue with current plan
|
|
172
|
+
console.warn(`Plan revision failed at iteration ${iteration}:`, revisionResult.error);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Max iterations reached without consensus
|
|
178
|
+
const lastIteration = iterations[iterations.length - 1];
|
|
179
|
+
return {
|
|
180
|
+
approved: false,
|
|
181
|
+
finalPlan: currentPlan,
|
|
182
|
+
finalScore: lastIteration?.result.score || 0,
|
|
183
|
+
iterations,
|
|
184
|
+
totalIterations: iteration,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Get a summary of the consensus process
|
|
190
|
+
*
|
|
191
|
+
* @param result - The consensus process result
|
|
192
|
+
* @returns Human-readable summary
|
|
193
|
+
*/
|
|
194
|
+
export function summarizeConsensusProcess(result: ConsensusProcessResult): string {
|
|
195
|
+
const lines: string[] = [];
|
|
196
|
+
|
|
197
|
+
lines.push(`## Consensus Summary`);
|
|
198
|
+
lines.push('');
|
|
199
|
+
lines.push(`**Status:** ${result.approved ? 'APPROVED' : 'NOT APPROVED'}`);
|
|
200
|
+
lines.push(`**Final Score:** ${result.finalScore}%`);
|
|
201
|
+
lines.push(`**Total Iterations:** ${result.totalIterations}`);
|
|
202
|
+
lines.push('');
|
|
203
|
+
|
|
204
|
+
lines.push(`### Iteration History`);
|
|
205
|
+
lines.push('');
|
|
206
|
+
|
|
207
|
+
for (const iteration of result.iterations) {
|
|
208
|
+
lines.push(`#### Iteration ${iteration.iteration}`);
|
|
209
|
+
lines.push(`- Score: ${iteration.result.score}%`);
|
|
210
|
+
lines.push(`- Strengths: ${iteration.result.strengths?.length || 0}`);
|
|
211
|
+
lines.push(`- Concerns: ${iteration.result.concerns?.length || 0}`);
|
|
212
|
+
lines.push('');
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (!result.approved) {
|
|
216
|
+
const lastResult = result.iterations[result.iterations.length - 1]?.result;
|
|
217
|
+
if (lastResult?.concerns && lastResult.concerns.length > 0) {
|
|
218
|
+
lines.push(`### Remaining Concerns`);
|
|
219
|
+
lines.push('');
|
|
220
|
+
for (const concern of lastResult.concerns) {
|
|
221
|
+
lines.push(`- ${concern}`);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return lines.join('\n');
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Validate a plan structure has required sections
|
|
231
|
+
*
|
|
232
|
+
* @param plan - The plan to validate
|
|
233
|
+
* @returns Validation result with missing sections
|
|
234
|
+
*/
|
|
235
|
+
export function validatePlanStructure(plan: string): {
|
|
236
|
+
valid: boolean;
|
|
237
|
+
missingSections: string[];
|
|
238
|
+
} {
|
|
239
|
+
const requiredSections = [
|
|
240
|
+
'Background',
|
|
241
|
+
'Goals',
|
|
242
|
+
'Milestones',
|
|
243
|
+
'Tasks',
|
|
244
|
+
'Test',
|
|
245
|
+
];
|
|
246
|
+
|
|
247
|
+
const missingSections: string[] = [];
|
|
248
|
+
|
|
249
|
+
for (const section of requiredSections) {
|
|
250
|
+
// Check for section header (case-insensitive)
|
|
251
|
+
const pattern = new RegExp(`(^|\\n)#+\\s*${section}`, 'i');
|
|
252
|
+
if (!pattern.test(plan)) {
|
|
253
|
+
missingSections.push(section);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return {
|
|
258
|
+
valid: missingSections.length === 0,
|
|
259
|
+
missingSections,
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Calculate average score across iterations
|
|
265
|
+
*
|
|
266
|
+
* @param iterations - The consensus iterations
|
|
267
|
+
* @returns Average score
|
|
268
|
+
*/
|
|
269
|
+
export function calculateAverageScore(iterations: ConsensusIteration[]): number {
|
|
270
|
+
if (iterations.length === 0) return 0;
|
|
271
|
+
|
|
272
|
+
const sum = iterations.reduce((acc, it) => acc + it.result.score, 0);
|
|
273
|
+
return Math.round(sum / iterations.length);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Get the score trend across iterations
|
|
278
|
+
*
|
|
279
|
+
* @param iterations - The consensus iterations
|
|
280
|
+
* @returns 'improving', 'declining', or 'stable'
|
|
281
|
+
*/
|
|
282
|
+
export function getScoreTrend(
|
|
283
|
+
iterations: ConsensusIteration[]
|
|
284
|
+
): 'improving' | 'declining' | 'stable' {
|
|
285
|
+
if (iterations.length < 2) return 'stable';
|
|
286
|
+
|
|
287
|
+
const scores = iterations.map((it) => it.result.score);
|
|
288
|
+
const firstHalf = scores.slice(0, Math.floor(scores.length / 2));
|
|
289
|
+
const secondHalf = scores.slice(Math.floor(scores.length / 2));
|
|
290
|
+
|
|
291
|
+
const firstAvg = firstHalf.reduce((a, b) => a + b, 0) / firstHalf.length;
|
|
292
|
+
const secondAvg = secondHalf.reduce((a, b) => a + b, 0) / secondHalf.length;
|
|
293
|
+
|
|
294
|
+
const diff = secondAvg - firstAvg;
|
|
295
|
+
|
|
296
|
+
if (diff > 5) return 'improving';
|
|
297
|
+
if (diff < -5) return 'declining';
|
|
298
|
+
return 'stable';
|
|
299
|
+
}
|