@grainulation/wheat 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 +136 -0
- package/bin/wheat.js +193 -0
- package/compiler/detect-sprints.js +319 -0
- package/compiler/generate-manifest.js +280 -0
- package/compiler/wheat-compiler.js +1229 -0
- package/lib/compiler.js +35 -0
- package/lib/connect.js +418 -0
- package/lib/disconnect.js +188 -0
- package/lib/guard.js +151 -0
- package/lib/index.js +14 -0
- package/lib/init.js +457 -0
- package/lib/install-prompt.js +186 -0
- package/lib/quickstart.js +276 -0
- package/lib/serve-mcp.js +509 -0
- package/lib/server.js +391 -0
- package/lib/stats.js +184 -0
- package/lib/status.js +135 -0
- package/lib/update.js +71 -0
- package/package.json +53 -0
- package/public/index.html +1798 -0
- package/templates/claude.md +122 -0
- package/templates/commands/blind-spot.md +47 -0
- package/templates/commands/brief.md +73 -0
- package/templates/commands/calibrate.md +39 -0
- package/templates/commands/challenge.md +72 -0
- package/templates/commands/connect.md +104 -0
- package/templates/commands/evaluate.md +80 -0
- package/templates/commands/feedback.md +60 -0
- package/templates/commands/handoff.md +53 -0
- package/templates/commands/init.md +68 -0
- package/templates/commands/merge.md +51 -0
- package/templates/commands/present.md +52 -0
- package/templates/commands/prototype.md +68 -0
- package/templates/commands/replay.md +61 -0
- package/templates/commands/research.md +73 -0
- package/templates/commands/resolve.md +42 -0
- package/templates/commands/status.md +56 -0
- package/templates/commands/witness.md +79 -0
- package/templates/explainer.html +343 -0
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* wheat-manifest.json generator
|
|
4
|
+
*
|
|
5
|
+
* Reads claims.json, compilation.json, and scans the repo directory structure
|
|
6
|
+
* to produce a topic-map manifest. Zero npm dependencies.
|
|
7
|
+
*
|
|
8
|
+
* Usage: node generate-manifest.js [--out wheat-manifest.json] [--dir <path>]
|
|
9
|
+
*
|
|
10
|
+
* Based on research claims r011 (single machine-readable manifest) and
|
|
11
|
+
* r017 (topic map structure over file tree).
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import fs from 'fs';
|
|
15
|
+
import path from 'path';
|
|
16
|
+
import { execFileSync } from 'child_process';
|
|
17
|
+
import { fileURLToPath } from 'url';
|
|
18
|
+
import { detectSprints } from './detect-sprints.js';
|
|
19
|
+
|
|
20
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
21
|
+
const __dirname = path.dirname(__filename);
|
|
22
|
+
|
|
23
|
+
// ─── Target directory ────────────────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
const _dirIdx = process.argv.indexOf('--dir');
|
|
26
|
+
const ROOT = _dirIdx !== -1 && process.argv[_dirIdx + 1]
|
|
27
|
+
? path.resolve(process.argv[_dirIdx + 1])
|
|
28
|
+
: __dirname;
|
|
29
|
+
|
|
30
|
+
// --- CLI args ---
|
|
31
|
+
const args = process.argv.slice(2);
|
|
32
|
+
function arg(name, fallback) {
|
|
33
|
+
const i = args.indexOf(`--${name}`);
|
|
34
|
+
return i !== -1 && args[i + 1] ? args[i + 1] : fallback;
|
|
35
|
+
}
|
|
36
|
+
const OUT_PATH = path.join(ROOT, arg('out', 'wheat-manifest.json'));
|
|
37
|
+
|
|
38
|
+
// --- Helpers ---
|
|
39
|
+
|
|
40
|
+
/** Safely parse JSON from a file path; returns null on failure. */
|
|
41
|
+
export function loadJSON(filePath) {
|
|
42
|
+
try {
|
|
43
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
44
|
+
} catch {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Recursively list files under dir, returning paths relative to ROOT. */
|
|
50
|
+
export function walk(dir, filter) {
|
|
51
|
+
const results = [];
|
|
52
|
+
if (!fs.existsSync(dir)) return results;
|
|
53
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
54
|
+
const full = path.join(dir, entry.name);
|
|
55
|
+
if (entry.isDirectory()) {
|
|
56
|
+
// skip hidden dirs and node_modules
|
|
57
|
+
if (entry.name.startsWith('.') || entry.name === 'node_modules') continue;
|
|
58
|
+
results.push(...walk(full, filter));
|
|
59
|
+
} else {
|
|
60
|
+
const rel = path.relative(ROOT, full).split(path.sep).join('/');
|
|
61
|
+
if (!filter || filter(rel, entry.name)) results.push(rel);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return results;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Determine file type from its path. */
|
|
68
|
+
export function classifyFile(relPath) {
|
|
69
|
+
const normalized = relPath.split(path.sep).join('/');
|
|
70
|
+
if (normalized.startsWith('prototypes/')) return 'prototype';
|
|
71
|
+
if (normalized.startsWith('research/')) return 'research';
|
|
72
|
+
if (normalized.startsWith('output/')) return 'output';
|
|
73
|
+
if (normalized.startsWith('evidence/')) return 'evidence';
|
|
74
|
+
if (normalized.startsWith('templates/')) return 'template';
|
|
75
|
+
if (normalized.startsWith('examples/')) return 'example';
|
|
76
|
+
if (normalized.startsWith('test/')) return 'test';
|
|
77
|
+
if (normalized.startsWith('docs/')) return 'docs';
|
|
78
|
+
// root-level files
|
|
79
|
+
if (relPath.endsWith('.json')) return 'config';
|
|
80
|
+
if (relPath.endsWith('.js') || relPath.endsWith('.mjs')) return 'script';
|
|
81
|
+
if (relPath.endsWith('.md')) return 'docs';
|
|
82
|
+
return 'other';
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Compute highest evidence tier from a list of claims. */
|
|
86
|
+
export function highestEvidence(claims) {
|
|
87
|
+
const tiers = ['stated', 'web', 'documented', 'tested', 'production'];
|
|
88
|
+
let max = 0;
|
|
89
|
+
for (const c of claims) {
|
|
90
|
+
const idx = tiers.indexOf(c.evidence);
|
|
91
|
+
if (idx > max) max = idx;
|
|
92
|
+
}
|
|
93
|
+
return tiers[max];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Detect sprints using detect-sprints.js (git-based, no config pointer).
|
|
98
|
+
* Falls back to a minimal scan if detect-sprints.js is unavailable.
|
|
99
|
+
*/
|
|
100
|
+
function detectSprintsForManifest() {
|
|
101
|
+
// Try to use the exported function directly
|
|
102
|
+
try {
|
|
103
|
+
const parsed = detectSprints(ROOT);
|
|
104
|
+
const sprints = {};
|
|
105
|
+
for (const s of (parsed.sprints || [])) {
|
|
106
|
+
sprints[s.name] = {
|
|
107
|
+
question: s.question || '',
|
|
108
|
+
phase: s.phase || 'unknown',
|
|
109
|
+
claims_count: s.claims_count || 0,
|
|
110
|
+
active_claims: s.active_claims || 0,
|
|
111
|
+
path: s.path,
|
|
112
|
+
status: s.status,
|
|
113
|
+
last_git_activity: s.last_git_activity,
|
|
114
|
+
git_commit_count: s.git_commit_count,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
return sprints;
|
|
118
|
+
} catch {
|
|
119
|
+
// Fall through to naive fallback
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Fallback: minimal scan without git info
|
|
123
|
+
const sprints = {};
|
|
124
|
+
const currentClaims = loadJSON(path.join(ROOT, 'claims.json'));
|
|
125
|
+
if (currentClaims) {
|
|
126
|
+
sprints['current'] = {
|
|
127
|
+
question: currentClaims.meta?.question || '',
|
|
128
|
+
phase: currentClaims.meta?.phase || 'unknown',
|
|
129
|
+
claims_count: currentClaims.claims?.length || 0,
|
|
130
|
+
path: '.'
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
const examplesDir = path.join(ROOT, 'examples');
|
|
134
|
+
if (fs.existsSync(examplesDir)) {
|
|
135
|
+
for (const entry of fs.readdirSync(examplesDir, { withFileTypes: true })) {
|
|
136
|
+
if (!entry.isDirectory()) continue;
|
|
137
|
+
const sprintClaims = loadJSON(path.join(examplesDir, entry.name, 'claims.json'));
|
|
138
|
+
if (sprintClaims) {
|
|
139
|
+
sprints[entry.name] = {
|
|
140
|
+
question: sprintClaims.meta?.question || '',
|
|
141
|
+
phase: sprintClaims.meta?.phase || 'unknown',
|
|
142
|
+
claims_count: sprintClaims.claims?.length || 0,
|
|
143
|
+
path: path.join('examples', entry.name)
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return sprints;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// --- Main (only when run directly) ---
|
|
152
|
+
|
|
153
|
+
const isMain = process.argv[1] && fileURLToPath(import.meta.url) === path.resolve(process.argv[1]);
|
|
154
|
+
|
|
155
|
+
if (isMain) {
|
|
156
|
+
const t0 = performance.now();
|
|
157
|
+
|
|
158
|
+
const claims = loadJSON(path.join(ROOT, 'claims.json'));
|
|
159
|
+
const compilation = loadJSON(path.join(ROOT, 'compilation.json'));
|
|
160
|
+
|
|
161
|
+
if (!claims) {
|
|
162
|
+
console.error('Error: claims.json not found or invalid at', path.join(ROOT, 'claims.json'));
|
|
163
|
+
process.exit(1);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// 1. Build topic map from claims
|
|
167
|
+
const topicMap = {};
|
|
168
|
+
for (const claim of claims.claims) {
|
|
169
|
+
const topic = claim.topic;
|
|
170
|
+
if (!topicMap[topic]) {
|
|
171
|
+
topicMap[topic] = { claims: [], files: new Set(), sprint: 'current', evidence_level: 'stated' };
|
|
172
|
+
}
|
|
173
|
+
topicMap[topic].claims.push(claim.id);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Compute evidence levels per topic
|
|
177
|
+
for (const topic of Object.keys(topicMap)) {
|
|
178
|
+
const topicClaims = claims.claims.filter(c => c.topic === topic);
|
|
179
|
+
topicMap[topic].evidence_level = highestEvidence(topicClaims);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// 2. Scan current sprint directories for files
|
|
183
|
+
const scanDirs = ['research', 'prototypes', 'output', 'evidence', 'templates', 'test', 'docs'];
|
|
184
|
+
const allFiles = {};
|
|
185
|
+
|
|
186
|
+
for (const dir of scanDirs) {
|
|
187
|
+
const files = walk(path.join(ROOT, dir));
|
|
188
|
+
for (const f of files) {
|
|
189
|
+
const type = classifyFile(f);
|
|
190
|
+
allFiles[f] = { topics: [], type };
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Also include root-level scripts/configs
|
|
195
|
+
for (const entry of fs.readdirSync(ROOT)) {
|
|
196
|
+
if (entry.startsWith('.') || entry === 'node_modules') continue;
|
|
197
|
+
const full = path.join(ROOT, entry);
|
|
198
|
+
try {
|
|
199
|
+
if (fs.statSync(full).isFile()) {
|
|
200
|
+
const type = classifyFile(entry);
|
|
201
|
+
if (type !== 'other') {
|
|
202
|
+
allFiles[entry] = { topics: [], type };
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
} catch { /* skip */ }
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// 3. Map files to topics using claim source artifacts and keyword heuristics
|
|
209
|
+
const topicKeywords = {
|
|
210
|
+
'multi-session': ['session', 'server.mjs', 'hooks-config', 'dashboard.html', 'ws.mjs'],
|
|
211
|
+
'multi-sprint': ['sprint', 'examples/'],
|
|
212
|
+
'cartography': ['manifest', 'cartography', 'index'],
|
|
213
|
+
'performance': ['performance', 'evaluation'],
|
|
214
|
+
'compatibility': ['compat']
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
for (const [filePath, fileInfo] of Object.entries(allFiles)) {
|
|
218
|
+
const lower = filePath.toLowerCase();
|
|
219
|
+
|
|
220
|
+
// Heuristic: match file paths to topics via keywords
|
|
221
|
+
for (const [topic, keywords] of Object.entries(topicKeywords)) {
|
|
222
|
+
if (keywords.some(kw => lower.includes(kw))) {
|
|
223
|
+
if (!fileInfo.topics.includes(topic)) fileInfo.topics.push(topic);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Claims that reference files as artifacts
|
|
228
|
+
for (const claim of claims.claims) {
|
|
229
|
+
if (claim.source?.artifact && filePath.includes(claim.source.artifact.replace(/^.*[/\\]prototypes[/\\]/, 'prototypes/'))) {
|
|
230
|
+
if (!fileInfo.topics.includes(claim.topic)) {
|
|
231
|
+
fileInfo.topics.push(claim.topic);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Add files to topic map
|
|
237
|
+
for (const topic of fileInfo.topics) {
|
|
238
|
+
if (topicMap[topic]) {
|
|
239
|
+
topicMap[topic].files.add(filePath);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// 4. Convert Sets to arrays for JSON serialization
|
|
245
|
+
for (const topic of Object.keys(topicMap)) {
|
|
246
|
+
topicMap[topic].files = [...topicMap[topic].files].sort();
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// 5. Detect sprints
|
|
250
|
+
const sprints = detectSprintsForManifest();
|
|
251
|
+
|
|
252
|
+
// 6. Build final manifest
|
|
253
|
+
const topicFiles = {};
|
|
254
|
+
for (const [filePath, info] of Object.entries(allFiles)) {
|
|
255
|
+
if (info.topics.length > 0) {
|
|
256
|
+
topicFiles[filePath] = info;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const manifest = {
|
|
261
|
+
generated: new Date().toISOString(),
|
|
262
|
+
generator: 'generate-manifest.js',
|
|
263
|
+
claims_hash: compilation?.claims_hash || null,
|
|
264
|
+
topics: topicMap,
|
|
265
|
+
sprints,
|
|
266
|
+
files: topicFiles
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
fs.writeFileSync(OUT_PATH, JSON.stringify(manifest, null, 2) + '\n');
|
|
270
|
+
const elapsed = (performance.now() - t0).toFixed(1);
|
|
271
|
+
|
|
272
|
+
// Summary
|
|
273
|
+
const topicCount = Object.keys(topicMap).length;
|
|
274
|
+
const fileCount = Object.keys(topicFiles).length;
|
|
275
|
+
const sprintCount = Object.keys(sprints).length;
|
|
276
|
+
const sizeBytes = Buffer.byteLength(JSON.stringify(manifest, null, 2));
|
|
277
|
+
|
|
278
|
+
console.log(`wheat-manifest.json generated in ${elapsed}ms`);
|
|
279
|
+
console.log(` Topics: ${topicCount} | Files: ${fileCount} | Sprints: ${sprintCount} | Size: ${(sizeBytes / 1024).toFixed(1)}KB`);
|
|
280
|
+
}
|