@grainulation/harvest 1.0.1 → 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/CONTRIBUTING.md +6 -0
- package/README.md +12 -11
- 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 +322 -112
- 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 +7 -2
package/CONTRIBUTING.md
CHANGED
|
@@ -15,16 +15,20 @@ No `npm install` needed -- harvest has zero dependencies.
|
|
|
15
15
|
## How to contribute
|
|
16
16
|
|
|
17
17
|
### Report a bug
|
|
18
|
+
|
|
18
19
|
Open an issue with:
|
|
20
|
+
|
|
19
21
|
- What you expected
|
|
20
22
|
- What happened instead
|
|
21
23
|
- Your Node version (`node --version`)
|
|
22
24
|
- Steps to reproduce
|
|
23
25
|
|
|
24
26
|
### Suggest a feature
|
|
27
|
+
|
|
25
28
|
Open an issue describing the use case, not just the solution. "I need X because Y" is more useful than "add X."
|
|
26
29
|
|
|
27
30
|
### Submit a PR
|
|
31
|
+
|
|
28
32
|
1. Fork the repo
|
|
29
33
|
2. Create a branch (`git checkout -b fix/description`)
|
|
30
34
|
3. Make your changes
|
|
@@ -71,11 +75,13 @@ Tests use Node's built-in test runner. No test framework dependencies.
|
|
|
71
75
|
## Commit messages
|
|
72
76
|
|
|
73
77
|
Follow the existing pattern:
|
|
78
|
+
|
|
74
79
|
```
|
|
75
80
|
harvest: <what changed>
|
|
76
81
|
```
|
|
77
82
|
|
|
78
83
|
Examples:
|
|
84
|
+
|
|
79
85
|
```
|
|
80
86
|
harvest: add velocity trend chart
|
|
81
87
|
harvest: fix decay calculation for stale claims
|
package/README.md
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
</p>
|
|
4
4
|
|
|
5
5
|
<p align="center">
|
|
6
|
-
<a href="https://www.npmjs.com/package/@grainulation/harvest"><img src="https://img.shields.io/npm/v/@grainulation/harvest" alt="npm version"></a> <a href="https://www.npmjs.com/package/@grainulation/harvest"><img src="https://img.shields.io/npm/dm/@grainulation/harvest" alt="npm downloads"></a> <a href="https://github.com/grainulation/harvest/blob/main/LICENSE"><img src="https://img.shields.io/
|
|
6
|
+
<a href="https://www.npmjs.com/package/@grainulation/harvest"><img src="https://img.shields.io/npm/v/@grainulation/harvest" alt="npm version"></a> <a href="https://www.npmjs.com/package/@grainulation/harvest"><img src="https://img.shields.io/npm/dm/@grainulation/harvest" alt="npm downloads"></a> <a href="https://github.com/grainulation/harvest/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-MIT-green" alt="license"></a> <a href="https://nodejs.org"><img src="https://img.shields.io/node/v/@grainulation/harvest" alt="node"></a> <a href="https://github.com/grainulation/harvest/actions"><img src="https://github.com/grainulation/harvest/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
|
|
7
|
+
<a href="https://deepwiki.com/grainulation/harvest"><img src="https://deepwiki.com/badge.svg" alt="Explore on DeepWiki"></a>
|
|
7
8
|
</p>
|
|
8
9
|
|
|
9
10
|
<p align="center"><strong>Are your decisions getting better?</strong></p>
|
|
@@ -84,16 +85,16 @@ Node built-in modules only.
|
|
|
84
85
|
|
|
85
86
|
## Part of the grainulation ecosystem
|
|
86
87
|
|
|
87
|
-
| Tool
|
|
88
|
-
|
|
89
|
-
| [wheat](https://github.com/grainulation/wheat)
|
|
90
|
-
| [farmer](https://github.com/grainulation/farmer)
|
|
91
|
-
| [barn](https://github.com/grainulation/barn)
|
|
92
|
-
| [mill](https://github.com/grainulation/mill)
|
|
93
|
-
| [silo](https://github.com/grainulation/silo)
|
|
94
|
-
| **harvest**
|
|
95
|
-
| [orchard](https://github.com/grainulation/orchard)
|
|
96
|
-
| [grainulation](https://github.com/grainulation/grainulation) | Unified CLI -- single entry point to the ecosystem
|
|
88
|
+
| Tool | Role |
|
|
89
|
+
| ------------------------------------------------------------ | ----------------------------------------------------------- |
|
|
90
|
+
| [wheat](https://github.com/grainulation/wheat) | Research engine -- grow structured evidence |
|
|
91
|
+
| [farmer](https://github.com/grainulation/farmer) | Permission dashboard -- approve AI actions in real time |
|
|
92
|
+
| [barn](https://github.com/grainulation/barn) | Shared tools -- templates, validators, sprint detection |
|
|
93
|
+
| [mill](https://github.com/grainulation/mill) | Format conversion -- export to PDF, CSV, slides, 24 formats |
|
|
94
|
+
| [silo](https://github.com/grainulation/silo) | Knowledge storage -- reusable claim libraries and packs |
|
|
95
|
+
| **harvest** | Analytics -- cross-sprint patterns and prediction scoring |
|
|
96
|
+
| [orchard](https://github.com/grainulation/orchard) | Orchestration -- multi-sprint coordination and dependencies |
|
|
97
|
+
| [grainulation](https://github.com/grainulation/grainulation) | Unified CLI -- single entry point to the ecosystem |
|
|
97
98
|
|
|
98
99
|
## License
|
|
99
100
|
|
package/bin/harvest.js
CHANGED
|
@@ -1,23 +1,31 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
const path = require(
|
|
6
|
-
const fs = require(
|
|
7
|
-
|
|
8
|
-
const { analyze } = require(
|
|
9
|
-
const { calibrate } = require(
|
|
10
|
-
const { detectPatterns } = require(
|
|
11
|
-
const { checkDecay } = require(
|
|
12
|
-
const { measureVelocity } = require(
|
|
13
|
-
const { generateReport } = require(
|
|
14
|
-
const { connect: farmerConnect } = require(
|
|
15
|
-
|
|
16
|
-
const
|
|
3
|
+
"use strict";
|
|
4
|
+
|
|
5
|
+
const path = require("node:path");
|
|
6
|
+
const fs = require("node:fs");
|
|
7
|
+
|
|
8
|
+
const { analyze } = require("../lib/analyzer.js");
|
|
9
|
+
const { calibrate } = require("../lib/calibration.js");
|
|
10
|
+
const { detectPatterns } = require("../lib/patterns.js");
|
|
11
|
+
const { checkDecay, decayAlerts } = require("../lib/decay.js");
|
|
12
|
+
const { measureVelocity } = require("../lib/velocity.js");
|
|
13
|
+
const { generateReport } = require("../lib/report.js");
|
|
14
|
+
const { connect: farmerConnect } = require("../lib/farmer.js");
|
|
15
|
+
const { analyzeTokens } = require("../lib/tokens.js");
|
|
16
|
+
const { trackCosts } = require("../lib/token-tracker.js");
|
|
17
|
+
const { generateWrapped } = require("../lib/wrapped.js");
|
|
18
|
+
const {
|
|
19
|
+
generateCard,
|
|
20
|
+
generateEmbedSnippet,
|
|
21
|
+
} = require("../lib/harvest-card.js");
|
|
22
|
+
|
|
23
|
+
const verbose =
|
|
24
|
+
process.argv.includes("--verbose") || process.argv.includes("-v");
|
|
17
25
|
function vlog(...a) {
|
|
18
26
|
if (!verbose) return;
|
|
19
27
|
const ts = new Date().toISOString();
|
|
20
|
-
process.stderr.write(`[${ts}] harvest: ${a.join(
|
|
28
|
+
process.stderr.write(`[${ts}] harvest: ${a.join(" ")}\n`);
|
|
21
29
|
}
|
|
22
30
|
|
|
23
31
|
const USAGE = `
|
|
@@ -29,8 +37,11 @@ Usage:
|
|
|
29
37
|
harvest patterns <sprints-dir> Detect decision patterns
|
|
30
38
|
harvest decay <sprints-dir> Find claims that need refreshing
|
|
31
39
|
harvest velocity <sprints-dir> Sprint timing and phase analysis
|
|
40
|
+
harvest tokens <sprints-dir> Token cost tracking and efficiency
|
|
41
|
+
harvest card <sprints-dir> [-o <output>] Generate Harvest Report SVG card
|
|
32
42
|
harvest report <sprints-dir> [-o <output>] Generate retrospective HTML
|
|
33
43
|
harvest trends <sprints-dir> All analyses in one pass
|
|
44
|
+
harvest intelligence <sprints-dir> Full intelligence report (all features)
|
|
34
45
|
harvest serve [--port 9096] [--root <sprints-dir>] Start the dashboard UI
|
|
35
46
|
harvest connect farmer [--url <url>] Configure farmer integration
|
|
36
47
|
|
|
@@ -43,22 +54,29 @@ Options:
|
|
|
43
54
|
|
|
44
55
|
function parseArgs(argv) {
|
|
45
56
|
const args = argv.slice(2);
|
|
46
|
-
const parsed = {
|
|
57
|
+
const parsed = {
|
|
58
|
+
command: null,
|
|
59
|
+
dir: null,
|
|
60
|
+
output: null,
|
|
61
|
+
json: false,
|
|
62
|
+
days: 90,
|
|
63
|
+
};
|
|
47
64
|
|
|
48
|
-
if (args.length === 0 || args.includes(
|
|
65
|
+
if (args.length === 0 || args.includes("-h") || args.includes("--help")) {
|
|
49
66
|
console.log(USAGE);
|
|
50
67
|
process.exit(0);
|
|
51
68
|
}
|
|
52
69
|
|
|
53
70
|
parsed.command = args[0];
|
|
54
|
-
parsed.dir =
|
|
71
|
+
parsed.dir =
|
|
72
|
+
args[1] && !args[1].startsWith("-") ? path.resolve(args[1]) : null;
|
|
55
73
|
|
|
56
74
|
for (let i = 2; i < args.length; i++) {
|
|
57
|
-
if ((args[i] ===
|
|
75
|
+
if ((args[i] === "-o" || args[i] === "--output") && args[i + 1]) {
|
|
58
76
|
parsed.output = path.resolve(args[++i]);
|
|
59
|
-
} else if (args[i] ===
|
|
77
|
+
} else if (args[i] === "--json") {
|
|
60
78
|
parsed.json = true;
|
|
61
|
-
} else if (args[i] ===
|
|
79
|
+
} else if (args[i] === "--days" && args[i + 1]) {
|
|
62
80
|
parsed.days = parseInt(args[++i], 10);
|
|
63
81
|
}
|
|
64
82
|
}
|
|
@@ -75,7 +93,7 @@ function loadSprintData(dir) {
|
|
|
75
93
|
const sprints = [];
|
|
76
94
|
|
|
77
95
|
// Include root if it has claims.json
|
|
78
|
-
const directClaims = path.join(dir,
|
|
96
|
+
const directClaims = path.join(dir, "claims.json");
|
|
79
97
|
if (fs.existsSync(directClaims)) {
|
|
80
98
|
sprints.push(loadSingleSprint(dir));
|
|
81
99
|
}
|
|
@@ -85,9 +103,9 @@ function loadSprintData(dir) {
|
|
|
85
103
|
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
86
104
|
for (const entry of entries) {
|
|
87
105
|
if (!entry.isDirectory()) continue;
|
|
88
|
-
if (entry.name.startsWith(
|
|
106
|
+
if (entry.name.startsWith(".")) continue;
|
|
89
107
|
const childDir = path.join(dir, entry.name);
|
|
90
|
-
const childClaims = path.join(childDir,
|
|
108
|
+
const childClaims = path.join(childDir, "claims.json");
|
|
91
109
|
if (fs.existsSync(childClaims)) {
|
|
92
110
|
sprints.push(loadSingleSprint(childDir));
|
|
93
111
|
}
|
|
@@ -96,20 +114,26 @@ function loadSprintData(dir) {
|
|
|
96
114
|
const subEntries = fs.readdirSync(childDir, { withFileTypes: true });
|
|
97
115
|
for (const sub of subEntries) {
|
|
98
116
|
if (!sub.isDirectory()) continue;
|
|
99
|
-
if (sub.name.startsWith(
|
|
117
|
+
if (sub.name.startsWith(".")) continue;
|
|
100
118
|
const subDir = path.join(childDir, sub.name);
|
|
101
|
-
const subClaims = path.join(subDir,
|
|
119
|
+
const subClaims = path.join(subDir, "claims.json");
|
|
102
120
|
if (fs.existsSync(subClaims)) {
|
|
103
121
|
sprints.push(loadSingleSprint(subDir));
|
|
104
122
|
}
|
|
105
123
|
}
|
|
106
|
-
} catch {
|
|
124
|
+
} catch {
|
|
125
|
+
/* skip */
|
|
126
|
+
}
|
|
107
127
|
}
|
|
108
|
-
} catch {
|
|
128
|
+
} catch {
|
|
129
|
+
/* skip */
|
|
130
|
+
}
|
|
109
131
|
|
|
110
132
|
if (sprints.length === 0) {
|
|
111
133
|
console.error(`harvest: no sprint data found in ${dir}`);
|
|
112
|
-
console.error(
|
|
134
|
+
console.error(
|
|
135
|
+
"Expected claims.json in the directory or its subdirectories.",
|
|
136
|
+
);
|
|
113
137
|
process.exit(1);
|
|
114
138
|
}
|
|
115
139
|
|
|
@@ -125,9 +149,9 @@ function loadSingleSprint(dir) {
|
|
|
125
149
|
gitLog: null,
|
|
126
150
|
};
|
|
127
151
|
|
|
128
|
-
const claimsPath = path.join(dir,
|
|
152
|
+
const claimsPath = path.join(dir, "claims.json");
|
|
129
153
|
try {
|
|
130
|
-
sprint.claims = JSON.parse(fs.readFileSync(claimsPath,
|
|
154
|
+
sprint.claims = JSON.parse(fs.readFileSync(claimsPath, "utf8"));
|
|
131
155
|
if (!Array.isArray(sprint.claims)) {
|
|
132
156
|
// Handle { claims: [...] } wrapper
|
|
133
157
|
sprint.claims = sprint.claims.claims || [];
|
|
@@ -136,10 +160,10 @@ function loadSingleSprint(dir) {
|
|
|
136
160
|
console.error(`harvest: could not parse ${claimsPath}: ${e.message}`);
|
|
137
161
|
}
|
|
138
162
|
|
|
139
|
-
const compilationPath = path.join(dir,
|
|
163
|
+
const compilationPath = path.join(dir, "compilation.json");
|
|
140
164
|
if (fs.existsSync(compilationPath)) {
|
|
141
165
|
try {
|
|
142
|
-
sprint.compilation = JSON.parse(fs.readFileSync(compilationPath,
|
|
166
|
+
sprint.compilation = JSON.parse(fs.readFileSync(compilationPath, "utf8"));
|
|
143
167
|
} catch (e) {
|
|
144
168
|
// skip
|
|
145
169
|
}
|
|
@@ -147,14 +171,23 @@ function loadSingleSprint(dir) {
|
|
|
147
171
|
|
|
148
172
|
// Try to read git log for the sprint directory
|
|
149
173
|
try {
|
|
150
|
-
const { execSync } = require(
|
|
174
|
+
const { execSync } = require("node:child_process");
|
|
151
175
|
sprint.gitLog = execSync(
|
|
152
176
|
`git log --oneline --format="%H|%ai|%s" -- claims.json`,
|
|
153
|
-
{
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
177
|
+
{
|
|
178
|
+
cwd: dir,
|
|
179
|
+
encoding: "utf8",
|
|
180
|
+
timeout: 5000,
|
|
181
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
182
|
+
},
|
|
183
|
+
)
|
|
184
|
+
.trim()
|
|
185
|
+
.split("\n")
|
|
186
|
+
.filter(Boolean)
|
|
187
|
+
.map((line) => {
|
|
188
|
+
const [hash, date, ...msg] = line.split("|");
|
|
189
|
+
return { hash, date, message: msg.join("|") };
|
|
190
|
+
});
|
|
158
191
|
} catch (e) {
|
|
159
192
|
sprint.gitLog = [];
|
|
160
193
|
}
|
|
@@ -165,7 +198,7 @@ function loadSingleSprint(dir) {
|
|
|
165
198
|
function output(result, opts) {
|
|
166
199
|
if (opts.json) {
|
|
167
200
|
console.log(JSON.stringify(result, null, 2));
|
|
168
|
-
} else if (typeof result ===
|
|
201
|
+
} else if (typeof result === "string") {
|
|
169
202
|
console.log(result);
|
|
170
203
|
} else {
|
|
171
204
|
console.log(JSON.stringify(result, null, 2));
|
|
@@ -174,7 +207,11 @@ function output(result, opts) {
|
|
|
174
207
|
|
|
175
208
|
async function main() {
|
|
176
209
|
const opts = parseArgs(process.argv);
|
|
177
|
-
vlog(
|
|
210
|
+
vlog(
|
|
211
|
+
"startup",
|
|
212
|
+
`command=${opts.command || "(none)"}`,
|
|
213
|
+
`dir=${opts.dir || "none"}`,
|
|
214
|
+
);
|
|
178
215
|
|
|
179
216
|
const commands = {
|
|
180
217
|
analyze() {
|
|
@@ -210,11 +247,49 @@ async function main() {
|
|
|
210
247
|
patternsFn: detectPatterns,
|
|
211
248
|
decayFn: checkDecay,
|
|
212
249
|
velocityFn: measureVelocity,
|
|
250
|
+
tokensFn: analyzeTokens,
|
|
251
|
+
wrappedFn: generateWrapped,
|
|
213
252
|
});
|
|
214
|
-
const outPath =
|
|
215
|
-
|
|
253
|
+
const outPath =
|
|
254
|
+
opts.output || path.join(process.cwd(), "retrospective.html");
|
|
255
|
+
fs.writeFileSync(outPath, html, "utf8");
|
|
216
256
|
console.log(`Retrospective written to ${outPath}`);
|
|
217
257
|
},
|
|
258
|
+
tokens() {
|
|
259
|
+
const sprints = loadSprintData(opts.dir);
|
|
260
|
+
const result = analyzeTokens(sprints);
|
|
261
|
+
output(result, opts);
|
|
262
|
+
},
|
|
263
|
+
card() {
|
|
264
|
+
const sprints = loadSprintData(opts.dir);
|
|
265
|
+
const { svg, stats } = generateCard(sprints);
|
|
266
|
+
|
|
267
|
+
if (opts.json) {
|
|
268
|
+
output(stats, opts);
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const outPath =
|
|
273
|
+
opts.output || path.join(process.cwd(), "harvest-card.svg");
|
|
274
|
+
fs.writeFileSync(outPath, svg, "utf8");
|
|
275
|
+
const embed = generateEmbedSnippet(path.basename(outPath));
|
|
276
|
+
console.log(`Harvest card written to ${outPath}`);
|
|
277
|
+
console.log(`\nEmbed in README:\n ${embed.markdown}`);
|
|
278
|
+
console.log(`\nHTML:\n ${embed.html}`);
|
|
279
|
+
},
|
|
280
|
+
intelligence() {
|
|
281
|
+
const sprints = loadSprintData(opts.dir);
|
|
282
|
+
const result = {
|
|
283
|
+
analysis: analyze(sprints),
|
|
284
|
+
calibration: calibrate(sprints),
|
|
285
|
+
patterns: detectPatterns(sprints),
|
|
286
|
+
decay: checkDecay(sprints, { thresholdDays: opts.days }),
|
|
287
|
+
decayAlerts: decayAlerts(sprints),
|
|
288
|
+
velocity: measureVelocity(sprints),
|
|
289
|
+
tokens: analyzeTokens(sprints),
|
|
290
|
+
};
|
|
291
|
+
output(result, opts);
|
|
292
|
+
},
|
|
218
293
|
trends() {
|
|
219
294
|
const sprints = loadSprintData(opts.dir);
|
|
220
295
|
const result = {
|
|
@@ -228,47 +303,47 @@ async function main() {
|
|
|
228
303
|
},
|
|
229
304
|
};
|
|
230
305
|
|
|
231
|
-
if (opts.command ===
|
|
306
|
+
if (opts.command === "help") {
|
|
232
307
|
console.log(USAGE);
|
|
233
308
|
process.exit(0);
|
|
234
309
|
}
|
|
235
310
|
|
|
236
|
-
if (opts.command ===
|
|
311
|
+
if (opts.command === "connect") {
|
|
237
312
|
// Forward remaining args to farmer connect handler
|
|
238
|
-
const connectArgs = process.argv.slice(process.argv.indexOf(
|
|
313
|
+
const connectArgs = process.argv.slice(process.argv.indexOf("connect") + 1);
|
|
239
314
|
await farmerConnect(opts.dir || process.cwd(), connectArgs);
|
|
240
315
|
return;
|
|
241
316
|
}
|
|
242
317
|
|
|
243
|
-
if (opts.command ===
|
|
318
|
+
if (opts.command === "serve") {
|
|
244
319
|
// Launch the ESM server module
|
|
245
|
-
const { execFile } = require(
|
|
246
|
-
const serverPath = path.join(__dirname,
|
|
320
|
+
const { execFile } = require("node:child_process");
|
|
321
|
+
const serverPath = path.join(__dirname, "..", "lib", "server.js");
|
|
247
322
|
const serverArgs = [];
|
|
248
323
|
// Forward --port and --root
|
|
249
|
-
const portIdx = process.argv.indexOf(
|
|
324
|
+
const portIdx = process.argv.indexOf("--port");
|
|
250
325
|
if (portIdx !== -1 && process.argv[portIdx + 1]) {
|
|
251
|
-
serverArgs.push(
|
|
326
|
+
serverArgs.push("--port", process.argv[portIdx + 1]);
|
|
252
327
|
}
|
|
253
|
-
const rootIdx = process.argv.indexOf(
|
|
328
|
+
const rootIdx = process.argv.indexOf("--root");
|
|
254
329
|
if (rootIdx !== -1 && process.argv[rootIdx + 1]) {
|
|
255
|
-
serverArgs.push(
|
|
330
|
+
serverArgs.push("--root", process.argv[rootIdx + 1]);
|
|
256
331
|
} else if (opts.dir) {
|
|
257
|
-
serverArgs.push(
|
|
332
|
+
serverArgs.push("--root", opts.dir);
|
|
258
333
|
}
|
|
259
|
-
const child = execFile(
|
|
260
|
-
stdio:
|
|
334
|
+
const child = execFile("node", [serverPath, ...serverArgs], {
|
|
335
|
+
stdio: "inherit",
|
|
261
336
|
env: process.env,
|
|
262
337
|
});
|
|
263
338
|
child.stdout && child.stdout.pipe(process.stdout);
|
|
264
339
|
child.stderr && child.stderr.pipe(process.stderr);
|
|
265
|
-
child.on(
|
|
340
|
+
child.on("error", (err) => {
|
|
266
341
|
console.error(`harvest: error starting server: ${err.message}`);
|
|
267
342
|
process.exit(1);
|
|
268
343
|
});
|
|
269
|
-
child.on(
|
|
270
|
-
process.on(
|
|
271
|
-
process.on(
|
|
344
|
+
child.on("exit", (code) => process.exit(code || 0));
|
|
345
|
+
process.on("SIGTERM", () => child.kill("SIGTERM"));
|
|
346
|
+
process.on("SIGINT", () => child.kill("SIGINT"));
|
|
272
347
|
return;
|
|
273
348
|
}
|
|
274
349
|
|
package/lib/analyzer.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
"use strict";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Cross-sprint claim analysis.
|
|
@@ -11,19 +11,21 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
function analyze(sprints) {
|
|
14
|
-
const allClaims = sprints.flatMap(s =>
|
|
14
|
+
const allClaims = sprints.flatMap((s) =>
|
|
15
|
+
s.claims.map((c) => ({ ...c, _sprint: s.name })),
|
|
16
|
+
);
|
|
15
17
|
|
|
16
|
-
const typeDistribution = countBy(allClaims,
|
|
17
|
-
const evidenceDistribution = countBy(allClaims,
|
|
18
|
-
const statusDistribution = countBy(allClaims,
|
|
18
|
+
const typeDistribution = countBy(allClaims, "type");
|
|
19
|
+
const evidenceDistribution = countBy(allClaims, "evidence");
|
|
20
|
+
const statusDistribution = countBy(allClaims, "status");
|
|
19
21
|
|
|
20
22
|
// Per-sprint density
|
|
21
|
-
const perSprint = sprints.map(s => ({
|
|
23
|
+
const perSprint = sprints.map((s) => ({
|
|
22
24
|
name: s.name,
|
|
23
25
|
claimCount: s.claims.length,
|
|
24
|
-
types: countBy(s.claims,
|
|
25
|
-
evidence: countBy(s.claims,
|
|
26
|
-
statuses: countBy(s.claims,
|
|
26
|
+
types: countBy(s.claims, "type"),
|
|
27
|
+
evidence: countBy(s.claims, "evidence"),
|
|
28
|
+
statuses: countBy(s.claims, "status"),
|
|
27
29
|
}));
|
|
28
30
|
|
|
29
31
|
// Find cross-sprint themes by looking at tags
|
|
@@ -36,35 +38,40 @@ function analyze(sprints) {
|
|
|
36
38
|
}
|
|
37
39
|
|
|
38
40
|
// Identify weak spots: claims with low evidence tiers
|
|
39
|
-
const weakClaims = allClaims.filter(
|
|
40
|
-
c.evidence ===
|
|
41
|
+
const weakClaims = allClaims.filter(
|
|
42
|
+
(c) => c.evidence === "stated" || c.evidence === "web",
|
|
41
43
|
);
|
|
42
44
|
|
|
43
45
|
// Type monoculture detection: sprints dominated by a single claim type
|
|
44
|
-
const monocultures = perSprint
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
46
|
+
const monocultures = perSprint
|
|
47
|
+
.filter((s) => {
|
|
48
|
+
const types = Object.entries(s.types);
|
|
49
|
+
if (types.length === 0) return false;
|
|
50
|
+
const max = Math.max(...types.map(([, v]) => v));
|
|
51
|
+
return max / s.claimCount > 0.7 && s.claimCount > 3;
|
|
52
|
+
})
|
|
53
|
+
.map((s) => ({
|
|
54
|
+
sprint: s.name,
|
|
55
|
+
dominantType: Object.entries(s.types).sort((a, b) => b[1] - a[1])[0][0],
|
|
56
|
+
ratio: Math.round(
|
|
57
|
+
(Math.max(...Object.values(s.types)) / s.claimCount) * 100,
|
|
58
|
+
),
|
|
59
|
+
}));
|
|
54
60
|
|
|
55
61
|
return {
|
|
56
62
|
summary: {
|
|
57
63
|
totalSprints: sprints.length,
|
|
58
64
|
totalClaims: allClaims.length,
|
|
59
|
-
averageClaimsPerSprint:
|
|
60
|
-
|
|
61
|
-
|
|
65
|
+
averageClaimsPerSprint:
|
|
66
|
+
sprints.length > 0
|
|
67
|
+
? Math.round((allClaims.length / sprints.length) * 10) / 10
|
|
68
|
+
: 0,
|
|
62
69
|
},
|
|
63
70
|
typeDistribution,
|
|
64
71
|
evidenceDistribution,
|
|
65
72
|
statusDistribution,
|
|
66
73
|
tagFrequency,
|
|
67
|
-
weakClaims: weakClaims.map(c => ({
|
|
74
|
+
weakClaims: weakClaims.map((c) => ({
|
|
68
75
|
id: c.id,
|
|
69
76
|
sprint: c._sprint,
|
|
70
77
|
type: c.type,
|
|
@@ -79,7 +86,7 @@ function analyze(sprints) {
|
|
|
79
86
|
function countBy(items, key) {
|
|
80
87
|
const counts = {};
|
|
81
88
|
for (const item of items) {
|
|
82
|
-
const val = item[key] ||
|
|
89
|
+
const val = item[key] || "unknown";
|
|
83
90
|
counts[val] = (counts[val] || 0) + 1;
|
|
84
91
|
}
|
|
85
92
|
return counts;
|