ripp-cli 1.0.0 → 1.2.1
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/CHANGELOG.md +63 -0
- package/README.md +57 -0
- package/index.js +138 -8
- package/lib/build.js +116 -10
- package/lib/checklist-parser.js +224 -0
- package/lib/config.js +2 -3
- package/lib/confirmation.js +77 -6
- package/lib/doctor.js +370 -0
- package/lib/evidence.js +210 -7
- package/lib/metrics.js +410 -0
- package/lib/packager.js +26 -14
- package/package.json +25 -8
- package/schema/evidence-pack.schema.json +201 -0
- package/schema/intent-candidates.schema.json +109 -0
- package/schema/intent-confirmed.schema.json +85 -0
- package/schema/ripp-1.0.schema.json +543 -0
- package/schema/ripp-config.schema.json +104 -0
package/lib/doctor.js
ADDED
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RIPP Doctor - Health Check and Diagnostics
|
|
3
|
+
*
|
|
4
|
+
* Checks repository health and provides actionable fix-it commands
|
|
5
|
+
* for common RIPP setup issues.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const { execSync } = require('child_process');
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Run all health checks
|
|
14
|
+
* @param {string} cwd - Current working directory
|
|
15
|
+
* @returns {Object} Health check results
|
|
16
|
+
*/
|
|
17
|
+
function runHealthChecks(cwd = process.cwd()) {
|
|
18
|
+
const checks = {
|
|
19
|
+
nodeVersion: checkNodeVersion(),
|
|
20
|
+
gitRepository: checkGitRepository(cwd),
|
|
21
|
+
rippDirectory: checkRippDirectory(cwd),
|
|
22
|
+
configFile: checkConfigFile(cwd),
|
|
23
|
+
evidencePack: checkEvidencePack(cwd),
|
|
24
|
+
candidates: checkCandidates(cwd),
|
|
25
|
+
confirmedIntent: checkConfirmedIntent(cwd),
|
|
26
|
+
schema: checkSchema(),
|
|
27
|
+
cliVersion: checkCliVersion()
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// Calculate overall health
|
|
31
|
+
const total = Object.keys(checks).length;
|
|
32
|
+
const passed = Object.values(checks).filter(c => c.status === 'pass').length;
|
|
33
|
+
const warnings = Object.values(checks).filter(c => c.status === 'warning').length;
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
checks,
|
|
37
|
+
summary: {
|
|
38
|
+
total,
|
|
39
|
+
passed,
|
|
40
|
+
warnings,
|
|
41
|
+
failed: total - passed - warnings,
|
|
42
|
+
healthy: passed === total
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Check Node.js version (>= 20.0.0)
|
|
49
|
+
*/
|
|
50
|
+
function checkNodeVersion() {
|
|
51
|
+
const version = process.version;
|
|
52
|
+
const major = parseInt(version.slice(1).split('.')[0]);
|
|
53
|
+
|
|
54
|
+
if (major >= 20) {
|
|
55
|
+
return {
|
|
56
|
+
status: 'pass',
|
|
57
|
+
message: `Node.js ${version}`,
|
|
58
|
+
fix: null
|
|
59
|
+
};
|
|
60
|
+
} else {
|
|
61
|
+
return {
|
|
62
|
+
status: 'fail',
|
|
63
|
+
message: `Node.js ${version} is too old`,
|
|
64
|
+
fix: 'Install Node.js 20 or later: https://nodejs.org/',
|
|
65
|
+
docs: 'https://dylan-natter.github.io/ripp-protocol/getting-started.html#prerequisites'
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Check if current directory is a Git repository
|
|
72
|
+
*/
|
|
73
|
+
function checkGitRepository(cwd) {
|
|
74
|
+
try {
|
|
75
|
+
const gitDir = path.join(cwd, '.git');
|
|
76
|
+
if (fs.existsSync(gitDir)) {
|
|
77
|
+
return {
|
|
78
|
+
status: 'pass',
|
|
79
|
+
message: 'Git repository detected',
|
|
80
|
+
fix: null
|
|
81
|
+
};
|
|
82
|
+
} else {
|
|
83
|
+
return {
|
|
84
|
+
status: 'fail',
|
|
85
|
+
message: 'Not a Git repository',
|
|
86
|
+
fix: 'Initialize Git: git init',
|
|
87
|
+
docs: 'https://git-scm.com/docs/git-init'
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
} catch (error) {
|
|
91
|
+
return {
|
|
92
|
+
status: 'fail',
|
|
93
|
+
message: 'Unable to check Git repository',
|
|
94
|
+
fix: 'Ensure you are in a valid directory',
|
|
95
|
+
docs: null
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Check if .ripp directory exists
|
|
102
|
+
*/
|
|
103
|
+
function checkRippDirectory(cwd) {
|
|
104
|
+
const rippDir = path.join(cwd, '.ripp');
|
|
105
|
+
if (fs.existsSync(rippDir) && fs.statSync(rippDir).isDirectory()) {
|
|
106
|
+
return {
|
|
107
|
+
status: 'pass',
|
|
108
|
+
message: '.ripp directory exists',
|
|
109
|
+
fix: null
|
|
110
|
+
};
|
|
111
|
+
} else {
|
|
112
|
+
return {
|
|
113
|
+
status: 'fail',
|
|
114
|
+
message: '.ripp directory not found',
|
|
115
|
+
fix: 'Initialize RIPP: ripp init',
|
|
116
|
+
docs: 'https://dylan-natter.github.io/ripp-protocol/getting-started.html'
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Check if config.yaml exists
|
|
123
|
+
*/
|
|
124
|
+
function checkConfigFile(cwd) {
|
|
125
|
+
const configPath = path.join(cwd, '.ripp', 'config.yaml');
|
|
126
|
+
if (fs.existsSync(configPath)) {
|
|
127
|
+
return {
|
|
128
|
+
status: 'pass',
|
|
129
|
+
message: 'config.yaml present',
|
|
130
|
+
fix: null
|
|
131
|
+
};
|
|
132
|
+
} else {
|
|
133
|
+
return {
|
|
134
|
+
status: 'warning',
|
|
135
|
+
message: 'config.yaml not found (using defaults)',
|
|
136
|
+
fix: 'Initialize RIPP: ripp init',
|
|
137
|
+
docs: 'https://dylan-natter.github.io/ripp-protocol/getting-started.html'
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Check if evidence pack exists
|
|
144
|
+
*/
|
|
145
|
+
function checkEvidencePack(cwd) {
|
|
146
|
+
const evidenceIndex = path.join(cwd, '.ripp', 'evidence', 'index.yaml');
|
|
147
|
+
if (fs.existsSync(evidenceIndex)) {
|
|
148
|
+
return {
|
|
149
|
+
status: 'pass',
|
|
150
|
+
message: 'Evidence pack built',
|
|
151
|
+
fix: null
|
|
152
|
+
};
|
|
153
|
+
} else {
|
|
154
|
+
return {
|
|
155
|
+
status: 'warning',
|
|
156
|
+
message: 'Evidence pack not built',
|
|
157
|
+
fix: 'Build evidence: ripp evidence build',
|
|
158
|
+
docs: 'https://dylan-natter.github.io/ripp-protocol/getting-started.html#step-2-build-evidence'
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Check if candidates exist (discovery has run)
|
|
165
|
+
*/
|
|
166
|
+
function checkCandidates(cwd) {
|
|
167
|
+
const candidatesDir = path.join(cwd, '.ripp', 'candidates');
|
|
168
|
+
if (fs.existsSync(candidatesDir)) {
|
|
169
|
+
const files = fs.readdirSync(candidatesDir);
|
|
170
|
+
const yamlFiles = files.filter(f => f.endsWith('.yaml') || f.endsWith('.yml'));
|
|
171
|
+
|
|
172
|
+
if (yamlFiles.length > 0) {
|
|
173
|
+
return {
|
|
174
|
+
status: 'pass',
|
|
175
|
+
message: `${yamlFiles.length} candidate(s) found`,
|
|
176
|
+
fix: null
|
|
177
|
+
};
|
|
178
|
+
} else {
|
|
179
|
+
return {
|
|
180
|
+
status: 'warning',
|
|
181
|
+
message: 'No candidate files in candidates directory',
|
|
182
|
+
fix: 'Run discovery: ripp discover (requires AI enabled)',
|
|
183
|
+
docs: 'https://dylan-natter.github.io/ripp-protocol/getting-started.html#step-3-discover-intent'
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
} else {
|
|
187
|
+
return {
|
|
188
|
+
status: 'warning',
|
|
189
|
+
message: 'Discovery not run',
|
|
190
|
+
fix: 'Run discovery: ripp discover (requires AI enabled)',
|
|
191
|
+
docs: 'https://dylan-natter.github.io/ripp-protocol/getting-started.html#step-3-discover-intent'
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Check if confirmed intent exists
|
|
198
|
+
*/
|
|
199
|
+
function checkConfirmedIntent(cwd) {
|
|
200
|
+
const intentPath = path.join(cwd, '.ripp', 'intent.confirmed.yaml');
|
|
201
|
+
if (fs.existsSync(intentPath)) {
|
|
202
|
+
return {
|
|
203
|
+
status: 'pass',
|
|
204
|
+
message: 'Intent confirmed',
|
|
205
|
+
fix: null
|
|
206
|
+
};
|
|
207
|
+
} else {
|
|
208
|
+
return {
|
|
209
|
+
status: 'warning',
|
|
210
|
+
message: 'Intent not confirmed',
|
|
211
|
+
fix: 'Confirm intent: ripp confirm --checklist (then ripp build --from-checklist)',
|
|
212
|
+
docs: 'https://dylan-natter.github.io/ripp-protocol/getting-started.html#step-4-confirm-intent'
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Check if RIPP schema is accessible
|
|
219
|
+
*/
|
|
220
|
+
function checkSchema() {
|
|
221
|
+
try {
|
|
222
|
+
// First check bundled schema (always available when CLI is installed)
|
|
223
|
+
const bundledSchemaPath = path.join(__dirname, '../schema', 'ripp-1.0.schema.json');
|
|
224
|
+
if (fs.existsSync(bundledSchemaPath)) {
|
|
225
|
+
return {
|
|
226
|
+
status: 'pass',
|
|
227
|
+
message: 'RIPP schema accessible',
|
|
228
|
+
fix: null
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Fallback: check if we're in project root (for development)
|
|
233
|
+
const projectRoot = path.resolve(__dirname, '../../..');
|
|
234
|
+
const schemaPath = path.join(projectRoot, 'schema', 'ripp-1.0.schema.json');
|
|
235
|
+
if (fs.existsSync(schemaPath)) {
|
|
236
|
+
return {
|
|
237
|
+
status: 'pass',
|
|
238
|
+
message: 'RIPP schema accessible',
|
|
239
|
+
fix: null
|
|
240
|
+
};
|
|
241
|
+
} else {
|
|
242
|
+
return {
|
|
243
|
+
status: 'warning',
|
|
244
|
+
message: 'RIPP schema not found locally',
|
|
245
|
+
fix: 'Schema will be loaded from repository when needed',
|
|
246
|
+
docs: 'https://dylan-natter.github.io/ripp-protocol/schema/ripp-1.0.schema.json'
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
} catch (error) {
|
|
250
|
+
return {
|
|
251
|
+
status: 'warning',
|
|
252
|
+
message: 'Unable to check schema',
|
|
253
|
+
fix: null,
|
|
254
|
+
docs: null
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Check CLI version
|
|
261
|
+
*/
|
|
262
|
+
function checkCliVersion() {
|
|
263
|
+
try {
|
|
264
|
+
const pkg = require('../package.json');
|
|
265
|
+
return {
|
|
266
|
+
status: 'pass',
|
|
267
|
+
message: `ripp-cli v${pkg.version}`,
|
|
268
|
+
fix: null
|
|
269
|
+
};
|
|
270
|
+
} catch (error) {
|
|
271
|
+
return {
|
|
272
|
+
status: 'warning',
|
|
273
|
+
message: 'Unable to determine CLI version',
|
|
274
|
+
fix: null,
|
|
275
|
+
docs: null
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Format health check results as text
|
|
282
|
+
*/
|
|
283
|
+
function formatHealthCheckText(results) {
|
|
284
|
+
const { checks, summary } = results;
|
|
285
|
+
|
|
286
|
+
let output = '\n';
|
|
287
|
+
output += '🔍 RIPP Health Check\n';
|
|
288
|
+
output += '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n';
|
|
289
|
+
|
|
290
|
+
// Overall summary
|
|
291
|
+
if (summary.healthy) {
|
|
292
|
+
output += '✅ All checks passed!\n\n';
|
|
293
|
+
} else {
|
|
294
|
+
output += `📊 Summary: ${summary.passed}/${summary.total} checks passed`;
|
|
295
|
+
if (summary.warnings > 0) {
|
|
296
|
+
output += `, ${summary.warnings} warnings`;
|
|
297
|
+
}
|
|
298
|
+
if (summary.failed > 0) {
|
|
299
|
+
output += `, ${summary.failed} failed`;
|
|
300
|
+
}
|
|
301
|
+
output += '\n\n';
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Individual checks
|
|
305
|
+
for (const [name, check] of Object.entries(checks)) {
|
|
306
|
+
const icon = check.status === 'pass' ? '✓' : check.status === 'warning' ? '⚠' : '✗';
|
|
307
|
+
const statusColor = check.status === 'pass' ? '' : check.status === 'warning' ? '⚠ ' : '✗ ';
|
|
308
|
+
|
|
309
|
+
output += `${icon} ${formatCheckName(name)}: ${check.message}\n`;
|
|
310
|
+
|
|
311
|
+
if (check.fix) {
|
|
312
|
+
output += ` → Fix: ${check.fix}\n`;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (check.docs) {
|
|
316
|
+
output += ` → Docs: ${check.docs}\n`;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
output += '\n';
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Next steps
|
|
323
|
+
if (!summary.healthy) {
|
|
324
|
+
output += '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n';
|
|
325
|
+
output += '💡 Next Steps:\n\n';
|
|
326
|
+
|
|
327
|
+
const failedChecks = Object.entries(checks)
|
|
328
|
+
.filter(([_, check]) => check.status === 'fail')
|
|
329
|
+
.map(([_, check]) => check.fix)
|
|
330
|
+
.filter(fix => fix !== null);
|
|
331
|
+
|
|
332
|
+
if (failedChecks.length > 0) {
|
|
333
|
+
failedChecks.forEach((fix, idx) => {
|
|
334
|
+
output += ` ${idx + 1}. ${fix}\n`;
|
|
335
|
+
});
|
|
336
|
+
} else {
|
|
337
|
+
output += ' All critical checks passed. Address warnings to improve workflow.\n';
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
output += '\n';
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
output += '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n';
|
|
344
|
+
output += 'For more help: https://dylan-natter.github.io/ripp-protocol/getting-started.html\n';
|
|
345
|
+
|
|
346
|
+
return output;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Format check name for display
|
|
351
|
+
*/
|
|
352
|
+
function formatCheckName(name) {
|
|
353
|
+
const names = {
|
|
354
|
+
nodeVersion: 'Node.js Version',
|
|
355
|
+
gitRepository: 'Git Repository',
|
|
356
|
+
rippDirectory: 'RIPP Directory',
|
|
357
|
+
configFile: 'Configuration',
|
|
358
|
+
evidencePack: 'Evidence Pack',
|
|
359
|
+
candidates: 'Intent Candidates',
|
|
360
|
+
confirmedIntent: 'Confirmed Intent',
|
|
361
|
+
schema: 'RIPP Schema',
|
|
362
|
+
cliVersion: 'CLI Version'
|
|
363
|
+
};
|
|
364
|
+
return names[name] || name;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
module.exports = {
|
|
368
|
+
runHealthChecks,
|
|
369
|
+
formatHealthCheckText
|
|
370
|
+
};
|
package/lib/evidence.js
CHANGED
|
@@ -74,14 +74,15 @@ async function buildEvidencePack(cwd, config) {
|
|
|
74
74
|
*/
|
|
75
75
|
async function scanFiles(cwd, evidenceConfig) {
|
|
76
76
|
const files = [];
|
|
77
|
+
let excludedCount = 0;
|
|
77
78
|
const { includeGlobs, excludeGlobs, maxFileSize } = evidenceConfig;
|
|
78
79
|
|
|
79
|
-
//
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
for (const pattern of patterns) {
|
|
80
|
+
// Use glob with cwd option instead of joining paths
|
|
81
|
+
// This ensures patterns work correctly on all platforms
|
|
82
|
+
for (const pattern of includeGlobs) {
|
|
83
83
|
const matches = await glob(pattern, {
|
|
84
|
-
|
|
84
|
+
cwd: cwd,
|
|
85
|
+
ignore: excludeGlobs,
|
|
85
86
|
nodir: true,
|
|
86
87
|
absolute: true
|
|
87
88
|
});
|
|
@@ -92,6 +93,7 @@ async function scanFiles(cwd, evidenceConfig) {
|
|
|
92
93
|
|
|
93
94
|
// Skip files that are too large
|
|
94
95
|
if (stats.size > maxFileSize) {
|
|
96
|
+
excludedCount++;
|
|
95
97
|
continue;
|
|
96
98
|
}
|
|
97
99
|
|
|
@@ -109,12 +111,13 @@ async function scanFiles(cwd, evidenceConfig) {
|
|
|
109
111
|
});
|
|
110
112
|
} catch (error) {
|
|
111
113
|
// Skip files we can't read
|
|
114
|
+
excludedCount++;
|
|
112
115
|
continue;
|
|
113
116
|
}
|
|
114
117
|
}
|
|
115
118
|
}
|
|
116
119
|
|
|
117
|
-
return files;
|
|
120
|
+
return { files, excludedCount };
|
|
118
121
|
}
|
|
119
122
|
|
|
120
123
|
/**
|
|
@@ -151,7 +154,10 @@ async function extractEvidence(files, cwd) {
|
|
|
151
154
|
routes: [],
|
|
152
155
|
schemas: [],
|
|
153
156
|
auth: [],
|
|
154
|
-
workflows: []
|
|
157
|
+
workflows: [],
|
|
158
|
+
projectType: detectProjectType(files),
|
|
159
|
+
keyInsights: extractKeyInsights(files, cwd),
|
|
160
|
+
codeSnippets: extractKeyCodeSnippets(files)
|
|
155
161
|
};
|
|
156
162
|
|
|
157
163
|
for (const file of files) {
|
|
@@ -362,6 +368,203 @@ function redactSecrets(text) {
|
|
|
362
368
|
return redacted;
|
|
363
369
|
}
|
|
364
370
|
|
|
371
|
+
/**
|
|
372
|
+
* Detect project type from evidence
|
|
373
|
+
*/
|
|
374
|
+
function detectProjectType(files) {
|
|
375
|
+
const indicators = {
|
|
376
|
+
cli: 0,
|
|
377
|
+
webApp: 0,
|
|
378
|
+
api: 0,
|
|
379
|
+
library: 0,
|
|
380
|
+
protocol: 0
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
for (const file of files) {
|
|
384
|
+
const content = file.content ? file.content.toLowerCase() : '';
|
|
385
|
+
const path = file.path.toLowerCase();
|
|
386
|
+
|
|
387
|
+
// CLI tool indicators
|
|
388
|
+
if (path.includes('/bin/') || path.includes('cli') || path.includes('command'))
|
|
389
|
+
indicators.cli += 3;
|
|
390
|
+
if (
|
|
391
|
+
content.includes('commander') ||
|
|
392
|
+
content.includes('yargs') ||
|
|
393
|
+
content.includes('process.argv')
|
|
394
|
+
)
|
|
395
|
+
indicators.cli += 2;
|
|
396
|
+
if (path === 'package.json' && content.includes('"bin"')) indicators.cli += 4;
|
|
397
|
+
|
|
398
|
+
// Web app indicators
|
|
399
|
+
if (path.includes('app/') || path.includes('pages/') || path.includes('components/'))
|
|
400
|
+
indicators.webApp += 3;
|
|
401
|
+
if (content.includes('react') || content.includes('vue') || content.includes('angular'))
|
|
402
|
+
indicators.webApp += 2;
|
|
403
|
+
if (path.includes('index.html') || path.includes('app.tsx')) indicators.webApp += 3;
|
|
404
|
+
|
|
405
|
+
// API indicators
|
|
406
|
+
if (path.includes('api/') || path.includes('routes/') || path.includes('controllers/'))
|
|
407
|
+
indicators.api += 3;
|
|
408
|
+
if (content.includes('express') || content.includes('fastify') || content.includes('koa'))
|
|
409
|
+
indicators.api += 2;
|
|
410
|
+
if (content.includes('@app.route') || content.includes('@route') || content.includes('router.'))
|
|
411
|
+
indicators.api += 2;
|
|
412
|
+
|
|
413
|
+
// Library indicators
|
|
414
|
+
if (path === 'package.json' && !content.includes('"bin"') && !content.includes('"scripts"'))
|
|
415
|
+
indicators.library += 2;
|
|
416
|
+
if (path.includes('lib/') || path.includes('src/index')) indicators.library += 1;
|
|
417
|
+
|
|
418
|
+
// Protocol/spec indicators
|
|
419
|
+
if (path.includes('spec.md') || path.includes('protocol') || path.includes('rfc'))
|
|
420
|
+
indicators.protocol += 4;
|
|
421
|
+
if (path.includes('schema/') && path.includes('.json')) indicators.protocol += 2;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// Return type with highest score
|
|
425
|
+
const sorted = Object.entries(indicators).sort((a, b) => b[1] - a[1]);
|
|
426
|
+
return {
|
|
427
|
+
primary: sorted[0][0],
|
|
428
|
+
secondary: sorted[1][1] > 0 ? sorted[1][0] : null,
|
|
429
|
+
confidence: sorted[0][1] > 5 ? 'high' : sorted[0][1] > 2 ? 'medium' : 'low',
|
|
430
|
+
scores: indicators
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Extract key insights from README, package.json, and main files
|
|
436
|
+
*/
|
|
437
|
+
function extractKeyInsights(files, cwd) {
|
|
438
|
+
const insights = {
|
|
439
|
+
purpose: null,
|
|
440
|
+
description: null,
|
|
441
|
+
mainFeatures: [],
|
|
442
|
+
architecture: null
|
|
443
|
+
};
|
|
444
|
+
|
|
445
|
+
for (const file of files) {
|
|
446
|
+
const path = file.path.toLowerCase();
|
|
447
|
+
const content = file.content || '';
|
|
448
|
+
|
|
449
|
+
// Extract from README
|
|
450
|
+
if (path.includes('readme.md') || path.includes('readme.txt')) {
|
|
451
|
+
// Extract first paragraph as description
|
|
452
|
+
const lines = content.split('\n');
|
|
453
|
+
let desc = '';
|
|
454
|
+
for (const line of lines) {
|
|
455
|
+
if (line.trim() && !line.startsWith('#') && !line.startsWith('[')) {
|
|
456
|
+
desc += line.trim() + ' ';
|
|
457
|
+
if (desc.length > 200) break;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
if (desc) insights.description = desc.slice(0, 300);
|
|
461
|
+
|
|
462
|
+
// Extract features (look for bullet points or numbered lists)
|
|
463
|
+
const featureMatch = content.match(
|
|
464
|
+
/(?:features|capabilities|includes)[\s\S]{0,50}?\n((?:[-*]\s.+\n)+)/i
|
|
465
|
+
);
|
|
466
|
+
if (featureMatch) {
|
|
467
|
+
insights.mainFeatures = featureMatch[1]
|
|
468
|
+
.split('\n')
|
|
469
|
+
.map(f => f.replace(/^[-*]\s+/, '').trim())
|
|
470
|
+
.filter(f => f.length > 0)
|
|
471
|
+
.slice(0, 5);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// Extract from package.json
|
|
476
|
+
if (path === 'package.json') {
|
|
477
|
+
try {
|
|
478
|
+
const pkg = JSON.parse(content);
|
|
479
|
+
if (pkg.description && !insights.description) {
|
|
480
|
+
insights.description = pkg.description;
|
|
481
|
+
}
|
|
482
|
+
if (pkg.name && !insights.purpose) {
|
|
483
|
+
insights.purpose = `${pkg.name}: ${pkg.description || 'No description'}`;
|
|
484
|
+
}
|
|
485
|
+
} catch (e) {
|
|
486
|
+
// Ignore JSON parse errors
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// Extract from SPEC files
|
|
491
|
+
if (path.includes('spec.md') || path.includes('architecture.md')) {
|
|
492
|
+
const purposeMatch = content.match(/(?:purpose|goal|objective)[\s:]+([^\n]{50,300})/i);
|
|
493
|
+
if (purposeMatch && !insights.purpose) {
|
|
494
|
+
insights.purpose = purposeMatch[1].trim();
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
return insights;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* Extract key code snippets (functions, classes, exports)
|
|
504
|
+
*/
|
|
505
|
+
function extractKeyCodeSnippets(files) {
|
|
506
|
+
const snippets = [];
|
|
507
|
+
let count = 0;
|
|
508
|
+
const maxSnippets = 15;
|
|
509
|
+
|
|
510
|
+
for (const file of files) {
|
|
511
|
+
if (count >= maxSnippets) break;
|
|
512
|
+
if (!file.content || file.type !== 'source') continue;
|
|
513
|
+
|
|
514
|
+
const content = file.content;
|
|
515
|
+
const path = file.path;
|
|
516
|
+
|
|
517
|
+
// Extract function definitions (JavaScript/TypeScript)
|
|
518
|
+
const funcMatches = content.matchAll(
|
|
519
|
+
/(?:export\s+)?(?:async\s+)?function\s+(\w+)\s*\([^)]*\)\s*{([^}]{0,200})/g
|
|
520
|
+
);
|
|
521
|
+
for (const match of funcMatches) {
|
|
522
|
+
if (count >= maxSnippets) break;
|
|
523
|
+
snippets.push({
|
|
524
|
+
file: path,
|
|
525
|
+
type: 'function',
|
|
526
|
+
name: match[1],
|
|
527
|
+
snippet: match[0].substring(0, 150)
|
|
528
|
+
});
|
|
529
|
+
count++;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// Extract class definitions
|
|
533
|
+
const classMatches = content.matchAll(
|
|
534
|
+
/(?:export\s+)?class\s+(\w+)(?:\s+extends\s+\w+)?\s*{([^}]{0,150})/g
|
|
535
|
+
);
|
|
536
|
+
for (const match of classMatches) {
|
|
537
|
+
if (count >= maxSnippets) break;
|
|
538
|
+
snippets.push({
|
|
539
|
+
file: path,
|
|
540
|
+
type: 'class',
|
|
541
|
+
name: match[1],
|
|
542
|
+
snippet: match[0].substring(0, 150)
|
|
543
|
+
});
|
|
544
|
+
count++;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// Extract key comments (JSDoc, purpose statements)
|
|
548
|
+
const commentMatches = content.matchAll(/\/\*\*\s*\n\s*\*\s*([^\n]{30,200})/g);
|
|
549
|
+
for (const match of commentMatches) {
|
|
550
|
+
if (count >= maxSnippets) break;
|
|
551
|
+
if (
|
|
552
|
+
match[1].toLowerCase().includes('purpose') ||
|
|
553
|
+
match[1].toLowerCase().includes('description')
|
|
554
|
+
) {
|
|
555
|
+
snippets.push({
|
|
556
|
+
file: path,
|
|
557
|
+
type: 'comment',
|
|
558
|
+
snippet: match[1].trim()
|
|
559
|
+
});
|
|
560
|
+
count++;
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
return snippets.slice(0, maxSnippets);
|
|
566
|
+
}
|
|
567
|
+
|
|
365
568
|
module.exports = {
|
|
366
569
|
buildEvidencePack,
|
|
367
570
|
redactSecrets
|