@grainulation/grainulation 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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 grainulation contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,108 @@
1
+ # grainulation
2
+
3
+ **Structured research for decisions that satisfice.**
4
+
5
+ ---
6
+
7
+ ## Slow down and trust the process
8
+
9
+ Most decisions fail not because the team lacked data, but because they lacked a process for turning data into evidence and evidence into conviction.
10
+
11
+ Grainulation is that process.
12
+
13
+ You start with a question. Not an answer, not a hypothesis -- a question. Then you grow evidence: claims with types, confidence levels, and evidence tiers. You challenge what you find. You look for blind spots. You corroborate with external sources. And only when the evidence compiles -- when conflicts are resolved and gaps are acknowledged -- do you write the brief.
14
+
15
+ The brief is not the goal. The brief is the receipt. The goal is the thinking that got you there.
16
+
17
+ ## The journey
18
+
19
+ ```mermaid
20
+ flowchart LR
21
+ Q["Question"] -->|"/init"| S["Seed Claims"]
22
+ S -->|"/research"| C["Grow Evidence"]
23
+ C -->|"/compile"| B["Compile Brief"]
24
+ C -->|"/challenge /blind-spot"| A["Adversarial Pressure"]
25
+ A -->|"/witness /feedback"| C
26
+ ```
27
+
28
+ Every step is tracked. Every claim has provenance. Every decision is reproducible.
29
+
30
+ ## The ecosystem
31
+
32
+ Eight tools. Each does one thing. Use what you need.
33
+
34
+ | Tool | What it does | Install |
35
+ |------|-------------|---------|
36
+ | **wheat** | Grows evidence. Research sprint engine. | `npx @grainulation/wheat init` |
37
+ | **farmer** | Permission dashboard. Approve AI actions in real time. | `npm i -g @grainulation/farmer` |
38
+ | **barn** | Shared tools. Claim schemas, templates, validators. | `npm i -g @grainulation/barn` |
39
+ | **mill** | Processes output. Export to PDF, slides, wiki. | `npm i -g @grainulation/mill` |
40
+ | **silo** | Stores knowledge. Reusable claim libraries. | `npm i -g @grainulation/silo` |
41
+ | **harvest** | Analytics. Cross-sprint learning and prediction scoring. | `npm i -g @grainulation/harvest` |
42
+ | **orchard** | Orchestration. Multi-sprint coordination. | `npm i -g @grainulation/orchard` |
43
+ | **grainulation** | The machine. Unified CLI and brand. | `npm i -g grainulation` |
44
+
45
+ **You don't need all eight.** Start with wheat. That's it. One command:
46
+
47
+ ```bash
48
+ npx @grainulation/wheat init
49
+ ```
50
+
51
+ Everything else is optional. Add tools when you feel the friction.
52
+
53
+ ## Quick start
54
+
55
+ ```bash
56
+ # Start a research sprint
57
+ npx @grainulation/wheat init
58
+
59
+ # Or install the unified CLI first
60
+ npm install -g @grainulation/grainulation
61
+
62
+ # See what's installed
63
+ grainulation doctor
64
+
65
+ # Interactive setup based on your role
66
+ grainulation setup
67
+
68
+ # Delegate to any tool
69
+ grainulation wheat init
70
+ grainulation farmer start
71
+ ```
72
+
73
+ ## The unified CLI
74
+
75
+ ```bash
76
+ grainulation # Ecosystem overview
77
+ grainulation doctor # Health check: which tools, which versions
78
+ grainulation setup # Install the right tools for your role
79
+ grainulation <tool> ... # Delegate to any grainulation tool
80
+ ```
81
+
82
+ The CLI is the wayfinder. It doesn't do the work -- it points you to the tool that does.
83
+
84
+ ## Philosophy
85
+
86
+ **Satisficing over maximizing.** You will never have perfect information. The goal is enough evidence to make a defensible decision, not a perfect one.
87
+
88
+ **Claims over opinions.** Every finding is a typed claim with an evidence tier. "I think" becomes "r003: factual, tested -- measured 340ms p95 latency under load."
89
+
90
+ **Adversarial pressure over consensus.** The `/challenge` command exists because comfortable agreement is the enemy of good decisions. If nobody is stress-testing the claims, the research isn't done.
91
+
92
+ **Process over heroics.** A reproducible sprint that anyone can pick up beats a brilliant analysis that lives in one person's head.
93
+
94
+ ## Zero dependencies
95
+
96
+ Every grainulation tool runs on Node built-ins only. No npm install waterfall. No left-pad. No supply chain anxiety. Just `node`, `fs`, `http`, and `crypto`.
97
+
98
+ ## The name
99
+
100
+ The name comes last.
101
+
102
+ You build the crop (wheat), the steward (farmer), the barn, the mill, the silo, the harvest, the orchard -- and only then do you name the machine that connects them all.
103
+
104
+ Grainulation: the machine that processes the grain.
105
+
106
+ ## License
107
+
108
+ MIT
@@ -0,0 +1,52 @@
1
+ #!/usr/bin/env node
2
+
3
+ 'use strict';
4
+
5
+ /**
6
+ * grainulation
7
+ *
8
+ * The unified entry point for the grainulation ecosystem.
9
+ * Routes to the right tool based on what you need.
10
+ *
11
+ * Usage:
12
+ * grainulation Show ecosystem overview
13
+ * grainulation doctor Check which tools are installed
14
+ * grainulation setup Interactive setup wizard
15
+ * grainulation serve Start the ecosystem control center
16
+ * grainulation <tool> [args] Delegate to a grainulation tool
17
+ */
18
+
19
+ const verbose = process.argv.includes('--verbose') || process.argv.includes('-v');
20
+ const jsonMode = process.argv.includes('--json');
21
+ function vlog(...a) {
22
+ if (!verbose) return;
23
+ const ts = new Date().toISOString();
24
+ process.stderr.write(`[${ts}] grainulation: ${a.join(' ')}\n`);
25
+ }
26
+
27
+ const command = process.argv[2];
28
+ vlog('startup', `command=${command || '(none)'}`, `cwd=${process.cwd()}`);
29
+
30
+ // Serve command — start the HTTP server (ESM module)
31
+ if (command === 'serve') {
32
+ const path = require('node:path');
33
+ const { spawn } = require('node:child_process');
34
+ const serverPath = path.join(__dirname, '..', 'lib', 'server.mjs');
35
+
36
+ // Forward remaining args to the server
37
+ const serverArgs = process.argv.slice(3);
38
+ const child = spawn(process.execPath, [serverPath, ...serverArgs], {
39
+ stdio: 'inherit',
40
+ });
41
+
42
+ child.on('close', (code) => process.exit(code ?? 0));
43
+ child.on('error', (err) => {
44
+ console.error(`grainulation: failed to start server: ${err.message}`);
45
+ process.exit(1);
46
+ });
47
+ } else {
48
+ const { route } = require('../lib/router');
49
+ // Strip --json from args before routing (it's handled as a mode flag)
50
+ const args = process.argv.slice(2).filter((a) => a !== '--json');
51
+ route(args, { json: jsonMode });
52
+ }
package/lib/doctor.js ADDED
@@ -0,0 +1,245 @@
1
+ 'use strict';
2
+
3
+ const { execSync } = require('node:child_process');
4
+ const { existsSync } = require('node:fs');
5
+ const path = require('node:path');
6
+ const { getInstallable } = require('./ecosystem');
7
+
8
+ /**
9
+ * Health check.
10
+ *
11
+ * Scans the system for installed grainulation tools using multiple
12
+ * detection methods: global npm, npx cache, local node_modules,
13
+ * source checkout, and npx --no-install availability.
14
+ */
15
+
16
+ /**
17
+ * Try to detect a package via global npm install.
18
+ * Returns { version, method } or null.
19
+ */
20
+ function checkGlobal(packageName) {
21
+ try {
22
+ const out = execSync(`npm list -g ${packageName} --depth=0 2>/dev/null`, {
23
+ stdio: 'pipe',
24
+ encoding: 'utf-8',
25
+ });
26
+ const match = out.match(new RegExp(`${escapeRegex(packageName)}@(\\S+)`));
27
+ return match ? { version: match[1], method: 'global' } : null;
28
+ } catch {
29
+ return null;
30
+ }
31
+ }
32
+
33
+ /**
34
+ * Check if the package exists in the npx cache (_npx directories).
35
+ * Returns { version, method } or null.
36
+ */
37
+ function checkNpxCache(packageName) {
38
+ try {
39
+ const prefix = execSync('npm config get cache', {
40
+ stdio: 'pipe',
41
+ encoding: 'utf-8',
42
+ }).trim();
43
+ const npxDir = path.join(prefix, '_npx');
44
+ if (!existsSync(npxDir)) return null;
45
+
46
+ // npx cache has hash-named directories, each with node_modules
47
+ const { readdirSync } = require('node:fs');
48
+ const entries = readdirSync(npxDir, { withFileTypes: true });
49
+ for (const entry of entries) {
50
+ if (!entry.isDirectory()) continue;
51
+ const pkgJson = path.join(npxDir, entry.name, 'node_modules', packageName, 'package.json');
52
+ if (existsSync(pkgJson)) {
53
+ try {
54
+ const pkg = JSON.parse(require('node:fs').readFileSync(pkgJson, 'utf-8'));
55
+ return { version: pkg.version || 'installed', method: 'npx cache' };
56
+ } catch {
57
+ return { version: 'installed', method: 'npx cache' };
58
+ }
59
+ }
60
+ }
61
+ return null;
62
+ } catch {
63
+ return null;
64
+ }
65
+ }
66
+
67
+ /**
68
+ * Check if the package exists in local node_modules (project-level install).
69
+ * Returns { version, method } or null.
70
+ */
71
+ function checkLocal(packageName) {
72
+ try {
73
+ const pkgJson = path.join(process.cwd(), 'node_modules', packageName, 'package.json');
74
+ if (existsSync(pkgJson)) {
75
+ const pkg = JSON.parse(require('node:fs').readFileSync(pkgJson, 'utf-8'));
76
+ return { version: pkg.version || 'installed', method: 'local' };
77
+ }
78
+ return null;
79
+ } catch {
80
+ return null;
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Check if the package is being run from a source/git checkout.
86
+ * Looks for package.json in common source locations.
87
+ * Returns { version, method } or null.
88
+ */
89
+ function checkSource(packageName) {
90
+ const candidates = [
91
+ // Sibling directory (monorepo or co-located checkouts)
92
+ path.join(process.cwd(), '..', packageName.replace(/^@[^/]+\//, '')),
93
+ path.join(process.cwd(), '..', packageName.replace(/^@[^/]+\//, ''), 'package.json'),
94
+ // Packages dir (monorepo)
95
+ path.join(process.cwd(), 'packages', packageName.replace(/^@[^/]+\//, '')),
96
+ ];
97
+ for (const candidate of candidates) {
98
+ const pkgJson = candidate.endsWith('package.json')
99
+ ? candidate
100
+ : path.join(candidate, 'package.json');
101
+ if (existsSync(pkgJson)) {
102
+ try {
103
+ const pkg = JSON.parse(require('node:fs').readFileSync(pkgJson, 'utf-8'));
104
+ if (pkg.name === packageName) {
105
+ return { version: pkg.version || 'installed', method: 'source' };
106
+ }
107
+ } catch {
108
+ // not a match, continue
109
+ }
110
+ }
111
+ }
112
+ return null;
113
+ }
114
+
115
+ /**
116
+ * Try `npx --no-install <package> --version` to check availability
117
+ * without downloading anything.
118
+ * Returns { version, method } or null.
119
+ */
120
+ function checkNpxNoInstall(packageName) {
121
+ try {
122
+ const out = execSync(`npx --no-install ${packageName} --version 2>/dev/null`, {
123
+ stdio: 'pipe',
124
+ encoding: 'utf-8',
125
+ timeout: 5000,
126
+ }).trim();
127
+ // Expect a version-like string
128
+ const match = out.match(/v?(\d+\.\d+\.\d+\S*)/);
129
+ return match ? { version: match[1], method: 'npx' } : null;
130
+ } catch {
131
+ return null;
132
+ }
133
+ }
134
+
135
+ function escapeRegex(str) {
136
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
137
+ }
138
+
139
+ /**
140
+ * Detect a package using all available methods, in priority order.
141
+ * Returns { version, method } or null.
142
+ */
143
+ function detect(packageName) {
144
+ return (
145
+ checkGlobal(packageName) ||
146
+ checkNpxCache(packageName) ||
147
+ checkLocal(packageName) ||
148
+ checkSource(packageName) ||
149
+ checkNpxNoInstall(packageName)
150
+ );
151
+ }
152
+
153
+ /**
154
+ * Legacy API: returns version string or null.
155
+ * Kept for backward compatibility with tests.
156
+ */
157
+ function getVersion(packageName) {
158
+ const result = detect(packageName);
159
+ return result ? result.version : null;
160
+ }
161
+
162
+ function getNodeVersion() {
163
+ return process.version;
164
+ }
165
+
166
+ function getNpmVersion() {
167
+ try {
168
+ return execSync('npm --version', { stdio: 'pipe', encoding: 'utf-8' }).trim();
169
+ } catch {
170
+ return 'not found';
171
+ }
172
+ }
173
+
174
+ function run(opts) {
175
+ const json = opts && opts.json;
176
+ const tools = getInstallable();
177
+
178
+ if (json) {
179
+ const toolResults = [];
180
+ for (const tool of tools) {
181
+ const result = detect(tool.package);
182
+ toolResults.push({
183
+ name: tool.name,
184
+ package: tool.package,
185
+ installed: !!result,
186
+ version: result ? result.version : null,
187
+ method: result ? result.method : null,
188
+ });
189
+ }
190
+ console.log(JSON.stringify({
191
+ environment: { node: getNodeVersion(), npm: getNpmVersion() },
192
+ tools: toolResults,
193
+ installed: toolResults.filter((t) => t.installed).length,
194
+ missing: toolResults.filter((t) => !t.installed).length,
195
+ }));
196
+ return;
197
+ }
198
+
199
+ let installed = 0;
200
+ let missing = 0;
201
+
202
+ console.log('');
203
+ console.log(' \x1b[1;33mgrainulation doctor\x1b[0m');
204
+ console.log(' Checking ecosystem health...');
205
+ console.log('');
206
+
207
+ // Environment
208
+ console.log(' \x1b[2mEnvironment:\x1b[0m');
209
+ console.log(` Node ${getNodeVersion()}`);
210
+ console.log(` npm v${getNpmVersion()}`);
211
+ console.log('');
212
+
213
+ // Tools
214
+ console.log(' \x1b[2mTools:\x1b[0m');
215
+ for (const tool of tools) {
216
+ const result = detect(tool.package);
217
+ if (result) {
218
+ installed++;
219
+ const ver = `v${result.version}`.padEnd(10);
220
+ console.log(
221
+ ` \x1b[32m\u2713\x1b[0m ${tool.name.padEnd(12)} ${ver} \x1b[2m(${result.method})\x1b[0m`
222
+ );
223
+ } else {
224
+ missing++;
225
+ console.log(` \x1b[2m\u2717 ${tool.name.padEnd(12)} -- (not found)\x1b[0m`);
226
+ }
227
+ }
228
+
229
+ console.log('');
230
+
231
+ // Summary
232
+ if (missing === tools.length) {
233
+ console.log(' \x1b[33mNo grainulation tools found.\x1b[0m');
234
+ console.log(' Start with: npx @grainulation/wheat init');
235
+ } else if (missing > 0) {
236
+ console.log(` \x1b[32m${installed} found\x1b[0m, \x1b[2m${missing} not found\x1b[0m`);
237
+ console.log(' Run \x1b[1mgrainulation setup\x1b[0m to install what you need.');
238
+ } else {
239
+ console.log(' \x1b[32mAll tools found. Full ecosystem ready.\x1b[0m');
240
+ }
241
+
242
+ console.log('');
243
+ }
244
+
245
+ module.exports = { run, getVersion, detect };
@@ -0,0 +1,122 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * The grainulation ecosystem registry.
5
+ *
6
+ * Eight tools. Each grows from the same soil:
7
+ * question -> evidence -> decision.
8
+ */
9
+
10
+ const TOOLS = [
11
+ {
12
+ name: 'wheat',
13
+ package: '@grainulation/wheat',
14
+ icon: 'W',
15
+ role: 'Grows evidence',
16
+ description: 'Research sprint engine. Ask a question, grow claims, compile a brief.',
17
+ category: 'core',
18
+ port: 9091,
19
+ serveCmd: ['serve'],
20
+ entryPoint: true,
21
+ },
22
+ {
23
+ name: 'farmer',
24
+ package: '@grainulation/farmer',
25
+ icon: 'F',
26
+ role: 'Permission dashboard',
27
+ description: 'Permission dashboard. Approve tool calls, review AI actions in real time.',
28
+ category: 'core',
29
+ port: 9090,
30
+ serveCmd: ['start'],
31
+ entryPoint: false,
32
+ },
33
+ {
34
+ name: 'barn',
35
+ package: '@grainulation/barn',
36
+ icon: 'B',
37
+ role: 'Shared tools',
38
+ description: 'Public utilities. Claim schemas, HTML templates, shared validators.',
39
+ category: 'foundation',
40
+ port: 9093,
41
+ serveCmd: ['serve'],
42
+ entryPoint: false,
43
+ },
44
+ {
45
+ name: 'mill',
46
+ package: '@grainulation/mill',
47
+ icon: 'M',
48
+ role: 'Processes output',
49
+ description: 'Export and publish. Turn compiled research into PDFs, slides, wikis.',
50
+ category: 'output',
51
+ port: 9094,
52
+ serveCmd: ['serve'],
53
+ entryPoint: false,
54
+ },
55
+ {
56
+ name: 'silo',
57
+ package: '@grainulation/silo',
58
+ icon: 'S',
59
+ role: 'Stores knowledge',
60
+ description: 'Reusable claim libraries. Share vetted claims across sprints and teams.',
61
+ category: 'storage',
62
+ port: 9095,
63
+ serveCmd: ['serve'],
64
+ entryPoint: false,
65
+ },
66
+ {
67
+ name: 'harvest',
68
+ package: '@grainulation/harvest',
69
+ icon: 'H',
70
+ role: 'Analytics & retrospectives',
71
+ description: 'Cross-sprint learning. Track prediction accuracy, find blind spots over time.',
72
+ category: 'analytics',
73
+ port: 9096,
74
+ serveCmd: ['serve'],
75
+ entryPoint: false,
76
+ },
77
+ {
78
+ name: 'orchard',
79
+ package: '@grainulation/orchard',
80
+ icon: 'O',
81
+ role: 'Orchestration',
82
+ description: 'Multi-sprint coordination. Run parallel research tracks, merge results.',
83
+ category: 'orchestration',
84
+ port: 9097,
85
+ serveCmd: ['serve'],
86
+ entryPoint: false,
87
+ },
88
+ {
89
+ name: 'grainulation',
90
+ package: '@grainulation/grainulation',
91
+ icon: 'G',
92
+ role: 'The machine',
93
+ description: 'Process manager and ecosystem hub. Start, stop, and monitor all tools.',
94
+ category: 'meta',
95
+ port: 9098,
96
+ serveCmd: ['serve'],
97
+ entryPoint: false,
98
+ },
99
+ ];
100
+
101
+ function getAll() {
102
+ return TOOLS;
103
+ }
104
+
105
+ function getByName(name) {
106
+ return TOOLS.find((t) => t.name === name);
107
+ }
108
+
109
+ function getInstallable() {
110
+ return TOOLS.filter((t) => t.name !== 'grainulation');
111
+ }
112
+
113
+ function getCategories() {
114
+ const cats = {};
115
+ for (const tool of TOOLS) {
116
+ if (!cats[tool.category]) cats[tool.category] = [];
117
+ cats[tool.category].push(tool);
118
+ }
119
+ return cats;
120
+ }
121
+
122
+ module.exports = { TOOLS, getAll, getByName, getInstallable, getCategories };