promptup-plugin 0.1.1

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 (42) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +78 -0
  3. package/bin/install.cjs +306 -0
  4. package/bin/promptup-plugin +8 -0
  5. package/dist/config.d.ts +40 -0
  6. package/dist/config.js +123 -0
  7. package/dist/db.d.ts +35 -0
  8. package/dist/db.js +327 -0
  9. package/dist/decision-detector.d.ts +11 -0
  10. package/dist/decision-detector.js +47 -0
  11. package/dist/evaluator.d.ts +10 -0
  12. package/dist/evaluator.js +844 -0
  13. package/dist/git-activity-extractor.d.ts +35 -0
  14. package/dist/git-activity-extractor.js +167 -0
  15. package/dist/index.d.ts +12 -0
  16. package/dist/index.js +54 -0
  17. package/dist/pr-report-generator.d.ts +20 -0
  18. package/dist/pr-report-generator.js +421 -0
  19. package/dist/shared/decision-classifier.d.ts +60 -0
  20. package/dist/shared/decision-classifier.js +385 -0
  21. package/dist/shared/decision-score.d.ts +7 -0
  22. package/dist/shared/decision-score.js +31 -0
  23. package/dist/shared/dimensions.d.ts +43 -0
  24. package/dist/shared/dimensions.js +361 -0
  25. package/dist/shared/scoring.d.ts +89 -0
  26. package/dist/shared/scoring.js +161 -0
  27. package/dist/shared/types.d.ts +108 -0
  28. package/dist/shared/types.js +9 -0
  29. package/dist/tools.d.ts +30 -0
  30. package/dist/tools.js +456 -0
  31. package/dist/transcript-parser.d.ts +36 -0
  32. package/dist/transcript-parser.js +201 -0
  33. package/hooks/auto-eval.sh +44 -0
  34. package/hooks/check-update.sh +26 -0
  35. package/hooks/debug-hook.sh +3 -0
  36. package/hooks/hooks.json +36 -0
  37. package/hooks/render-eval.sh +137 -0
  38. package/package.json +60 -0
  39. package/skills/eval/SKILL.md +12 -0
  40. package/skills/pr-report/SKILL.md +37 -0
  41. package/skills/status/SKILL.md +28 -0
  42. package/statusline.sh +46 -0
@@ -0,0 +1,361 @@
1
+ /**
2
+ * Base 6-dimension rubric definitions for PromptUp evaluation.
3
+ * Sourced from the PromptUp Evaluator rubric v0.1.
4
+ *
5
+ * STANDALONE copy — no imports from @promptup/shared.
6
+ */
7
+ export const BASE_DIMENSION_KEYS = [
8
+ 'task_decomposition',
9
+ 'prompt_specificity',
10
+ 'output_validation',
11
+ 'iteration_quality',
12
+ 'strategic_tool_usage',
13
+ 'context_management',
14
+ ];
15
+ export const BASE_DIMENSIONS = {
16
+ task_decomposition: {
17
+ key: 'task_decomposition',
18
+ label: 'Task Decomposition',
19
+ description: 'How well the developer breaks down complex problems before prompting.',
20
+ scoring_guidance: 'Score 0-100 based on how effectively the developer decomposes complex tasks into manageable sub-tasks with clear sequencing and dependency management.',
21
+ signals: [
22
+ 'One thing at a time vs everything at once?',
23
+ 'References prior step outputs?',
24
+ 'Visible plan or improvising?',
25
+ ],
26
+ ranges: [
27
+ { min: 0, max: 20, description: 'Dumps entire complex task in one prompt, no structure' },
28
+ { min: 21, max: 40, description: 'Some structure but mixes multiple concerns per prompt' },
29
+ { min: 41, max: 60, description: 'Logical steps but suboptimal sequencing' },
30
+ { min: 61, max: 80, description: 'Clear decomposition with logical sequencing and dependencies' },
31
+ { min: 81, max: 100, description: 'Expert — optimal subtask order, explicit dependency management' },
32
+ ],
33
+ },
34
+ prompt_specificity: {
35
+ key: 'prompt_specificity',
36
+ label: 'Prompt Specificity',
37
+ description: 'How precise and well-structured the prompts are.',
38
+ scoring_guidance: 'Score 0-100 based on information density, constraints, examples, output format requirements, and edge case coverage.',
39
+ signals: [
40
+ 'Information density (long != good, dense = good)',
41
+ 'Output format requirements?',
42
+ 'Few-shot examples?',
43
+ 'Constraints (what NOT to do)?',
44
+ ],
45
+ ranges: [
46
+ { min: 0, max: 20, description: 'Vague/ambiguous ("make it better", "fix this")' },
47
+ { min: 21, max: 40, description: 'Some specificity, missing key constraints or context' },
48
+ { min: 41, max: 60, description: 'Adequate, gets reasonable results but room for misinterpretation' },
49
+ { min: 61, max: 80, description: 'Well-structured with clear constraints, examples, or format requirements' },
50
+ {
51
+ min: 81,
52
+ max: 100,
53
+ description: 'Expert — role setting, constraints, examples, output format, edge cases, success criteria',
54
+ },
55
+ ],
56
+ },
57
+ output_validation: {
58
+ key: 'output_validation',
59
+ label: 'Output Validation',
60
+ description: 'Does the developer critically evaluate AI responses or accept blindly?',
61
+ scoring_guidance: 'Score 0-100 based on how thoroughly the developer validates, questions, and tests AI-generated outputs.',
62
+ signals: [
63
+ 'Says "that\'s wrong" or "check that"?',
64
+ 'Tests code before accepting?',
65
+ 'Catches hallucinations?',
66
+ 'Asks for sources?',
67
+ 'Notices logical inconsistencies?',
68
+ ],
69
+ ranges: [
70
+ { min: 0, max: 20, description: 'Accepts every response without question' },
71
+ { min: 21, max: 40, description: 'Occasionally pushes back on obvious errors only' },
72
+ { min: 41, max: 60, description: 'Checks key claims/results, catches some errors' },
73
+ {
74
+ min: 61,
75
+ max: 80,
76
+ description: 'Systematic — tests outputs, cross-references, identifies hallucinations',
77
+ },
78
+ {
79
+ min: 81,
80
+ max: 100,
81
+ description: 'Expert — challenges assumptions, tests edge cases, verifies against external sources',
82
+ },
83
+ ],
84
+ },
85
+ iteration_quality: {
86
+ key: 'iteration_quality',
87
+ label: 'Iteration Quality',
88
+ description: 'When iterating, does each iteration meaningfully improve on the previous?',
89
+ scoring_guidance: 'Score 0-100 based on whether each follow-up prompt builds purposefully on previous responses and converges efficiently.',
90
+ signals: [
91
+ 'Clear purpose per follow-up?',
92
+ 'Converging or going in circles?',
93
+ 'References specific parts of previous responses?',
94
+ 'Pivots when approach fails?',
95
+ ],
96
+ ranges: [
97
+ { min: 0, max: 20, description: 'Repeats similar prompts hoping for different results' },
98
+ { min: 21, max: 40, description: 'Changes are random/unfocused' },
99
+ { min: 41, max: 60, description: 'Shows direction but includes unnecessary repetition' },
100
+ { min: 61, max: 80, description: 'Focused iterations addressing specific weaknesses' },
101
+ {
102
+ min: 81,
103
+ max: 100,
104
+ description: 'Expert — each prompt builds precisely on previous, converges efficiently',
105
+ },
106
+ ],
107
+ },
108
+ strategic_tool_usage: {
109
+ key: 'strategic_tool_usage',
110
+ label: 'Strategic Tool Usage',
111
+ description: 'Intelligent choices about which AI capabilities to use.',
112
+ scoring_guidance: 'Score 0-100 based on deliberate selection of models, tools, and capabilities matched to task requirements.',
113
+ signals: [
114
+ 'Switches models/tools when appropriate?',
115
+ 'Uses code execution for verification?',
116
+ 'Leverages search for factual accuracy?',
117
+ 'Understands capability differences?',
118
+ ],
119
+ ranges: [
120
+ { min: 0, max: 20, description: 'One model/approach for everything' },
121
+ { min: 21, max: 40, description: 'Aware of different capabilities but doesn\'t leverage them' },
122
+ { min: 41, max: 60, description: 'Some tool/model choices but not always optimal' },
123
+ { min: 61, max: 80, description: 'Deliberately selects models/tools based on task requirements' },
124
+ {
125
+ min: 81,
126
+ max: 100,
127
+ description: 'Expert — switches models mid-workflow, uses appropriate tools at right moments',
128
+ },
129
+ ],
130
+ },
131
+ context_management: {
132
+ key: 'context_management',
133
+ label: 'Context Management',
134
+ description: 'How well the developer manages conversation context and information flow.',
135
+ scoring_guidance: 'Score 0-100 based on deliberate context structuring, summarization, and information flow management.',
136
+ signals: [
137
+ 'Provides context at start?',
138
+ 'References previous parts?',
139
+ 'Summarises/checkpoints progress?',
140
+ 'Re-provides context when conversation gets long?',
141
+ ],
142
+ ranges: [
143
+ {
144
+ min: 0,
145
+ max: 20,
146
+ description: 'No context management, each prompt independent, repeats information',
147
+ },
148
+ { min: 21, max: 40, description: 'Some reference to previous context but inconsistent' },
149
+ {
150
+ min: 41,
151
+ max: 60,
152
+ description: "Maintains thread but doesn't explicitly manage context window",
153
+ },
154
+ {
155
+ min: 61,
156
+ max: 80,
157
+ description: 'Deliberately structures info flow — summarises, references, builds on prior exchanges',
158
+ },
159
+ {
160
+ min: 81,
161
+ max: 100,
162
+ description: 'Expert — relevant background upfront, summarises intermediate results, manages long conversations',
163
+ },
164
+ ],
165
+ },
166
+ };
167
+ // ─── Domain Dimensions (5 depth-of-understanding dimensions) ──────────
168
+ export const DOMAIN_DIMENSION_KEYS = [
169
+ 'architectural_awareness',
170
+ 'error_anticipation',
171
+ 'technical_vocabulary',
172
+ 'dependency_reasoning',
173
+ 'tradeoff_articulation',
174
+ ];
175
+ export const DOMAIN_DIMENSIONS = {
176
+ architectural_awareness: {
177
+ key: 'architectural_awareness',
178
+ label: 'Architectural Awareness',
179
+ description: 'Understanding of system architecture, patterns, and design trade-offs.',
180
+ scoring_guidance: 'Score 0-100 based on how well the developer demonstrates awareness of system-level architecture, design patterns, and structural implications of their changes.',
181
+ signals: [
182
+ 'References architecture when making decisions?',
183
+ 'Considers cross-cutting concerns?',
184
+ 'Understands component boundaries?',
185
+ 'Anticipates scaling implications?',
186
+ ],
187
+ ranges: [
188
+ { min: 0, max: 20, description: 'No awareness of system structure, treats code in isolation' },
189
+ { min: 21, max: 40, description: 'Aware of immediate file/module scope only' },
190
+ { min: 41, max: 60, description: 'Understands local architecture but misses wider implications' },
191
+ { min: 61, max: 80, description: 'Considers system-level design patterns and trade-offs' },
192
+ { min: 81, max: 100, description: 'Expert — reasons about architecture holistically, anticipates cascading effects' },
193
+ ],
194
+ },
195
+ error_anticipation: {
196
+ key: 'error_anticipation',
197
+ label: 'Error Anticipation',
198
+ description: 'Proactively considering failure modes, edge cases, and error handling.',
199
+ scoring_guidance: 'Score 0-100 based on how proactively the developer considers failure modes, edge cases, and error recovery strategies.',
200
+ signals: [
201
+ 'Asks about edge cases proactively?',
202
+ 'Considers failure scenarios?',
203
+ 'Plans error recovery?',
204
+ 'Tests unhappy paths?',
205
+ ],
206
+ ranges: [
207
+ { min: 0, max: 20, description: 'No consideration of failure modes or edge cases' },
208
+ { min: 21, max: 40, description: 'Handles obvious errors only when prompted' },
209
+ { min: 41, max: 60, description: 'Some proactive error thinking but gaps in coverage' },
210
+ { min: 61, max: 80, description: 'Systematically considers failure modes and edge cases' },
211
+ { min: 81, max: 100, description: 'Expert — anticipates subtle failures, designs for resilience' },
212
+ ],
213
+ },
214
+ technical_vocabulary: {
215
+ key: 'technical_vocabulary',
216
+ label: 'Technical Vocabulary',
217
+ description: 'Precision of technical language and domain-specific terminology.',
218
+ scoring_guidance: 'Score 0-100 based on the precision and appropriateness of technical language, correct use of domain terminology, and communication clarity.',
219
+ signals: [
220
+ 'Uses correct technical terms?',
221
+ 'Distinguishes similar concepts precisely?',
222
+ 'Communicates intent clearly to AI?',
223
+ 'Names things well in code?',
224
+ ],
225
+ ranges: [
226
+ { min: 0, max: 20, description: 'Vague or incorrect terminology, imprecise communication' },
227
+ { min: 21, max: 40, description: 'Basic terms used correctly but lacks precision' },
228
+ { min: 41, max: 60, description: 'Generally correct terminology with occasional imprecision' },
229
+ { min: 61, max: 80, description: 'Precise technical language, clear domain-specific communication' },
230
+ { min: 81, max: 100, description: 'Expert — nuanced vocabulary, distinguishes subtle concept differences' },
231
+ ],
232
+ },
233
+ dependency_reasoning: {
234
+ key: 'dependency_reasoning',
235
+ label: 'Dependency Reasoning',
236
+ description: 'Understanding how components interact, import chains, and side effects.',
237
+ scoring_guidance: 'Score 0-100 based on awareness of dependency graphs, understanding of side effects, and ability to trace interaction chains.',
238
+ signals: [
239
+ 'Understands import/dependency chains?',
240
+ 'Considers side effects of changes?',
241
+ 'Traces data flow across boundaries?',
242
+ 'Identifies coupling risks?',
243
+ ],
244
+ ranges: [
245
+ { min: 0, max: 20, description: 'No awareness of dependencies or side effects' },
246
+ { min: 21, max: 40, description: 'Understands direct dependencies only' },
247
+ { min: 41, max: 60, description: 'Traces some dependency chains but misses indirect effects' },
248
+ { min: 61, max: 80, description: 'Good understanding of interaction patterns and side effects' },
249
+ { min: 81, max: 100, description: 'Expert — traces full dependency graphs, predicts cascading side effects' },
250
+ ],
251
+ },
252
+ tradeoff_articulation: {
253
+ key: 'tradeoff_articulation',
254
+ label: 'Tradeoff Articulation',
255
+ description: 'Ability to weigh and explain trade-offs between different approaches.',
256
+ scoring_guidance: 'Score 0-100 based on ability to identify, compare, and clearly articulate trade-offs when choosing between approaches.',
257
+ signals: [
258
+ 'Compares alternatives explicitly?',
259
+ 'Weighs pros/cons of approaches?',
260
+ 'Explains reasoning for choices?',
261
+ 'Considers non-functional requirements?',
262
+ ],
263
+ ranges: [
264
+ { min: 0, max: 20, description: 'Accepts first solution without considering alternatives' },
265
+ { min: 21, max: 40, description: 'Occasionally mentions alternatives but no structured comparison' },
266
+ { min: 41, max: 60, description: 'Identifies trade-offs but analysis lacks depth' },
267
+ { min: 61, max: 80, description: 'Structured comparison of approaches with clear reasoning' },
268
+ { min: 81, max: 100, description: 'Expert — multi-dimensional trade-off analysis including performance, maintainability, and business impact' },
269
+ ],
270
+ },
271
+ };
272
+ /** All 11 dimension keys (6 base + 5 domain) */
273
+ export const ALL_DIMENSION_KEYS = [...BASE_DIMENSION_KEYS, ...DOMAIN_DIMENSION_KEYS];
274
+ /** Default base dimension configuration for a new rubric */
275
+ export const DEFAULT_BASE_DIMENSIONS = {
276
+ task_decomposition: { weight: 1.0, enabled: true },
277
+ prompt_specificity: { weight: 1.0, enabled: true },
278
+ output_validation: { weight: 1.0, enabled: true },
279
+ iteration_quality: { weight: 1.0, enabled: true },
280
+ strategic_tool_usage: { weight: 1.0, enabled: true },
281
+ context_management: { weight: 1.0, enabled: true },
282
+ };
283
+ // ─── Preset Weight Profiles ──────────────────────────────────────────────
284
+ export const WEIGHT_PROFILE_KEYS = [
285
+ 'balanced',
286
+ 'greenfield',
287
+ 'bugfix',
288
+ 'refactor',
289
+ 'security_review',
290
+ ];
291
+ export const WEIGHT_PROFILES = {
292
+ balanced: {
293
+ key: 'balanced',
294
+ label: 'Balanced',
295
+ description: 'Equal weight across all dimensions. Good default for general development tasks.',
296
+ weights: {
297
+ task_decomposition: 0.167,
298
+ prompt_specificity: 0.167,
299
+ output_validation: 0.167,
300
+ iteration_quality: 0.167,
301
+ strategic_tool_usage: 0.167,
302
+ context_management: 0.165,
303
+ },
304
+ },
305
+ greenfield: {
306
+ key: 'greenfield',
307
+ label: 'Greenfield',
308
+ description: 'Emphasizes task decomposition and prompt clarity for new feature development.',
309
+ weights: {
310
+ task_decomposition: 0.25,
311
+ prompt_specificity: 0.20,
312
+ output_validation: 0.15,
313
+ iteration_quality: 0.15,
314
+ strategic_tool_usage: 0.15,
315
+ context_management: 0.10,
316
+ },
317
+ },
318
+ bugfix: {
319
+ key: 'bugfix',
320
+ label: 'Bugfix',
321
+ description: 'Emphasizes output validation and iteration for debugging and fixing issues.',
322
+ weights: {
323
+ task_decomposition: 0.10,
324
+ prompt_specificity: 0.15,
325
+ output_validation: 0.30,
326
+ iteration_quality: 0.20,
327
+ strategic_tool_usage: 0.15,
328
+ context_management: 0.10,
329
+ },
330
+ },
331
+ refactor: {
332
+ key: 'refactor',
333
+ label: 'Refactor',
334
+ description: 'Emphasizes decomposition, validation, and context management for code restructuring.',
335
+ weights: {
336
+ task_decomposition: 0.20,
337
+ prompt_specificity: 0.15,
338
+ output_validation: 0.20,
339
+ iteration_quality: 0.15,
340
+ strategic_tool_usage: 0.10,
341
+ context_management: 0.20,
342
+ },
343
+ },
344
+ security_review: {
345
+ key: 'security_review',
346
+ label: 'Security Review',
347
+ description: 'Heavily weights output validation for security-focused code review tasks.',
348
+ weights: {
349
+ task_decomposition: 0.10,
350
+ prompt_specificity: 0.15,
351
+ output_validation: 0.35,
352
+ iteration_quality: 0.15,
353
+ strategic_tool_usage: 0.10,
354
+ context_management: 0.15,
355
+ },
356
+ },
357
+ };
358
+ /** Look up a weight profile by key. Returns undefined if not found. */
359
+ export function getWeightProfile(key) {
360
+ return WEIGHT_PROFILES[key];
361
+ }
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Multi-layer composite scoring for PromptUp evaluations.
3
+ *
4
+ * Score hierarchy:
5
+ * base composite — weighted avg of 6 base dimensions
6
+ * domain composite — weighted avg of 5 domain dimensions
7
+ * tech composite — avg across roadmap-level tech expertise scores
8
+ * overall composite — blend of base + domain (60/40)
9
+ * grand composite — blend of overall + tech (70/30)
10
+ *
11
+ * STANDALONE copy — no imports from @promptup/shared.
12
+ */
13
+ export interface CompositeScores {
14
+ composite_score: number;
15
+ domain_composite_score: number | null;
16
+ tech_composite_score: number | null;
17
+ overall_composite_score: number | null;
18
+ grand_composite_score: number | null;
19
+ }
20
+ export interface CompositeDimensionInput {
21
+ score: number;
22
+ weight?: number;
23
+ }
24
+ export interface TechExpertiseScoreInput {
25
+ score: number;
26
+ }
27
+ export interface RiskFlag {
28
+ type: string;
29
+ dimension: string;
30
+ score: number;
31
+ severity: 'warning' | 'critical';
32
+ message: string;
33
+ }
34
+ export interface RiskFlagDimensionScore {
35
+ dimension: string;
36
+ score: number;
37
+ }
38
+ /** Weight of base composite in the overall composite blend */
39
+ export declare const OVERALL_BASE_WEIGHT = 0.6;
40
+ /** Weight of domain composite in the overall composite blend */
41
+ export declare const OVERALL_DOMAIN_WEIGHT = 0.4;
42
+ /** Weight of overall composite in the grand composite blend */
43
+ export declare const GRAND_OVERALL_WEIGHT = 0.7;
44
+ /** Weight of tech composite in the grand composite blend */
45
+ export declare const GRAND_TECH_WEIGHT = 0.3;
46
+ export declare function clamp(min: number, max: number, value: number): number;
47
+ /**
48
+ * Compute weighted composite score from dimension scores + weights.
49
+ * Dimensions with weight 0 are excluded from calculation.
50
+ */
51
+ export declare function computeCompositeScore(dimensions: {
52
+ score: number;
53
+ weight: number;
54
+ }[]): number;
55
+ /**
56
+ * Compute domain composite from 5 domain dimension scores.
57
+ * Returns null if no scores provided.
58
+ */
59
+ export declare function computeDomainComposite(domainScores: Record<string, CompositeDimensionInput>): number | null;
60
+ /**
61
+ * Compute tech composite from roadmap-level expertise scores.
62
+ * Returns null if no tech expertise entries.
63
+ */
64
+ export declare function computeTechComposite(techExpertise: TechExpertiseScoreInput[]): number | null;
65
+ /**
66
+ * Compute overall composite = blend of base + domain composites.
67
+ * Returns null if domain composite is null.
68
+ */
69
+ export declare function computeOverallComposite(baseComposite: number, domainComposite: number | null): number | null;
70
+ /**
71
+ * Compute grand composite = blend of overall + tech composites.
72
+ * Returns null if either overall or tech composite is null.
73
+ */
74
+ export declare function computeGrandComposite(overallComposite: number | null, techComposite: number | null): number | null;
75
+ /**
76
+ * Compute risk flags from dimension scores and composite score.
77
+ *
78
+ * Flags:
79
+ * - "Hidden Weakness": any dimension < 30 while composite > 60 (critical)
80
+ * - "Extreme Imbalance": any dimension > 85 while another < 35 (warning)
81
+ */
82
+ export declare function computeRiskFlags(dimensionScores: RiskFlagDimensionScore[], compositeScore: number): RiskFlag[];
83
+ /**
84
+ * Compute risk flags including historical comparison.
85
+ *
86
+ * In addition to the base flags from computeRiskFlags, adds:
87
+ * - "Volatile": delta > 40 between consecutive checkpoints on any dimension (warning)
88
+ */
89
+ export declare function computeRiskFlagsWithHistory(current: RiskFlagDimensionScore[], previous: RiskFlagDimensionScore[] | null, compositeScore?: number): RiskFlag[];
@@ -0,0 +1,161 @@
1
+ /**
2
+ * Multi-layer composite scoring for PromptUp evaluations.
3
+ *
4
+ * Score hierarchy:
5
+ * base composite — weighted avg of 6 base dimensions
6
+ * domain composite — weighted avg of 5 domain dimensions
7
+ * tech composite — avg across roadmap-level tech expertise scores
8
+ * overall composite — blend of base + domain (60/40)
9
+ * grand composite — blend of overall + tech (70/30)
10
+ *
11
+ * STANDALONE copy — no imports from @promptup/shared.
12
+ */
13
+ // ─── Constants ──────────────────────────────────────────────────────────
14
+ /** Weight of base composite in the overall composite blend */
15
+ export const OVERALL_BASE_WEIGHT = 0.6;
16
+ /** Weight of domain composite in the overall composite blend */
17
+ export const OVERALL_DOMAIN_WEIGHT = 0.4;
18
+ /** Weight of overall composite in the grand composite blend */
19
+ export const GRAND_OVERALL_WEIGHT = 0.7;
20
+ /** Weight of tech composite in the grand composite blend */
21
+ export const GRAND_TECH_WEIGHT = 0.3;
22
+ // ─── Helpers ─────────────────────────────────────────────────────────────
23
+ export function clamp(min, max, value) {
24
+ return Math.max(min, Math.min(max, value));
25
+ }
26
+ // ─── Functions ──────────────────────────────────────────────────────────
27
+ /**
28
+ * Compute weighted composite score from dimension scores + weights.
29
+ * Dimensions with weight 0 are excluded from calculation.
30
+ */
31
+ export function computeCompositeScore(dimensions) {
32
+ const totalWeight = dimensions.reduce((sum, d) => (d.weight > 0 ? sum + d.weight : sum), 0);
33
+ if (totalWeight === 0)
34
+ return 0;
35
+ const weightedSum = dimensions.reduce((sum, d) => (d.weight > 0 ? sum + d.score * d.weight : sum), 0);
36
+ return Math.round((weightedSum / totalWeight) * 100) / 100;
37
+ }
38
+ /**
39
+ * Compute domain composite from 5 domain dimension scores.
40
+ * Returns null if no scores provided.
41
+ */
42
+ export function computeDomainComposite(domainScores) {
43
+ const entries = Object.values(domainScores);
44
+ if (entries.length === 0)
45
+ return null;
46
+ const totalWeight = entries.reduce((sum, d) => sum + (d.weight ?? 1), 0);
47
+ if (totalWeight === 0)
48
+ return null;
49
+ const weighted = entries.reduce((sum, d) => sum + d.score * (d.weight ?? 1), 0);
50
+ return Math.round((weighted / totalWeight) * 10) / 10;
51
+ }
52
+ /**
53
+ * Compute tech composite from roadmap-level expertise scores.
54
+ * Returns null if no tech expertise entries.
55
+ */
56
+ export function computeTechComposite(techExpertise) {
57
+ if (techExpertise.length === 0)
58
+ return null;
59
+ const sum = techExpertise.reduce((s, te) => s + te.score, 0);
60
+ return Math.round((sum / techExpertise.length) * 10) / 10;
61
+ }
62
+ /**
63
+ * Compute overall composite = blend of base + domain composites.
64
+ * Returns null if domain composite is null.
65
+ */
66
+ export function computeOverallComposite(baseComposite, domainComposite) {
67
+ if (domainComposite === null)
68
+ return null;
69
+ const score = baseComposite * OVERALL_BASE_WEIGHT +
70
+ domainComposite * OVERALL_DOMAIN_WEIGHT;
71
+ return Math.round(score * 10) / 10;
72
+ }
73
+ /**
74
+ * Compute grand composite = blend of overall + tech composites.
75
+ * Returns null if either overall or tech composite is null.
76
+ */
77
+ export function computeGrandComposite(overallComposite, techComposite) {
78
+ if (overallComposite === null || techComposite === null)
79
+ return null;
80
+ const score = overallComposite * GRAND_OVERALL_WEIGHT +
81
+ techComposite * GRAND_TECH_WEIGHT;
82
+ return Math.round(score * 10) / 10;
83
+ }
84
+ // ─── Risk Flags ──────────────────────────────────────────────────────────
85
+ /**
86
+ * Compute risk flags from dimension scores and composite score.
87
+ *
88
+ * Flags:
89
+ * - "Hidden Weakness": any dimension < 30 while composite > 60 (critical)
90
+ * - "Extreme Imbalance": any dimension > 85 while another < 35 (warning)
91
+ */
92
+ export function computeRiskFlags(dimensionScores, compositeScore) {
93
+ const flags = [];
94
+ // Hidden Weakness: any dim < 30 AND composite > 60
95
+ if (compositeScore > 60) {
96
+ for (const ds of dimensionScores) {
97
+ if (ds.score < 30) {
98
+ flags.push({
99
+ type: 'Hidden Weakness',
100
+ dimension: ds.dimension,
101
+ score: ds.score,
102
+ severity: 'critical',
103
+ message: `${ds.dimension} scored ${ds.score} despite composite score of ${compositeScore}. This weakness may be masked by strong performance in other areas.`,
104
+ });
105
+ }
106
+ }
107
+ }
108
+ // Extreme Imbalance: any dim > 85 AND any other dim < 35
109
+ const highDims = dimensionScores.filter((d) => d.score > 85);
110
+ const lowDims = dimensionScores.filter((d) => d.score < 35);
111
+ if (highDims.length > 0 && lowDims.length > 0) {
112
+ for (const low of lowDims) {
113
+ for (const high of highDims) {
114
+ if (low.dimension !== high.dimension) {
115
+ flags.push({
116
+ type: 'Extreme Imbalance',
117
+ dimension: low.dimension,
118
+ score: low.score,
119
+ severity: 'warning',
120
+ message: `${low.dimension} (${low.score}) is critically low while ${high.dimension} (${high.score}) is very high. Consider balancing effort across dimensions.`,
121
+ });
122
+ break; // One flag per low dimension is sufficient
123
+ }
124
+ }
125
+ }
126
+ }
127
+ return flags;
128
+ }
129
+ /**
130
+ * Compute risk flags including historical comparison.
131
+ *
132
+ * In addition to the base flags from computeRiskFlags, adds:
133
+ * - "Volatile": delta > 40 between consecutive checkpoints on any dimension (warning)
134
+ */
135
+ export function computeRiskFlagsWithHistory(current, previous, compositeScore = 0) {
136
+ // Compute composite from current if not provided
137
+ const composite = compositeScore > 0
138
+ ? compositeScore
139
+ : current.reduce((sum, d) => sum + d.score, 0) / (current.length || 1);
140
+ const flags = computeRiskFlags(current, composite);
141
+ // Volatile: delta > 40 between consecutive checkpoints
142
+ if (previous) {
143
+ const prevMap = new Map(previous.map((d) => [d.dimension, d.score]));
144
+ for (const curr of current) {
145
+ const prevScore = prevMap.get(curr.dimension);
146
+ if (prevScore !== undefined) {
147
+ const delta = Math.abs(curr.score - prevScore);
148
+ if (delta > 40) {
149
+ flags.push({
150
+ type: 'Volatile',
151
+ dimension: curr.dimension,
152
+ score: curr.score,
153
+ severity: 'warning',
154
+ message: `${curr.dimension} changed by ${delta} points (${prevScore} -> ${curr.score}) between consecutive checkpoints.`,
155
+ });
156
+ }
157
+ }
158
+ }
159
+ }
160
+ return flags;
161
+ }