@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
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Code of Conduct
|
|
2
|
+
|
|
3
|
+
## Our standards
|
|
4
|
+
|
|
5
|
+
We are committed to providing a welcoming and productive environment for everyone. We expect participants to:
|
|
6
|
+
|
|
7
|
+
- Use welcoming and inclusive language
|
|
8
|
+
- Respect differing viewpoints and experiences
|
|
9
|
+
- Accept constructive criticism gracefully
|
|
10
|
+
- Focus on what is best for the community and the project
|
|
11
|
+
- Show empathy toward other participants
|
|
12
|
+
|
|
13
|
+
Unacceptable behavior includes harassment, trolling, personal attacks, and publishing others' private information without permission.
|
|
14
|
+
|
|
15
|
+
## Scope
|
|
16
|
+
|
|
17
|
+
This code of conduct applies to all project spaces -- issues, pull requests, discussions, and any public channel where someone represents the project.
|
|
18
|
+
|
|
19
|
+
## Enforcement
|
|
20
|
+
|
|
21
|
+
Instances of unacceptable behavior may be reported to the project maintainers. All complaints will be reviewed and investigated, and will result in a response deemed necessary and appropriate.
|
|
22
|
+
|
|
23
|
+
## Attribution
|
|
24
|
+
|
|
25
|
+
Adapted from the [Contributor Covenant](https://www.contributor-covenant.org/), version 2.1.
|
package/CONTRIBUTING.md
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# Contributing to Harvest
|
|
2
|
+
|
|
3
|
+
Thanks for considering contributing. Harvest is the retrospective and analytics engine for the grainulation ecosystem -- it turns sprint history into insights and calibration data.
|
|
4
|
+
|
|
5
|
+
## Quick setup
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
git clone https://github.com/grainulation/harvest.git
|
|
9
|
+
cd harvest
|
|
10
|
+
node bin/harvest.js --help
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
No `npm install` needed -- harvest has zero dependencies.
|
|
14
|
+
|
|
15
|
+
## How to contribute
|
|
16
|
+
|
|
17
|
+
### Report a bug
|
|
18
|
+
|
|
19
|
+
Open an issue with:
|
|
20
|
+
|
|
21
|
+
- What you expected
|
|
22
|
+
- What happened instead
|
|
23
|
+
- Your Node version (`node --version`)
|
|
24
|
+
- Steps to reproduce
|
|
25
|
+
|
|
26
|
+
### Suggest a feature
|
|
27
|
+
|
|
28
|
+
Open an issue describing the use case, not just the solution. "I need X because Y" is more useful than "add X."
|
|
29
|
+
|
|
30
|
+
### Submit a PR
|
|
31
|
+
|
|
32
|
+
1. Fork the repo
|
|
33
|
+
2. Create a branch (`git checkout -b fix/description`)
|
|
34
|
+
3. Make your changes
|
|
35
|
+
4. Run the tests: `node --test test/basic.test.js`
|
|
36
|
+
5. Commit with a clear message
|
|
37
|
+
6. Open a PR
|
|
38
|
+
|
|
39
|
+
## Architecture
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
bin/harvest.js CLI entrypoint -- dispatches subcommands
|
|
43
|
+
lib/analyzer.js Sprint data analysis and pattern detection
|
|
44
|
+
lib/calibration.js Prediction vs outcome scoring
|
|
45
|
+
lib/decay.js Claim staleness and evidence decay
|
|
46
|
+
lib/patterns.js Recurring pattern extraction
|
|
47
|
+
lib/report.js Report generation from analyzed data
|
|
48
|
+
lib/server.js Local preview server (SSE, zero deps)
|
|
49
|
+
lib/templates.js Template rendering for HTML output
|
|
50
|
+
lib/velocity.js Sprint velocity tracking
|
|
51
|
+
templates/ HTML templates (retrospective, etc.)
|
|
52
|
+
public/ Web UI -- retrospective dashboard
|
|
53
|
+
site/ Public website (harvest.grainulation.com)
|
|
54
|
+
test/ Node built-in test runner tests
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
The key architectural principle: **harvest reads sprint artifacts (claims, compilations, git history) and produces calibrated insights.** It never modifies source data -- read-only analysis, write-only reports.
|
|
58
|
+
|
|
59
|
+
## Code style
|
|
60
|
+
|
|
61
|
+
- Zero dependencies. If you need something, write it or use Node built-ins.
|
|
62
|
+
- No transpilation. Ship what you write.
|
|
63
|
+
- ESM imports (`import`/`export`). Node 18+ required.
|
|
64
|
+
- Keep functions small. If a function needs a scroll, split it.
|
|
65
|
+
- No emojis in code, CLI output, or reports.
|
|
66
|
+
|
|
67
|
+
## Testing
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
node --test test/basic.test.js
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Tests use Node's built-in test runner. No test framework dependencies.
|
|
74
|
+
|
|
75
|
+
## Commit messages
|
|
76
|
+
|
|
77
|
+
Follow the existing pattern:
|
|
78
|
+
|
|
79
|
+
```
|
|
80
|
+
harvest: <what changed>
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Examples:
|
|
84
|
+
|
|
85
|
+
```
|
|
86
|
+
harvest: add velocity trend chart
|
|
87
|
+
harvest: fix decay calculation for stale claims
|
|
88
|
+
harvest: update calibration scoring algorithm
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## License
|
|
92
|
+
|
|
93
|
+
MIT. See LICENSE for details.
|
package/README.md
CHANGED
|
@@ -1,10 +1,27 @@
|
|
|
1
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="site/wordmark.svg" alt="Harvest" width="400">
|
|
3
|
+
</p>
|
|
2
4
|
|
|
3
|
-
|
|
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/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>
|
|
8
|
+
</p>
|
|
4
9
|
|
|
5
|
-
|
|
10
|
+
<p align="center"><strong>Are your decisions getting better?</strong></p>
|
|
6
11
|
|
|
7
|
-
|
|
12
|
+
Harvest is the analytics layer for research sprints. It looks across sprints to find patterns, score predictions, and surface knowledge that's gone stale.
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install -g @grainulation/harvest
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Or use directly:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npx @grainulation/harvest analyze ./sprints/
|
|
24
|
+
```
|
|
8
25
|
|
|
9
26
|
## What it does
|
|
10
27
|
|
|
@@ -15,21 +32,9 @@ Learn from every decision you've made.
|
|
|
15
32
|
- **Sprint velocity** -- how long do sprints take, where do they stall?
|
|
16
33
|
- **Retrospective reports** -- dark-themed HTML reports for the team
|
|
17
34
|
|
|
18
|
-
##
|
|
19
|
-
|
|
20
|
-
```sh
|
|
21
|
-
npm install @grainulation/harvest
|
|
22
|
-
```
|
|
35
|
+
## Quick start
|
|
23
36
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
```sh
|
|
27
|
-
npx @grainulation/harvest analyze ./sprints/
|
|
28
|
-
```
|
|
29
|
-
|
|
30
|
-
## Usage
|
|
31
|
-
|
|
32
|
-
```sh
|
|
37
|
+
```bash
|
|
33
38
|
# Cross-sprint claim analysis
|
|
34
39
|
harvest analyze ./sprints/
|
|
35
40
|
|
|
@@ -50,52 +55,46 @@ harvest report ./sprints/ -o retrospective.html
|
|
|
50
55
|
|
|
51
56
|
# All analyses in one pass
|
|
52
57
|
harvest trends ./sprints/ --json
|
|
58
|
+
|
|
59
|
+
# Start the live dashboard (SSE updates, dark theme)
|
|
60
|
+
harvest serve --root ./sprints/ --port 9096
|
|
61
|
+
|
|
62
|
+
# Connect to farmer for mobile monitoring
|
|
63
|
+
harvest connect farmer --url http://localhost:9094
|
|
53
64
|
```
|
|
54
65
|
|
|
55
66
|
## Data format
|
|
56
67
|
|
|
57
68
|
Harvest reads standard wheat sprint data:
|
|
58
69
|
|
|
59
|
-
- `claims.json` -- array of typed claims with `id`, `type`, `evidence`, `status`, `text`, `created
|
|
70
|
+
- `claims.json` -- array of typed claims with `id`, `type`, `evidence`, `status`, `text`, `created`
|
|
60
71
|
- `compilation.json` -- compiled sprint state (optional, enriches analysis)
|
|
61
72
|
- Git history on `claims.json` -- used for velocity and timing analysis
|
|
62
73
|
|
|
63
|
-
Point harvest at a directory containing sprint subdirectories, or at a single sprint directory
|
|
64
|
-
|
|
65
|
-
```
|
|
66
|
-
sprints/
|
|
67
|
-
sprint-alpha/
|
|
68
|
-
claims.json
|
|
69
|
-
compilation.json
|
|
70
|
-
sprint-beta/
|
|
71
|
-
claims.json
|
|
72
|
-
```
|
|
74
|
+
Point harvest at a directory containing sprint subdirectories, or at a single sprint directory.
|
|
73
75
|
|
|
74
76
|
## Design
|
|
75
77
|
|
|
76
|
-
- **Zero dependencies** -- Node built-in modules only (fs, path, child_process)
|
|
77
78
|
- **Reads, never writes** -- harvest is a pure analysis tool; it won't modify your sprint data
|
|
78
79
|
- **Git-aware** -- uses git log timestamps for velocity analysis when available
|
|
79
80
|
- **Composable** -- each module (analyzer, calibration, patterns, decay, velocity) works independently
|
|
80
81
|
|
|
81
|
-
##
|
|
82
|
+
## Zero dependencies
|
|
82
83
|
|
|
83
|
-
|
|
84
|
-
|---|---|
|
|
85
|
-
| `constraint` | Hard requirements, non-negotiable |
|
|
86
|
-
| `factual` | Verifiable statements |
|
|
87
|
-
| `estimate` | Predictions, projections, ranges |
|
|
88
|
-
| `risk` | Potential failure modes |
|
|
89
|
-
| `recommendation` | Proposed courses of action |
|
|
90
|
-
| `feedback` | Stakeholder input |
|
|
84
|
+
Node built-in modules only.
|
|
91
85
|
|
|
92
|
-
##
|
|
86
|
+
## Part of the grainulation ecosystem
|
|
93
87
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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 |
|
|
99
98
|
|
|
100
99
|
## License
|
|
101
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
|
|