@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/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
+ }