@grainulation/grainulation 1.0.0 → 1.1.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/README.md CHANGED
@@ -1,85 +1,71 @@
1
- # grainulation
1
+ <p align="center">
2
+ <img src="site/wordmark.svg" alt="grainulation" width="400">
3
+ </p>
2
4
 
3
- **Structured research for decisions that satisfice.**
5
+ <p align="center">
6
+ <a href="https://www.npmjs.com/package/@grainulation/grainulation"><img src="https://img.shields.io/npm/v/@grainulation/grainulation?label=%40grainulation%2Fgrainulation" alt="npm version"></a>
7
+ <a href="https://www.npmjs.com/package/@grainulation/grainulation"><img src="https://img.shields.io/npm/dm/@grainulation/grainulation" alt="npm downloads"></a>
8
+ <a href="https://github.com/grainulation/grainulation/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-MIT-green" alt="license"></a>
9
+ <a href="https://nodejs.org"><img src="https://img.shields.io/node/v/@grainulation/grainulation" alt="node"></a>
10
+ <a href="https://github.com/grainulation/grainulation/actions"><img src="https://github.com/grainulation/grainulation/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
11
+ <a href="https://deepwiki.com/grainulation/grainulation"><img src="https://deepwiki.com/badge.svg" alt="Explore on DeepWiki"></a>
12
+ </p>
4
13
 
5
- ---
14
+ <p align="center"><strong>Structured research for decisions that satisfice.</strong></p>
6
15
 
7
- ## Slow down and trust the process
16
+ Most decisions fail not because the team lacked data, but because they lacked a process for turning data into evidence and evidence into conviction. Grainulation is that process.
8
17
 
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.
18
+ You start with a question. You grow evidence: claims with types, confidence levels, and evidence tiers. You challenge what you find. You look for blind spots. And only when the evidence compiles -- when conflicts are resolved and gaps are acknowledged -- do you write the brief.
10
19
 
11
- Grainulation is that process.
20
+ ## Install
12
21
 
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
22
+ ```bash
23
+ npm install -g @grainulation/grainulation
26
24
  ```
27
25
 
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:
26
+ Or start a research sprint directly:
46
27
 
47
28
  ```bash
48
29
  npx @grainulation/wheat init
49
30
  ```
50
31
 
51
- Everything else is optional. Add tools when you feel the friction.
52
-
53
32
  ## Quick start
54
33
 
55
34
  ```bash
56
- # Start a research sprint
57
- npx @grainulation/wheat init
35
+ grainulation # Ecosystem overview
36
+ grainulation doctor # Health check: which tools, which versions
37
+ grainulation setup # Install the right tools for your role
38
+ grainulation wheat init # Delegate to any tool
39
+ grainulation farmer start
40
+ ```
58
41
 
59
- # Or install the unified CLI first
60
- npm install -g @grainulation/grainulation
42
+ ## The ecosystem
61
43
 
62
- # See what's installed
63
- grainulation doctor
44
+ Eight tools. Each does one thing. Use what you need.
64
45
 
65
- # Interactive setup based on your role
66
- grainulation setup
46
+ | Tool | What it does | Install |
47
+ | ------------------------------------------------------------ | ----------------------------------------------------------------------------- | ------------------------------------- |
48
+ | [wheat](https://github.com/grainulation/wheat) | Research engine. Grow structured evidence. | `npx @grainulation/wheat init` |
49
+ | [farmer](https://github.com/grainulation/farmer) | Permission dashboard. Approve AI actions in real time (admin + viewer roles). | `npm i -g @grainulation/farmer` |
50
+ | [barn](https://github.com/grainulation/barn) | Shared tools. Templates, validators, sprint detection. | `npm i -g @grainulation/barn` |
51
+ | [mill](https://github.com/grainulation/mill) | Format conversion. Export to PDF, CSV, slides, 24 formats. | `npm i -g @grainulation/mill` |
52
+ | [silo](https://github.com/grainulation/silo) | Knowledge storage. Reusable claim libraries and packs. | `npm i -g @grainulation/silo` |
53
+ | [harvest](https://github.com/grainulation/harvest) | Analytics. Cross-sprint patterns and prediction scoring. | `npm i -g @grainulation/harvest` |
54
+ | [orchard](https://github.com/grainulation/orchard) | Orchestration. Multi-sprint coordination and dependencies. | `npm i -g @grainulation/orchard` |
55
+ | [grainulation](https://github.com/grainulation/grainulation) | Unified CLI. Single entry point to the ecosystem. | `npm i -g @grainulation/grainulation` |
67
56
 
68
- # Delegate to any tool
69
- grainulation wheat init
70
- grainulation farmer start
71
- ```
57
+ **You don't need all eight.** Start with wheat. That's it. One command. Everything else is optional -- add tools when you feel the friction.
72
58
 
73
- ## The unified CLI
59
+ ## The journey
74
60
 
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
61
+ ```
62
+ Question --> Seed Claims --> Grow Evidence --> Compile Brief
63
+ /init /research /challenge /brief
64
+ /blind-spot
65
+ /witness
80
66
  ```
81
67
 
82
- The CLI is the wayfinder. It doesn't do the work -- it points you to the tool that does.
68
+ Every step is tracked. Every claim has provenance. Every decision is reproducible.
83
69
 
84
70
  ## Philosophy
85
71
 
@@ -87,18 +73,16 @@ The CLI is the wayfinder. It doesn't do the work -- it points you to the tool th
87
73
 
88
74
  **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
75
 
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.
76
+ **Adversarial pressure over consensus.** The `/challenge` command exists because comfortable agreement is the enemy of good decisions.
91
77
 
92
78
  **Process over heroics.** A reproducible sprint that anyone can pick up beats a brilliant analysis that lives in one person's head.
93
79
 
94
80
  ## Zero dependencies
95
81
 
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`.
82
+ Every grainulation tool runs on Node built-ins only. No npm install waterfall. No left-pad. No supply chain anxiety.
97
83
 
98
84
  ## The name
99
85
 
100
- The name comes last.
101
-
102
86
  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
87
 
104
88
  Grainulation: the machine that processes the grain.
@@ -1,7 +1,5 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- 'use strict';
4
-
5
3
  /**
6
4
  * grainulation
7
5
  *
package/lib/doctor.js CHANGED
@@ -1,5 +1,3 @@
1
- 'use strict';
2
-
3
1
  const { execSync } = require('node:child_process');
4
2
  const { existsSync } = require('node:fs');
5
3
  const path = require('node:path');
@@ -22,6 +20,7 @@ function checkGlobal(packageName) {
22
20
  const out = execSync(`npm list -g ${packageName} --depth=0 2>/dev/null`, {
23
21
  stdio: 'pipe',
24
22
  encoding: 'utf-8',
23
+ timeout: 5000,
25
24
  });
26
25
  const match = out.match(new RegExp(`${escapeRegex(packageName)}@(\\S+)`));
27
26
  return match ? { version: match[1], method: 'global' } : null;
@@ -39,6 +38,7 @@ function checkNpxCache(packageName) {
39
38
  const prefix = execSync('npm config get cache', {
40
39
  stdio: 'pipe',
41
40
  encoding: 'utf-8',
41
+ timeout: 5000,
42
42
  }).trim();
43
43
  const npxDir = path.join(prefix, '_npx');
44
44
  if (!existsSync(npxDir)) return null;
@@ -95,9 +95,7 @@ function checkSource(packageName) {
95
95
  path.join(process.cwd(), 'packages', packageName.replace(/^@[^/]+\//, '')),
96
96
  ];
97
97
  for (const candidate of candidates) {
98
- const pkgJson = candidate.endsWith('package.json')
99
- ? candidate
100
- : path.join(candidate, 'package.json');
98
+ const pkgJson = candidate.endsWith('package.json') ? candidate : path.join(candidate, 'package.json');
101
99
  if (existsSync(pkgJson)) {
102
100
  try {
103
101
  const pkg = JSON.parse(require('node:fs').readFileSync(pkgJson, 'utf-8'));
@@ -165,16 +163,62 @@ function getNodeVersion() {
165
163
 
166
164
  function getNpmVersion() {
167
165
  try {
168
- return execSync('npm --version', { stdio: 'pipe', encoding: 'utf-8' }).trim();
166
+ return execSync('npm --version', {
167
+ stdio: 'pipe',
168
+ encoding: 'utf-8',
169
+ timeout: 5000,
170
+ }).trim();
169
171
  } catch {
170
172
  return 'not found';
171
173
  }
172
174
  }
173
175
 
176
+ function getPnpmVersion() {
177
+ try {
178
+ return execSync('pnpm --version', {
179
+ stdio: 'pipe',
180
+ encoding: 'utf-8',
181
+ timeout: 5000,
182
+ }).trim();
183
+ } catch {
184
+ return null;
185
+ }
186
+ }
187
+
188
+ function getBiomeVersion() {
189
+ try {
190
+ const out = execSync('npx biome --version', {
191
+ stdio: 'pipe',
192
+ encoding: 'utf-8',
193
+ timeout: 5000,
194
+ }).trim();
195
+ const match = out.match(/(\d+\.\d+\.\d+\S*)/);
196
+ return match ? match[1] : out;
197
+ } catch {
198
+ return null;
199
+ }
200
+ }
201
+
202
+ function getHooksPath() {
203
+ try {
204
+ return execSync('git config core.hooksPath', {
205
+ stdio: 'pipe',
206
+ encoding: 'utf-8',
207
+ timeout: 5000,
208
+ }).trim();
209
+ } catch {
210
+ return null;
211
+ }
212
+ }
213
+
174
214
  function run(opts) {
175
- const json = opts && opts.json;
215
+ const json = opts?.json;
176
216
  const tools = getInstallable();
177
217
 
218
+ const pnpmVersion = getPnpmVersion();
219
+ const biomeVersion = getBiomeVersion();
220
+ const hooksPath = getHooksPath();
221
+
178
222
  if (json) {
179
223
  const toolResults = [];
180
224
  for (const tool of tools) {
@@ -187,12 +231,20 @@ function run(opts) {
187
231
  method: result ? result.method : null,
188
232
  });
189
233
  }
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
- }));
234
+ console.log(
235
+ JSON.stringify({
236
+ environment: {
237
+ node: getNodeVersion(),
238
+ npm: getNpmVersion(),
239
+ pnpm: pnpmVersion,
240
+ biome: biomeVersion,
241
+ hooksPath: hooksPath,
242
+ },
243
+ tools: toolResults,
244
+ installed: toolResults.filter((t) => t.installed).length,
245
+ missing: toolResults.filter((t) => !t.installed).length,
246
+ }),
247
+ );
196
248
  return;
197
249
  }
198
250
 
@@ -208,6 +260,25 @@ function run(opts) {
208
260
  console.log(' \x1b[2mEnvironment:\x1b[0m');
209
261
  console.log(` Node ${getNodeVersion()}`);
210
262
  console.log(` npm v${getNpmVersion()}`);
263
+ if (pnpmVersion) {
264
+ console.log(` pnpm v${pnpmVersion}`);
265
+ } else {
266
+ console.log(' pnpm \x1b[2mnot found\x1b[0m');
267
+ }
268
+ console.log('');
269
+
270
+ // DX tooling
271
+ console.log(' \x1b[2mDX tooling:\x1b[0m');
272
+ if (biomeVersion) {
273
+ console.log(` \x1b[32m\u2713\x1b[0m Biome v${biomeVersion}`);
274
+ } else {
275
+ console.log(' \x1b[2m\u2717 Biome not found (pnpm install to set up)\x1b[0m');
276
+ }
277
+ if (hooksPath) {
278
+ console.log(` \x1b[32m\u2713\x1b[0m Git hooks ${hooksPath}`);
279
+ } else {
280
+ console.log(' \x1b[2m\u2717 Git hooks not configured (run: git config core.hooksPath .githooks)\x1b[0m');
281
+ }
211
282
  console.log('');
212
283
 
213
284
  // Tools
@@ -217,9 +288,7 @@ function run(opts) {
217
288
  if (result) {
218
289
  installed++;
219
290
  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
- );
291
+ console.log(` \x1b[32m\u2713\x1b[0m ${tool.name.padEnd(12)} ${ver} \x1b[2m(${result.method})\x1b[0m`);
223
292
  } else {
224
293
  missing++;
225
294
  console.log(` \x1b[2m\u2717 ${tool.name.padEnd(12)} -- (not found)\x1b[0m`);
@@ -242,4 +311,11 @@ function run(opts) {
242
311
  console.log('');
243
312
  }
244
313
 
245
- module.exports = { run, getVersion, detect };
314
+ module.exports = {
315
+ run,
316
+ getVersion,
317
+ detect,
318
+ getPnpmVersion,
319
+ getBiomeVersion,
320
+ getHooksPath,
321
+ };
package/lib/ecosystem.js CHANGED
@@ -1,5 +1,3 @@
1
- 'use strict';
2
-
3
1
  /**
4
2
  * The grainulation ecosystem registry.
5
3
  *
package/lib/pm.js CHANGED
@@ -1,5 +1,3 @@
1
- 'use strict';
2
-
3
1
  /**
4
2
  * Process Manager — start, stop, and monitor grainulation tools.
5
3
  *
@@ -7,7 +5,7 @@
7
5
  * This module spawns/kills them and probes ports for health.
8
6
  */
9
7
 
10
- const { spawn, execSync } = require('node:child_process');
8
+ const { spawn, execFileSync } = require('node:child_process');
11
9
  const { existsSync, readFileSync, writeFileSync, mkdirSync } = require('node:fs');
12
10
  const { join } = require('node:path');
13
11
  const http = require('node:http');
@@ -19,7 +17,11 @@ const PID_DIR = join(PM_DIR, 'pids');
19
17
  const CONFIG_FILE = join(PM_DIR, 'config.json');
20
18
 
21
19
  function loadConfig() {
22
- try { return JSON.parse(readFileSync(CONFIG_FILE, 'utf8')); } catch { return {}; }
20
+ try {
21
+ return JSON.parse(readFileSync(CONFIG_FILE, 'utf8'));
22
+ } catch {
23
+ return {};
24
+ }
23
25
  }
24
26
 
25
27
  function ensureDirs() {
@@ -36,7 +38,7 @@ function readPid(toolName) {
36
38
  if (!existsSync(f)) return null;
37
39
  try {
38
40
  const pid = parseInt(readFileSync(f, 'utf8').trim(), 10);
39
- if (isNaN(pid)) return null;
41
+ if (Number.isNaN(pid)) return null;
40
42
  // Check if process is alive
41
43
  process.kill(pid, 0);
42
44
  return pid;
@@ -52,7 +54,9 @@ function writePid(toolName, pid) {
52
54
 
53
55
  function removePid(toolName) {
54
56
  const f = pidFile(toolName);
55
- try { require('node:fs').unlinkSync(f); } catch {}
57
+ try {
58
+ require('node:fs').unlinkSync(f);
59
+ } catch {}
56
60
  }
57
61
 
58
62
  /**
@@ -69,7 +73,10 @@ function probe(port, timeoutMs = 2000) {
69
73
  resolve({ alive: true, statusCode: res.statusCode, latencyMs });
70
74
  });
71
75
  req.on('error', () => resolve({ alive: false }));
72
- req.on('timeout', () => { req.destroy(); resolve({ alive: false }); });
76
+ req.on('timeout', () => {
77
+ req.destroy();
78
+ resolve({ alive: false });
79
+ });
73
80
  });
74
81
  }
75
82
 
@@ -78,10 +85,7 @@ function probe(port, timeoutMs = 2000) {
78
85
  */
79
86
  function findBin(tool) {
80
87
  const shortName = tool.package.replace(/^@[^/]+\//, '');
81
- const candidates = [
82
- join(__dirname, '..', '..', shortName),
83
- join(process.cwd(), '..', shortName),
84
- ];
88
+ const candidates = [join(__dirname, '..', '..', shortName), join(process.cwd(), '..', shortName)];
85
89
  for (const dir of candidates) {
86
90
  try {
87
91
  const pkgPath = join(dir, 'package.json');
@@ -97,7 +101,7 @@ function findBin(tool) {
97
101
  }
98
102
  } catch {}
99
103
  }
100
- return { cmd: 'npx', args: [tool.package], shell: true };
104
+ return { cmd: 'npx', args: [tool.package] };
101
105
  }
102
106
 
103
107
  /**
@@ -128,7 +132,7 @@ function startTool(toolName, extraArgs = []) {
128
132
  const child = spawn(bin.cmd, args, {
129
133
  stdio: 'ignore',
130
134
  detached: true,
131
- shell: bin.shell || false,
135
+ shell: false,
132
136
  });
133
137
 
134
138
  child.unref();
@@ -143,10 +147,18 @@ function startTool(toolName, extraArgs = []) {
143
147
  */
144
148
  function findPidByPort(port) {
145
149
  try {
146
- const out = execSync(`lsof -ti :${port}`, { timeout: 3000, stdio: ['ignore', 'pipe', 'pipe'] });
147
- const pids = out.toString().trim().split('\n').map(s => parseInt(s, 10)).filter(n => !isNaN(n) && n > 0);
150
+ const out = execFileSync('lsof', ['-ti', `:${port}`], {
151
+ timeout: 3000,
152
+ stdio: ['ignore', 'pipe', 'pipe'],
153
+ });
154
+ const pids = out
155
+ .toString()
156
+ .trim()
157
+ .split('\n')
158
+ .map((s) => parseInt(s, 10))
159
+ .filter((n) => !Number.isNaN(n) && n > 0);
148
160
  // Return the first PID that isn't our own process
149
- return pids.find(p => p !== process.pid) || null;
161
+ return pids.find((p) => p !== process.pid) || null;
150
162
  } catch {
151
163
  return null;
152
164
  }
@@ -181,19 +193,21 @@ function stopTool(toolName) {
181
193
  */
182
194
  async function ps() {
183
195
  const tools = getInstallable();
184
- const results = await Promise.all(tools.map(async (tool) => {
185
- const pid = readPid(tool.name);
186
- const health = await probe(tool.port);
187
- return {
188
- name: tool.name,
189
- port: tool.port,
190
- role: tool.role,
191
- pid: pid || null,
192
- alive: health.alive,
193
- latencyMs: health.latencyMs || null,
194
- statusCode: health.statusCode || null,
195
- };
196
- }));
196
+ const results = await Promise.all(
197
+ tools.map(async (tool) => {
198
+ const pid = readPid(tool.name);
199
+ const health = await probe(tool.port);
200
+ return {
201
+ name: tool.name,
202
+ port: tool.port,
203
+ role: tool.role,
204
+ pid: pid || null,
205
+ alive: health.alive,
206
+ latencyMs: health.latencyMs || null,
207
+ statusCode: health.statusCode || null,
208
+ };
209
+ }),
210
+ );
197
211
  return results;
198
212
  }
199
213
 
@@ -203,8 +217,12 @@ async function ps() {
203
217
  */
204
218
  function up(toolNames, extraArgs = []) {
205
219
  const defaults = ['farmer', 'wheat'];
206
- const names = (!toolNames || toolNames.length === 0) ? defaults :
207
- (toolNames[0] === 'all' ? getInstallable().map(t => t.name) : toolNames);
220
+ const names =
221
+ !toolNames || toolNames.length === 0
222
+ ? defaults
223
+ : toolNames[0] === 'all'
224
+ ? getInstallable().map((t) => t.name)
225
+ : toolNames;
208
226
 
209
227
  const results = [];
210
228
  for (const name of names) {
@@ -222,9 +240,7 @@ function up(toolNames, extraArgs = []) {
222
240
  * Stop multiple tools. Default: stop all running.
223
241
  */
224
242
  function down(toolNames) {
225
- const names = (!toolNames || toolNames.length === 0)
226
- ? getInstallable().map(t => t.name)
227
- : toolNames;
243
+ const names = !toolNames || toolNames.length === 0 ? getInstallable().map((t) => t.name) : toolNames;
228
244
 
229
245
  const results = [];
230
246
  for (const name of names) {