@grainulation/orchard 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +118 -0
- package/bin/orchard.js +209 -0
- package/lib/assignments.js +98 -0
- package/lib/conflicts.js +150 -0
- package/lib/dashboard.js +291 -0
- package/lib/doctor.js +137 -0
- package/lib/export.js +98 -0
- package/lib/farmer.js +107 -0
- package/lib/planner.js +198 -0
- package/lib/server.js +707 -0
- package/lib/sync.js +100 -0
- package/lib/tracker.js +124 -0
- package/package.json +50 -0
- package/public/index.html +922 -0
- package/templates/dashboard.html +981 -0
- package/templates/orchard-dashboard.html +171 -0
package/lib/sync.js
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const { readSprintState } = require('./tracker.js');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Sync all sprint states from their directories into orchard.json.
|
|
9
|
+
* Updates status based on what's actually on disk.
|
|
10
|
+
*/
|
|
11
|
+
function syncAll(config, root) {
|
|
12
|
+
const sprints = config.sprints || [];
|
|
13
|
+
let updated = 0;
|
|
14
|
+
|
|
15
|
+
console.log('');
|
|
16
|
+
console.log(' Syncing sprint states...');
|
|
17
|
+
console.log('');
|
|
18
|
+
|
|
19
|
+
for (const sprint of sprints) {
|
|
20
|
+
const state = readSprintState(sprint.path, root);
|
|
21
|
+
|
|
22
|
+
if (!state.exists) {
|
|
23
|
+
console.log(` [!] ${path.basename(sprint.path)}: directory not found`);
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const oldStatus = sprint.status;
|
|
28
|
+
const inferred = inferStatus(sprint, state);
|
|
29
|
+
|
|
30
|
+
if (oldStatus !== inferred) {
|
|
31
|
+
sprint.status = inferred;
|
|
32
|
+
updated++;
|
|
33
|
+
console.log(` [~] ${path.basename(sprint.path)}: ${oldStatus || 'unknown'} -> ${inferred}`);
|
|
34
|
+
} else {
|
|
35
|
+
console.log(` [=] ${path.basename(sprint.path)}: ${inferred} (${state.claimsCount} claims)`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (updated > 0) {
|
|
40
|
+
const configPath = path.join(root, 'orchard.json');
|
|
41
|
+
const tmp = configPath + '.tmp.' + process.pid;
|
|
42
|
+
fs.writeFileSync(tmp, JSON.stringify(config, null, 2) + '\n');
|
|
43
|
+
fs.renameSync(tmp, configPath);
|
|
44
|
+
console.log('');
|
|
45
|
+
console.log(` Updated ${updated} sprint(s) in orchard.json.`);
|
|
46
|
+
} else {
|
|
47
|
+
console.log('');
|
|
48
|
+
console.log(' All sprints up to date.');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
console.log('');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Infer sprint status from disk state + config.
|
|
56
|
+
*/
|
|
57
|
+
function inferStatus(sprint, state) {
|
|
58
|
+
// If manually set to done, keep it
|
|
59
|
+
if (sprint.status === 'done') return 'done';
|
|
60
|
+
|
|
61
|
+
// If manually blocked, keep it
|
|
62
|
+
if (sprint.status === 'blocked') return 'blocked';
|
|
63
|
+
|
|
64
|
+
if (!state.exists) return 'not-found';
|
|
65
|
+
if (state.claimsCount === 0) return 'not-started';
|
|
66
|
+
if (state.hasCompilation) return 'compiled';
|
|
67
|
+
return 'active';
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Check which sprints are ready to start (all dependencies met).
|
|
72
|
+
*/
|
|
73
|
+
function findReady(config, root) {
|
|
74
|
+
const sprints = config.sprints || [];
|
|
75
|
+
const statuses = new Map();
|
|
76
|
+
|
|
77
|
+
for (const s of sprints) {
|
|
78
|
+
statuses.set(s.path, s.status || 'unknown');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const ready = [];
|
|
82
|
+
for (const s of sprints) {
|
|
83
|
+
if (s.status === 'done' || s.status === 'active') continue;
|
|
84
|
+
|
|
85
|
+
const deps = s.depends_on || [];
|
|
86
|
+
const allDone = deps.every((d) => statuses.get(d) === 'done' || statuses.get(d) === 'compiled');
|
|
87
|
+
|
|
88
|
+
if (allDone) {
|
|
89
|
+
ready.push(s);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return ready;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
module.exports = {
|
|
97
|
+
syncAll,
|
|
98
|
+
inferStatus,
|
|
99
|
+
findReady,
|
|
100
|
+
};
|
package/lib/tracker.js
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Read sprint status from its directory.
|
|
8
|
+
* Looks for claims.json and compilation.json to determine state.
|
|
9
|
+
*/
|
|
10
|
+
function readSprintState(sprintPath, root) {
|
|
11
|
+
const absPath = path.isAbsolute(sprintPath) ? sprintPath : path.join(root, sprintPath);
|
|
12
|
+
const state = {
|
|
13
|
+
exists: false,
|
|
14
|
+
claimsCount: 0,
|
|
15
|
+
hasCompilation: false,
|
|
16
|
+
lastModified: null,
|
|
17
|
+
status: 'unknown',
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
if (!fs.existsSync(absPath)) return state;
|
|
21
|
+
state.exists = true;
|
|
22
|
+
|
|
23
|
+
const claimsPath = path.join(absPath, 'claims.json');
|
|
24
|
+
if (fs.existsSync(claimsPath)) {
|
|
25
|
+
try {
|
|
26
|
+
const claims = JSON.parse(fs.readFileSync(claimsPath, 'utf8'));
|
|
27
|
+
state.claimsCount = Array.isArray(claims) ? claims.length :
|
|
28
|
+
(claims.claims ? claims.claims.length : 0);
|
|
29
|
+
const stat = fs.statSync(claimsPath);
|
|
30
|
+
state.lastModified = stat.mtime;
|
|
31
|
+
} catch {
|
|
32
|
+
// Ignore parse errors
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const compilationPath = path.join(absPath, 'compilation.json');
|
|
37
|
+
state.hasCompilation = fs.existsSync(compilationPath);
|
|
38
|
+
|
|
39
|
+
// Infer status
|
|
40
|
+
if (state.claimsCount === 0) {
|
|
41
|
+
state.status = 'not-started';
|
|
42
|
+
} else if (state.hasCompilation) {
|
|
43
|
+
state.status = 'compiled';
|
|
44
|
+
} else {
|
|
45
|
+
state.status = 'in-progress';
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return state;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Print status table for all sprints.
|
|
53
|
+
*/
|
|
54
|
+
function printStatus(config, root) {
|
|
55
|
+
const sprints = config.sprints || [];
|
|
56
|
+
|
|
57
|
+
if (sprints.length === 0) {
|
|
58
|
+
console.log('No sprints configured. Add sprints to orchard.json.');
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const active = sprints.filter((s) => s.status === 'active' || !s.status).length;
|
|
63
|
+
const done = sprints.filter((s) => s.status === 'done').length;
|
|
64
|
+
|
|
65
|
+
console.log('');
|
|
66
|
+
console.log(` ${sprints.length} sprints tracked. ${active} active, ${done} done.`);
|
|
67
|
+
console.log(' ' + '-'.repeat(70));
|
|
68
|
+
console.log(
|
|
69
|
+
' ' +
|
|
70
|
+
'Sprint'.padEnd(20) +
|
|
71
|
+
'Status'.padEnd(14) +
|
|
72
|
+
'Claims'.padEnd(10) +
|
|
73
|
+
'Assigned'.padEnd(16) +
|
|
74
|
+
'Deadline'
|
|
75
|
+
);
|
|
76
|
+
console.log(' ' + '-'.repeat(70));
|
|
77
|
+
|
|
78
|
+
for (const sprint of sprints) {
|
|
79
|
+
const state = readSprintState(sprint.path, root);
|
|
80
|
+
const name = path.basename(sprint.path).substring(0, 18);
|
|
81
|
+
const status = sprint.status || state.status;
|
|
82
|
+
const claims = state.claimsCount.toString();
|
|
83
|
+
const assignee = (sprint.assigned_to || '-').substring(0, 14);
|
|
84
|
+
const deadline = sprint.deadline || '-';
|
|
85
|
+
|
|
86
|
+
const statusDisplay =
|
|
87
|
+
status === 'active' ? '* active' :
|
|
88
|
+
status === 'done' ? 'x done' :
|
|
89
|
+
status === 'blocked' ? '! blocked' :
|
|
90
|
+
` ${status}`;
|
|
91
|
+
|
|
92
|
+
console.log(
|
|
93
|
+
' ' +
|
|
94
|
+
name.padEnd(20) +
|
|
95
|
+
statusDisplay.padEnd(14) +
|
|
96
|
+
claims.padEnd(10) +
|
|
97
|
+
assignee.padEnd(16) +
|
|
98
|
+
deadline
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
console.log(' ' + '-'.repeat(70));
|
|
103
|
+
console.log('');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Get status summary as data (for dashboard generation).
|
|
108
|
+
*/
|
|
109
|
+
function getStatusData(config, root) {
|
|
110
|
+
return (config.sprints || []).map((sprint) => {
|
|
111
|
+
const state = readSprintState(sprint.path, root);
|
|
112
|
+
return {
|
|
113
|
+
...sprint,
|
|
114
|
+
state,
|
|
115
|
+
effectiveStatus: sprint.status || state.status,
|
|
116
|
+
};
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
module.exports = {
|
|
121
|
+
readSprintState,
|
|
122
|
+
printStatus,
|
|
123
|
+
getStatusData,
|
|
124
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@grainulation/orchard",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Multi-sprint research orchestrator \u2014 coordinate parallel research across teams",
|
|
5
|
+
"main": "lib/planner.js",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": "./lib/planner.js",
|
|
8
|
+
"./server": "./lib/server.js",
|
|
9
|
+
"./sync": "./lib/sync.js",
|
|
10
|
+
"./tracker": "./lib/tracker.js",
|
|
11
|
+
"./doctor": "./lib/doctor.js",
|
|
12
|
+
"./package.json": "./package.json"
|
|
13
|
+
},
|
|
14
|
+
"bin": {
|
|
15
|
+
"orchard": "bin/orchard.js"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"bin/",
|
|
19
|
+
"lib/",
|
|
20
|
+
"public/",
|
|
21
|
+
"templates/",
|
|
22
|
+
"README.md",
|
|
23
|
+
"LICENSE"
|
|
24
|
+
],
|
|
25
|
+
"scripts": {
|
|
26
|
+
"test": "node test/basic.test.js",
|
|
27
|
+
"start": "node bin/orchard.js status",
|
|
28
|
+
"serve": "node lib/server.js"
|
|
29
|
+
},
|
|
30
|
+
"keywords": [
|
|
31
|
+
"research",
|
|
32
|
+
"sprint",
|
|
33
|
+
"orchestrator",
|
|
34
|
+
"multi-sprint",
|
|
35
|
+
"wheat"
|
|
36
|
+
],
|
|
37
|
+
"author": "grainulation contributors",
|
|
38
|
+
"license": "MIT",
|
|
39
|
+
"repository": {
|
|
40
|
+
"type": "git",
|
|
41
|
+
"url": "git+https://github.com/grainulation/orchard.git"
|
|
42
|
+
},
|
|
43
|
+
"bugs": {
|
|
44
|
+
"url": "https://github.com/grainulation/orchard/issues"
|
|
45
|
+
},
|
|
46
|
+
"homepage": "https://orchard.grainulation.com",
|
|
47
|
+
"engines": {
|
|
48
|
+
"node": ">=18"
|
|
49
|
+
}
|
|
50
|
+
}
|