@grainulation/harvest 1.0.0 → 1.0.2
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/CODE_OF_CONDUCT.md +25 -0
- package/CONTRIBUTING.md +93 -0
- package/README.md +44 -45
- package/bin/harvest.js +135 -60
- package/lib/analyzer.js +33 -26
- package/lib/calibration.js +199 -32
- package/lib/dashboard.js +54 -32
- package/lib/decay.js +224 -18
- package/lib/farmer.js +54 -38
- package/lib/harvest-card.js +475 -0
- package/lib/patterns.js +64 -43
- package/lib/report.js +243 -61
- package/lib/server.js +323 -150
- package/lib/templates.js +47 -32
- package/lib/token-tracker.js +288 -0
- package/lib/tokens.js +317 -0
- package/lib/velocity.js +68 -40
- package/lib/wrapped.js +489 -0
- package/package.json +10 -3
package/lib/wrapped.js
ADDED
|
@@ -0,0 +1,489 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Harvest Wrapped — Spotify-Wrapped-style report data for research sprints.
|
|
5
|
+
*
|
|
6
|
+
* Generates a "Harvest Report" with:
|
|
7
|
+
* 1. Sprint count and velocity trends
|
|
8
|
+
* 2. Claim type distribution and ratios
|
|
9
|
+
* 3. Evidence tier progression (are you testing more over time?)
|
|
10
|
+
* 4. "Research personality" type based on command usage patterns
|
|
11
|
+
* 5. Key milestones and achievements
|
|
12
|
+
*
|
|
13
|
+
* Based on claims from harvest-in-grainulator and token-economics sprints:
|
|
14
|
+
* - r628: Wrapped data points (sprint count, type ratios, personality)
|
|
15
|
+
* - r627: Spotify Wrapped viral design (personalization, identity, shareability)
|
|
16
|
+
* - r602: Behavioral science (make patterns feel like identity, not metrics)
|
|
17
|
+
* - d005: Harvest Reports = Spotify Wrapped for research
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Research personality archetypes based on command/claim usage patterns.
|
|
22
|
+
* Each archetype has detection heuristics and a descriptive label.
|
|
23
|
+
*/
|
|
24
|
+
const ARCHETYPES = {
|
|
25
|
+
challenger: {
|
|
26
|
+
id: "challenger",
|
|
27
|
+
label: "The Challenger",
|
|
28
|
+
emoji: "sword",
|
|
29
|
+
description:
|
|
30
|
+
"You stress-test everything. High challenge and contest rates mean your surviving claims are battle-hardened.",
|
|
31
|
+
detect: (stats) => stats.challengeRatio > 0.15,
|
|
32
|
+
},
|
|
33
|
+
prototyper: {
|
|
34
|
+
id: "prototyper",
|
|
35
|
+
label: "The Prototyper",
|
|
36
|
+
emoji: "lab",
|
|
37
|
+
description:
|
|
38
|
+
"You build to learn. Heavy prototype and tested-evidence usage means you validate through hands-on experimentation.",
|
|
39
|
+
detect: (stats) => stats.testedRatio > 0.25,
|
|
40
|
+
},
|
|
41
|
+
scholar: {
|
|
42
|
+
id: "scholar",
|
|
43
|
+
label: "The Scholar",
|
|
44
|
+
emoji: "book",
|
|
45
|
+
description:
|
|
46
|
+
"You research deeply. High factual claim counts and documented evidence show rigorous investigation.",
|
|
47
|
+
detect: (stats) => stats.factualRatio > 0.45 && stats.documentedRatio > 0.2,
|
|
48
|
+
},
|
|
49
|
+
strategist: {
|
|
50
|
+
id: "strategist",
|
|
51
|
+
label: "The Strategist",
|
|
52
|
+
emoji: "chess",
|
|
53
|
+
description:
|
|
54
|
+
"You drive toward decisions. High recommendation-to-factual ratios show you synthesize findings into action.",
|
|
55
|
+
detect: (stats) => stats.recommendationRatio > 0.2,
|
|
56
|
+
},
|
|
57
|
+
sentinel: {
|
|
58
|
+
id: "sentinel",
|
|
59
|
+
label: "The Sentinel",
|
|
60
|
+
emoji: "shield",
|
|
61
|
+
description:
|
|
62
|
+
"You spot what could go wrong. High risk identification means you protect decisions from blind spots.",
|
|
63
|
+
detect: (stats) => stats.riskRatio > 0.15,
|
|
64
|
+
},
|
|
65
|
+
explorer: {
|
|
66
|
+
id: "explorer",
|
|
67
|
+
label: "The Explorer",
|
|
68
|
+
emoji: "compass",
|
|
69
|
+
description:
|
|
70
|
+
"You cover wide ground. Diverse topics and broad tag coverage show you leave no stone unturned.",
|
|
71
|
+
detect: (stats) => stats.topicDiversity > 0.7,
|
|
72
|
+
},
|
|
73
|
+
balanced: {
|
|
74
|
+
id: "balanced",
|
|
75
|
+
label: "The Balanced Researcher",
|
|
76
|
+
emoji: "scales",
|
|
77
|
+
description:
|
|
78
|
+
"You maintain equilibrium across research activities. A well-rounded approach with no single dominant pattern.",
|
|
79
|
+
detect: () => true, // fallback
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Generate Harvest Wrapped report data.
|
|
85
|
+
*
|
|
86
|
+
* @param {Array} sprints - Sprint objects (from loadSprintData)
|
|
87
|
+
* @param {object} [opts] - Options
|
|
88
|
+
* @param {object} [opts.tokenReport] - Token tracking report (from trackCosts or analyzeTokens)
|
|
89
|
+
* @returns {object} Wrapped report data
|
|
90
|
+
*/
|
|
91
|
+
function generateWrapped(sprints, opts = {}) {
|
|
92
|
+
const stats = computeWrappedStats(sprints);
|
|
93
|
+
const personality = detectPersonality(stats);
|
|
94
|
+
const tierProgression = computeTierProgression(sprints);
|
|
95
|
+
const highlights = computeHighlights(sprints, stats);
|
|
96
|
+
const tokenSummary = summarizeTokens(opts.tokenReport);
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
period: determinePeriod(sprints),
|
|
100
|
+
sprintCount: sprints.length,
|
|
101
|
+
personality,
|
|
102
|
+
stats: {
|
|
103
|
+
totalClaims: stats.totalClaims,
|
|
104
|
+
totalActiveClaims: stats.activeClaims,
|
|
105
|
+
typeDistribution: stats.typeDistribution,
|
|
106
|
+
evidenceDistribution: stats.evidenceDistribution,
|
|
107
|
+
topTags: stats.topTags,
|
|
108
|
+
topTopics: stats.topTopics,
|
|
109
|
+
avgClaimsPerSprint: stats.avgClaimsPerSprint,
|
|
110
|
+
},
|
|
111
|
+
tierProgression,
|
|
112
|
+
highlights,
|
|
113
|
+
tokenSummary,
|
|
114
|
+
shareCard: buildShareCard(sprints.length, stats, personality),
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Compute raw stats for wrapped report.
|
|
120
|
+
*/
|
|
121
|
+
function computeWrappedStats(sprints) {
|
|
122
|
+
const typeDistribution = {};
|
|
123
|
+
const evidenceDistribution = {};
|
|
124
|
+
const tagCounts = {};
|
|
125
|
+
const topicCounts = {};
|
|
126
|
+
let totalClaims = 0;
|
|
127
|
+
let activeClaims = 0;
|
|
128
|
+
let challengeClaims = 0;
|
|
129
|
+
let contestedClaims = 0;
|
|
130
|
+
|
|
131
|
+
for (const sprint of sprints) {
|
|
132
|
+
for (const c of sprint.claims || []) {
|
|
133
|
+
totalClaims++;
|
|
134
|
+
if (c.status === "active") activeClaims++;
|
|
135
|
+
if (c.status === "contested") contestedClaims++;
|
|
136
|
+
|
|
137
|
+
const type = c.type || "unknown";
|
|
138
|
+
typeDistribution[type] = (typeDistribution[type] || 0) + 1;
|
|
139
|
+
|
|
140
|
+
// Handle both string and object evidence formats
|
|
141
|
+
const evidence =
|
|
142
|
+
typeof c.evidence === "string"
|
|
143
|
+
? c.evidence
|
|
144
|
+
: c.evidence?.tier || c.evidence_tier || "unknown";
|
|
145
|
+
evidenceDistribution[evidence] =
|
|
146
|
+
(evidenceDistribution[evidence] || 0) + 1;
|
|
147
|
+
|
|
148
|
+
// Track challenge claims (x-prefixed IDs)
|
|
149
|
+
if (c.id && c.id.startsWith("x")) challengeClaims++;
|
|
150
|
+
|
|
151
|
+
// Tags
|
|
152
|
+
for (const tag of c.tags || []) {
|
|
153
|
+
tagCounts[tag] = (tagCounts[tag] || 0) + 1;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Topics
|
|
157
|
+
const topic = c.topic || null;
|
|
158
|
+
if (topic) {
|
|
159
|
+
topicCounts[topic] = (topicCounts[topic] || 0) + 1;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Ratios for personality detection
|
|
165
|
+
const factualRatio =
|
|
166
|
+
totalClaims > 0 ? (typeDistribution.factual || 0) / totalClaims : 0;
|
|
167
|
+
const recommendationRatio =
|
|
168
|
+
totalClaims > 0 ? (typeDistribution.recommendation || 0) / totalClaims : 0;
|
|
169
|
+
const riskRatio =
|
|
170
|
+
totalClaims > 0 ? (typeDistribution.risk || 0) / totalClaims : 0;
|
|
171
|
+
const testedRatio =
|
|
172
|
+
totalClaims > 0 ? (evidenceDistribution.tested || 0) / totalClaims : 0;
|
|
173
|
+
const documentedRatio =
|
|
174
|
+
totalClaims > 0 ? (evidenceDistribution.documented || 0) / totalClaims : 0;
|
|
175
|
+
const challengeRatio =
|
|
176
|
+
totalClaims > 0 ? (challengeClaims + contestedClaims) / totalClaims : 0;
|
|
177
|
+
|
|
178
|
+
// Topic diversity: unique topics / total claims (normalized 0-1)
|
|
179
|
+
const uniqueTopics = Object.keys(topicCounts).length;
|
|
180
|
+
const topicDiversity =
|
|
181
|
+
totalClaims > 0 ? Math.min(1, uniqueTopics / Math.sqrt(totalClaims)) : 0;
|
|
182
|
+
|
|
183
|
+
// Top tags and topics
|
|
184
|
+
const topTags = Object.entries(tagCounts)
|
|
185
|
+
.sort((a, b) => b[1] - a[1])
|
|
186
|
+
.slice(0, 10)
|
|
187
|
+
.map(([tag, count]) => ({ tag, count }));
|
|
188
|
+
|
|
189
|
+
const topTopics = Object.entries(topicCounts)
|
|
190
|
+
.sort((a, b) => b[1] - a[1])
|
|
191
|
+
.slice(0, 5)
|
|
192
|
+
.map(([topic, count]) => ({ topic, count }));
|
|
193
|
+
|
|
194
|
+
return {
|
|
195
|
+
totalClaims,
|
|
196
|
+
activeClaims,
|
|
197
|
+
typeDistribution,
|
|
198
|
+
evidenceDistribution,
|
|
199
|
+
factualRatio,
|
|
200
|
+
recommendationRatio,
|
|
201
|
+
riskRatio,
|
|
202
|
+
testedRatio,
|
|
203
|
+
documentedRatio,
|
|
204
|
+
challengeRatio,
|
|
205
|
+
topicDiversity,
|
|
206
|
+
topTags,
|
|
207
|
+
topTopics,
|
|
208
|
+
avgClaimsPerSprint:
|
|
209
|
+
sprints.length > 0 ? Math.round(totalClaims / sprints.length) : 0,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Detect research personality archetype from usage stats.
|
|
215
|
+
*/
|
|
216
|
+
function detectPersonality(stats) {
|
|
217
|
+
// Check archetypes in priority order (most specific first)
|
|
218
|
+
const priorityOrder = [
|
|
219
|
+
"challenger",
|
|
220
|
+
"prototyper",
|
|
221
|
+
"scholar",
|
|
222
|
+
"strategist",
|
|
223
|
+
"sentinel",
|
|
224
|
+
"explorer",
|
|
225
|
+
"balanced",
|
|
226
|
+
];
|
|
227
|
+
|
|
228
|
+
for (const id of priorityOrder) {
|
|
229
|
+
const archetype = ARCHETYPES[id];
|
|
230
|
+
if (archetype.detect(stats)) {
|
|
231
|
+
return {
|
|
232
|
+
id: archetype.id,
|
|
233
|
+
label: archetype.label,
|
|
234
|
+
description: archetype.description,
|
|
235
|
+
confidence: computePersonalityConfidence(id, stats),
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return ARCHETYPES.balanced;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Compute confidence score for a personality match.
|
|
245
|
+
*/
|
|
246
|
+
function computePersonalityConfidence(id, stats) {
|
|
247
|
+
switch (id) {
|
|
248
|
+
case "challenger":
|
|
249
|
+
return Math.min(1, stats.challengeRatio / 0.3);
|
|
250
|
+
case "prototyper":
|
|
251
|
+
return Math.min(1, stats.testedRatio / 0.5);
|
|
252
|
+
case "scholar":
|
|
253
|
+
return Math.min(1, (stats.factualRatio + stats.documentedRatio) / 0.8);
|
|
254
|
+
case "strategist":
|
|
255
|
+
return Math.min(1, stats.recommendationRatio / 0.4);
|
|
256
|
+
case "sentinel":
|
|
257
|
+
return Math.min(1, stats.riskRatio / 0.3);
|
|
258
|
+
case "explorer":
|
|
259
|
+
return Math.min(1, stats.topicDiversity);
|
|
260
|
+
default:
|
|
261
|
+
return 0.5;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Compute evidence tier progression across sprints (ordered by time).
|
|
267
|
+
* Shows if the researcher is using higher-quality evidence over time.
|
|
268
|
+
*/
|
|
269
|
+
function computeTierProgression(sprints) {
|
|
270
|
+
const tierWeight = {
|
|
271
|
+
stated: 1,
|
|
272
|
+
web: 2,
|
|
273
|
+
documented: 3,
|
|
274
|
+
tested: 4,
|
|
275
|
+
production: 5,
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
if (sprints.length < 2) return null;
|
|
279
|
+
|
|
280
|
+
const sprintScores = sprints.map((sprint) => {
|
|
281
|
+
const claims = sprint.claims || [];
|
|
282
|
+
if (claims.length === 0)
|
|
283
|
+
return { sprint: sprint.name, avgTier: 0, claims: 0 };
|
|
284
|
+
|
|
285
|
+
let totalWeight = 0;
|
|
286
|
+
for (const c of claims) {
|
|
287
|
+
const evidence =
|
|
288
|
+
typeof c.evidence === "string"
|
|
289
|
+
? c.evidence
|
|
290
|
+
: c.evidence?.tier || c.evidence_tier || "stated";
|
|
291
|
+
totalWeight += tierWeight[evidence] || 1;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return {
|
|
295
|
+
sprint: sprint.name,
|
|
296
|
+
avgTier: Math.round((totalWeight / claims.length) * 100) / 100,
|
|
297
|
+
claims: claims.length,
|
|
298
|
+
};
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
// Trend: compare first half vs second half
|
|
302
|
+
const mid = Math.floor(sprintScores.length / 2);
|
|
303
|
+
const firstHalf = sprintScores.slice(0, mid).filter((s) => s.claims > 0);
|
|
304
|
+
const secondHalf = sprintScores.slice(mid).filter((s) => s.claims > 0);
|
|
305
|
+
|
|
306
|
+
const avgFirst =
|
|
307
|
+
firstHalf.length > 0
|
|
308
|
+
? firstHalf.reduce((a, s) => a + s.avgTier, 0) / firstHalf.length
|
|
309
|
+
: 0;
|
|
310
|
+
const avgSecond =
|
|
311
|
+
secondHalf.length > 0
|
|
312
|
+
? secondHalf.reduce((a, s) => a + s.avgTier, 0) / secondHalf.length
|
|
313
|
+
: 0;
|
|
314
|
+
|
|
315
|
+
return {
|
|
316
|
+
sprints: sprintScores,
|
|
317
|
+
trend:
|
|
318
|
+
avgSecond > avgFirst
|
|
319
|
+
? "improving"
|
|
320
|
+
: avgSecond < avgFirst
|
|
321
|
+
? "declining"
|
|
322
|
+
: "stable",
|
|
323
|
+
firstHalfAvg: Math.round(avgFirst * 100) / 100,
|
|
324
|
+
secondHalfAvg: Math.round(avgSecond * 100) / 100,
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Compute highlights and achievements.
|
|
330
|
+
*/
|
|
331
|
+
function computeHighlights(sprints, stats) {
|
|
332
|
+
const highlights = [];
|
|
333
|
+
|
|
334
|
+
// Sprint milestones
|
|
335
|
+
if (sprints.length >= 10)
|
|
336
|
+
highlights.push({
|
|
337
|
+
type: "milestone",
|
|
338
|
+
text: `${sprints.length} sprints completed`,
|
|
339
|
+
});
|
|
340
|
+
if (sprints.length >= 50)
|
|
341
|
+
highlights.push({
|
|
342
|
+
type: "milestone",
|
|
343
|
+
text: "Power researcher: 50+ sprints",
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
// Claim milestones
|
|
347
|
+
if (stats.totalClaims >= 100)
|
|
348
|
+
highlights.push({
|
|
349
|
+
type: "milestone",
|
|
350
|
+
text: `${stats.totalClaims} claims across all sprints`,
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
// Evidence quality
|
|
354
|
+
const tested = stats.evidenceDistribution.tested || 0;
|
|
355
|
+
const production = stats.evidenceDistribution.production || 0;
|
|
356
|
+
const highQuality = tested + production;
|
|
357
|
+
if (highQuality > 0 && stats.totalClaims > 0) {
|
|
358
|
+
const pct = Math.round((highQuality / stats.totalClaims) * 100);
|
|
359
|
+
if (pct >= 30)
|
|
360
|
+
highlights.push({
|
|
361
|
+
type: "quality",
|
|
362
|
+
text: `${pct}% of claims backed by tested or production evidence`,
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Challenge rate
|
|
367
|
+
if (stats.challengeRatio > 0.1) {
|
|
368
|
+
highlights.push({
|
|
369
|
+
type: "rigor",
|
|
370
|
+
text: `${Math.round(stats.challengeRatio * 100)}% challenge rate -- strong adversarial testing`,
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Top tag
|
|
375
|
+
if (stats.topTags.length > 0) {
|
|
376
|
+
highlights.push({
|
|
377
|
+
type: "focus",
|
|
378
|
+
text: `Most researched tag: "${stats.topTags[0].tag}" (${stats.topTags[0].count} claims)`,
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Most productive sprint
|
|
383
|
+
let maxClaims = 0;
|
|
384
|
+
let maxSprint = null;
|
|
385
|
+
for (const s of sprints) {
|
|
386
|
+
if ((s.claims || []).length > maxClaims) {
|
|
387
|
+
maxClaims = (s.claims || []).length;
|
|
388
|
+
maxSprint = s.name;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
if (maxSprint) {
|
|
392
|
+
highlights.push({
|
|
393
|
+
type: "record",
|
|
394
|
+
text: `Most productive sprint: "${maxSprint}" with ${maxClaims} claims`,
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
return highlights;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Summarize token costs for the wrapped report.
|
|
403
|
+
*/
|
|
404
|
+
function summarizeTokens(tokenReport) {
|
|
405
|
+
if (!tokenReport || !tokenReport.summary) return null;
|
|
406
|
+
|
|
407
|
+
const s = tokenReport.summary;
|
|
408
|
+
return {
|
|
409
|
+
totalCostUsd: s.totalCostUsd || 0,
|
|
410
|
+
costPerVerifiedClaim: s.costPerVerifiedClaim || null,
|
|
411
|
+
avgCostPerSprint: s.avgCostPerSprint || null,
|
|
412
|
+
costTrend: tokenReport.costTrend || null,
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Determine the reporting period from sprint data.
|
|
418
|
+
*/
|
|
419
|
+
function determinePeriod(sprints) {
|
|
420
|
+
const timestamps = [];
|
|
421
|
+
for (const sprint of sprints) {
|
|
422
|
+
for (const c of sprint.claims || []) {
|
|
423
|
+
const ts = c.timestamp || c.created;
|
|
424
|
+
if (ts) timestamps.push(new Date(ts));
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
if (timestamps.length === 0) {
|
|
429
|
+
return { start: null, end: null, label: "All time" };
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
timestamps.sort((a, b) => a - b);
|
|
433
|
+
const start = timestamps[0];
|
|
434
|
+
const end = timestamps[timestamps.length - 1];
|
|
435
|
+
|
|
436
|
+
// Determine label
|
|
437
|
+
const months = [
|
|
438
|
+
"Jan",
|
|
439
|
+
"Feb",
|
|
440
|
+
"Mar",
|
|
441
|
+
"Apr",
|
|
442
|
+
"May",
|
|
443
|
+
"Jun",
|
|
444
|
+
"Jul",
|
|
445
|
+
"Aug",
|
|
446
|
+
"Sep",
|
|
447
|
+
"Oct",
|
|
448
|
+
"Nov",
|
|
449
|
+
"Dec",
|
|
450
|
+
];
|
|
451
|
+
const label =
|
|
452
|
+
start.getFullYear() === end.getFullYear()
|
|
453
|
+
? `${months[start.getMonth()]}--${months[end.getMonth()]} ${end.getFullYear()}`
|
|
454
|
+
: `${months[start.getMonth()]} ${start.getFullYear()}--${months[end.getMonth()]} ${end.getFullYear()}`;
|
|
455
|
+
|
|
456
|
+
return {
|
|
457
|
+
start: start.toISOString(),
|
|
458
|
+
end: end.toISOString(),
|
|
459
|
+
label,
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* Build shareable summary card data (no proprietary content).
|
|
465
|
+
*/
|
|
466
|
+
function buildShareCard(sprintCount, stats, personality) {
|
|
467
|
+
return {
|
|
468
|
+
sprintCount,
|
|
469
|
+
totalClaims: stats.totalClaims,
|
|
470
|
+
personality: personality.label,
|
|
471
|
+
topEvidenceTier: getTopEvidenceTier(stats.evidenceDistribution),
|
|
472
|
+
avgClaimsPerSprint: stats.avgClaimsPerSprint,
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function getTopEvidenceTier(distribution) {
|
|
477
|
+
const tiers = ["production", "tested", "documented", "web", "stated"];
|
|
478
|
+
for (const tier of tiers) {
|
|
479
|
+
if (distribution[tier] > 0) return tier;
|
|
480
|
+
}
|
|
481
|
+
return "stated";
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
module.exports = {
|
|
485
|
+
generateWrapped,
|
|
486
|
+
computeWrappedStats,
|
|
487
|
+
detectPersonality,
|
|
488
|
+
ARCHETYPES,
|
|
489
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@grainulation/harvest",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "Analytics and retrospective layer for research sprints -- learn from every decision you've made",
|
|
5
5
|
"main": "lib/analyzer.js",
|
|
6
6
|
"exports": {
|
|
@@ -10,6 +10,10 @@
|
|
|
10
10
|
"./decay": "./lib/decay.js",
|
|
11
11
|
"./patterns": "./lib/patterns.js",
|
|
12
12
|
"./velocity": "./lib/velocity.js",
|
|
13
|
+
"./tokens": "./lib/tokens.js",
|
|
14
|
+
"./token-tracker": "./lib/token-tracker.js",
|
|
15
|
+
"./wrapped": "./lib/wrapped.js",
|
|
16
|
+
"./harvest-card": "./lib/harvest-card.js",
|
|
13
17
|
"./package.json": "./package.json"
|
|
14
18
|
},
|
|
15
19
|
"bin": {
|
|
@@ -29,16 +33,19 @@
|
|
|
29
33
|
],
|
|
30
34
|
"author": "grainulation contributors",
|
|
31
35
|
"license": "MIT",
|
|
36
|
+
"type": "module",
|
|
32
37
|
"files": [
|
|
33
38
|
"bin/",
|
|
34
39
|
"lib/",
|
|
35
40
|
"public/",
|
|
36
41
|
"templates/",
|
|
37
42
|
"README.md",
|
|
38
|
-
"LICENSE"
|
|
43
|
+
"LICENSE",
|
|
44
|
+
"CODE_OF_CONDUCT.md",
|
|
45
|
+
"CONTRIBUTING.md"
|
|
39
46
|
],
|
|
40
47
|
"engines": {
|
|
41
|
-
"node": ">=
|
|
48
|
+
"node": ">=20"
|
|
42
49
|
},
|
|
43
50
|
"repository": {
|
|
44
51
|
"type": "git",
|