getprismo 0.1.8 → 0.1.10
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 +1 -1
- package/lib/prismo-dev/context-optimize.js +146 -12
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -394,7 +394,7 @@ prismo-dev-report.md full diagnostic report
|
|
|
394
394
|
|
|
395
395
|
if an existing `.claudeignore` or `.cursorignore` already covers prismo's recommendations, doctor skips the suggested ignore file instead of creating redundant noise. the default recommendations include common project state, local db, export, credential, and token patterns such as `*_state.json`, `*_tokens.json`, `*_export.json`, `*.sqlite`, `models/`, and `state-backups/`.
|
|
396
396
|
|
|
397
|
-
backend and frontend summaries include load-bearing candidates ranked by
|
|
397
|
+
backend and frontend summaries include load-bearing candidates ranked by import references, text-reference signals, recent git touches when available, and file size, not just directory listings.
|
|
398
398
|
|
|
399
399
|
what doctor never touches:
|
|
400
400
|
|
|
@@ -10,6 +10,7 @@ module.exports = function createContextOptimize(deps) {
|
|
|
10
10
|
color,
|
|
11
11
|
writeGeneratedFile,
|
|
12
12
|
} = deps;
|
|
13
|
+
const { execFileSync } = require("child_process");
|
|
13
14
|
|
|
14
15
|
function findRepoFiles(result, predicate, limit = 40) {
|
|
15
16
|
return result.files
|
|
@@ -59,6 +60,15 @@ function detectFrameworks(root, result) {
|
|
|
59
60
|
}
|
|
60
61
|
|
|
61
62
|
if (pyprojectFiles.length && !frameworks.has("Python")) frameworks.add("Python");
|
|
63
|
+
const pythonFiles = [...textFiles.values()].filter((file) => file.path.endsWith(".py") && !isNonSourcePath(file.path)).slice(0, 80);
|
|
64
|
+
for (const file of pythonFiles) {
|
|
65
|
+
const text = readIfText(path.join(root, file.path), 128 * 1024) || "";
|
|
66
|
+
if (/FastAPI\s*\(|from\s+fastapi\s+import|APIRouter\s*\(/.test(text)) frameworks.add("FastAPI");
|
|
67
|
+
if (/from\s+django|import\s+django|DJANGO_SETTINGS_MODULE/.test(text)) frameworks.add("Django");
|
|
68
|
+
if (/Flask\s*\(|from\s+flask\s+import/.test(text)) frameworks.add("Flask");
|
|
69
|
+
if (/sqlalchemy|create_engine|SessionLocal/i.test(text)) frameworks.add("SQLAlchemy");
|
|
70
|
+
}
|
|
71
|
+
if (pythonFiles.length) frameworks.add("Python");
|
|
62
72
|
if ([...textFiles.keys()].some((rel) => rel.endsWith("Cargo.toml"))) frameworks.add("Rust");
|
|
63
73
|
if ([...textFiles.keys()].some((rel) => rel.endsWith("go.mod"))) frameworks.add("Go");
|
|
64
74
|
if ([...textFiles.keys()].some((rel) => rel.endsWith("docker-compose.yml") || rel.endsWith("docker-compose.yaml"))) frameworks.add("Docker");
|
|
@@ -178,10 +188,14 @@ function createOptimizeContext(rootDir = process.cwd(), scope = null) {
|
|
|
178
188
|
const frameworks = detectFrameworks(root, scan);
|
|
179
189
|
const folders = topLevelDirectories(root);
|
|
180
190
|
const entrypoints = detectEntrypoints(scan);
|
|
181
|
-
const backendDetected = folders.includes("backend") || frameworks.some((name) => ["FastAPI", "Django", "Flask"].includes(name));
|
|
182
|
-
const frontendDetected = folders.includes("frontend") || frameworks.some((name) => ["Next.js", "React", "Vite"].includes(name));
|
|
183
191
|
const backend = detectBackendPaths(scan);
|
|
184
192
|
const frontend = detectFrontendPaths(scan);
|
|
193
|
+
const backendDetected = folders.includes("backend") ||
|
|
194
|
+
frameworks.some((name) => ["FastAPI", "Django", "Flask"].includes(name)) ||
|
|
195
|
+
Boolean(backend.api.length || backend.services.length || backend.db.length || backend.auth.length);
|
|
196
|
+
const frontendDetected = folders.includes("frontend") ||
|
|
197
|
+
frameworks.some((name) => ["Next.js", "React", "Vite"].includes(name)) ||
|
|
198
|
+
Boolean(frontend.app.length || frontend.components.length || frontend.apiClient.length || frontend.state.length);
|
|
185
199
|
const warnings = [];
|
|
186
200
|
if (scan.exposedLargeFiles.length) warnings.push(`${scan.exposedLargeFiles.length} exposed large file(s) may bloat AI context.`);
|
|
187
201
|
if (!scan.hasClaudeIgnore) warnings.push(".claudeignore is missing.");
|
|
@@ -205,6 +219,7 @@ function createOptimizeContext(rootDir = process.cwd(), scope = null) {
|
|
|
205
219
|
frontendDetected,
|
|
206
220
|
backend,
|
|
207
221
|
frontend,
|
|
222
|
+
gitActivity: getGitActivity(root),
|
|
208
223
|
warnings,
|
|
209
224
|
suggestions,
|
|
210
225
|
estimatedContextReduction: scan.avoidableWaste,
|
|
@@ -217,20 +232,139 @@ function mdList(items, empty = "None detected.") {
|
|
|
217
232
|
return items.map((item) => `- \`${item}\``).join("\n");
|
|
218
233
|
}
|
|
219
234
|
|
|
220
|
-
function
|
|
235
|
+
function moduleNameForPath(rel) {
|
|
236
|
+
return rel
|
|
237
|
+
.replace(/\.[^.]+$/, "")
|
|
238
|
+
.replace(/\/__init__$/, "")
|
|
239
|
+
.replace(/\/index$/, "")
|
|
240
|
+
.replace(/\//g, ".");
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function basenameStem(rel) {
|
|
244
|
+
return path.basename(rel).replace(/\.[^.]+$/, "");
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function extractPythonImports(text) {
|
|
248
|
+
const imports = new Set();
|
|
249
|
+
const importRe = /^\s*import\s+([a-zA-Z0-9_.,\s]+)/gm;
|
|
250
|
+
const fromRe = /^\s*from\s+([a-zA-Z0-9_.]+)\s+import\s+/gm;
|
|
251
|
+
let match;
|
|
252
|
+
while ((match = importRe.exec(text))) {
|
|
253
|
+
match[1].split(",").map((part) => part.trim().split(/\s+as\s+/)[0]).filter(Boolean).forEach((name) => imports.add(name));
|
|
254
|
+
}
|
|
255
|
+
while ((match = fromRe.exec(text))) imports.add(match[1]);
|
|
256
|
+
return imports;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function resolveJsImport(fromRel, specifier, sourcePaths) {
|
|
260
|
+
if (!specifier.startsWith(".")) return null;
|
|
261
|
+
const fromDir = path.posix.dirname(fromRel);
|
|
262
|
+
const base = path.posix.normalize(path.posix.join(fromDir, specifier));
|
|
263
|
+
const candidates = [
|
|
264
|
+
base,
|
|
265
|
+
`${base}.ts`,
|
|
266
|
+
`${base}.tsx`,
|
|
267
|
+
`${base}.js`,
|
|
268
|
+
`${base}.jsx`,
|
|
269
|
+
`${base}/index.ts`,
|
|
270
|
+
`${base}/index.tsx`,
|
|
271
|
+
`${base}/index.js`,
|
|
272
|
+
`${base}/index.jsx`,
|
|
273
|
+
];
|
|
274
|
+
return candidates.find((candidate) => sourcePaths.has(candidate)) || null;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function extractJsImports(fromRel, text, sourcePaths) {
|
|
278
|
+
const imports = new Set();
|
|
279
|
+
const importRe = /\bfrom\s+["']([^"']+)["']|import\s*\(\s*["']([^"']+)["']\s*\)|require\s*\(\s*["']([^"']+)["']\s*\)/g;
|
|
280
|
+
let match;
|
|
281
|
+
while ((match = importRe.exec(text))) {
|
|
282
|
+
const specifier = match[1] || match[2] || match[3];
|
|
283
|
+
const resolved = resolveJsImport(fromRel, specifier, sourcePaths);
|
|
284
|
+
if (resolved) imports.add(resolved);
|
|
285
|
+
}
|
|
286
|
+
return imports;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function getGitActivity(root) {
|
|
290
|
+
const activity = new Map();
|
|
291
|
+
try {
|
|
292
|
+
const output = execFileSync("git", ["-C", root, "log", "--since=180 days ago", "--name-only", "--format=%ct"], {
|
|
293
|
+
encoding: "utf8",
|
|
294
|
+
maxBuffer: 8 * 1024 * 1024,
|
|
295
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
296
|
+
});
|
|
297
|
+
let currentTs = 0;
|
|
298
|
+
for (const rawLine of output.split(/\r?\n/)) {
|
|
299
|
+
const line = rawLine.trim();
|
|
300
|
+
if (!line) continue;
|
|
301
|
+
if (/^\d{9,}$/.test(line)) {
|
|
302
|
+
currentTs = Number(line);
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
const rel = line.replace(/\\/g, "/");
|
|
306
|
+
const previous = activity.get(rel) || { touches: 0, lastTouched: 0 };
|
|
307
|
+
previous.touches += 1;
|
|
308
|
+
previous.lastTouched = Math.max(previous.lastTouched, currentTs);
|
|
309
|
+
activity.set(rel, previous);
|
|
310
|
+
}
|
|
311
|
+
} catch {
|
|
312
|
+
return activity;
|
|
313
|
+
}
|
|
314
|
+
return activity;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function formatGitActivity(activity) {
|
|
318
|
+
if (!activity || !activity.touches) return "no recent git touches";
|
|
319
|
+
const date = activity.lastTouched ? new Date(activity.lastTouched * 1000).toISOString().slice(0, 10) : "unknown date";
|
|
320
|
+
return `${activity.touches} recent git touch${activity.touches === 1 ? "" : "es"}, last ${date}`;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function topLoadBearing(root, files, allFiles, gitActivity, limit = 8) {
|
|
221
324
|
const textFiles = (allFiles || []).filter((file) => !file.ignored && file.kind !== "binary" && /\.(py|tsx?|jsx?)$/.test(file.path));
|
|
222
|
-
const
|
|
325
|
+
const sourcePaths = new Set(textFiles.map((file) => file.path));
|
|
326
|
+
const pythonModuleToPath = new Map();
|
|
327
|
+
for (const file of textFiles.filter((candidate) => candidate.path.endsWith(".py"))) {
|
|
328
|
+
pythonModuleToPath.set(moduleNameForPath(file.path), file.path);
|
|
329
|
+
pythonModuleToPath.set(basenameStem(file.path), file.path);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const importRefs = new Map();
|
|
333
|
+
const textRefs = new Map();
|
|
334
|
+
const corpus = textFiles.map((file) => {
|
|
335
|
+
const text = readIfText(path.join(root, file.path), 512 * 1024) || "";
|
|
336
|
+
if (file.path.endsWith(".py")) {
|
|
337
|
+
for (const imported of extractPythonImports(text)) {
|
|
338
|
+
const target = pythonModuleToPath.get(imported) || pythonModuleToPath.get(imported.split(".").pop());
|
|
339
|
+
if (target && target !== file.path) importRefs.set(target, (importRefs.get(target) || 0) + 1);
|
|
340
|
+
}
|
|
341
|
+
} else if (/\.(tsx?|jsx?)$/.test(file.path)) {
|
|
342
|
+
for (const target of extractJsImports(file.path, text, sourcePaths)) {
|
|
343
|
+
if (target !== file.path) importRefs.set(target, (importRefs.get(target) || 0) + 1);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
return `${file.path}\n${text}`;
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
for (const file of files || []) {
|
|
350
|
+
const base = basenameStem(file.path);
|
|
351
|
+
const importName = base.replace(/[-.]/g, "_");
|
|
352
|
+
const refs = corpus.reduce((sum, text) => sum + (text.includes(base) || text.includes(importName) ? 1 : 0), 0);
|
|
353
|
+
textRefs.set(file.path, refs);
|
|
354
|
+
}
|
|
355
|
+
|
|
223
356
|
return [...(files || [])]
|
|
224
357
|
.filter((file) => !file.ignored && file.kind !== "binary")
|
|
225
358
|
.map((file) => {
|
|
226
|
-
const
|
|
227
|
-
const
|
|
228
|
-
const
|
|
229
|
-
|
|
359
|
+
const imports = importRefs.get(file.path) || 0;
|
|
360
|
+
const refs = textRefs.get(file.path) || 0;
|
|
361
|
+
const git = gitActivity.get(file.path) || { touches: 0, lastTouched: 0 };
|
|
362
|
+
const score = imports * 1000 + refs * 25 + Math.min(git.touches, 20) * 10 + Math.log10(file.size + 1);
|
|
363
|
+
return { file, imports, refs, git, score };
|
|
230
364
|
})
|
|
231
|
-
.sort((a, b) => b.
|
|
365
|
+
.sort((a, b) => b.score - a.score || b.file.size - a.file.size)
|
|
232
366
|
.slice(0, limit)
|
|
233
|
-
.map(({ file, refs }) => `${file.path} (${refs} reference signal${refs === 1 ? "" : "s"}, ${formatBytes(file.size)})`);
|
|
367
|
+
.map(({ file, imports, refs, git }) => `${file.path} (${imports} import ref${imports === 1 ? "" : "s"}, ${refs} text reference signal${refs === 1 ? "" : "s"}, ${formatGitActivity(git)}, ${formatBytes(file.size)})`);
|
|
234
368
|
}
|
|
235
369
|
|
|
236
370
|
function inferGaps(ctx) {
|
|
@@ -342,7 +476,7 @@ function renderArchitectureSummary(ctx) {
|
|
|
342
476
|
}
|
|
343
477
|
|
|
344
478
|
function renderBackendSummary(ctx) {
|
|
345
|
-
const backendCandidates = topLoadBearing(ctx.root, ctx.scan.files.filter((file) => file.path.endsWith(".py") && !isNonSourcePath(file.path)), ctx.scan.files, 8);
|
|
479
|
+
const backendCandidates = topLoadBearing(ctx.root, ctx.scan.files.filter((file) => file.path.endsWith(".py") && !isNonSourcePath(file.path)), ctx.scan.files, ctx.gitActivity, 8);
|
|
346
480
|
return [
|
|
347
481
|
"# Backend Summary",
|
|
348
482
|
"",
|
|
@@ -386,7 +520,7 @@ function renderBackendSummary(ctx) {
|
|
|
386
520
|
}
|
|
387
521
|
|
|
388
522
|
function renderFrontendSummary(ctx) {
|
|
389
|
-
const frontendCandidates = topLoadBearing(ctx.root, ctx.scan.files.filter((file) => /\.(tsx?|jsx?)$/.test(file.path) && /(^|\/)(frontend|src|app|components|hooks)\//.test(file.path)), ctx.scan.files, 8);
|
|
523
|
+
const frontendCandidates = topLoadBearing(ctx.root, ctx.scan.files.filter((file) => /\.(tsx?|jsx?)$/.test(file.path) && /(^|\/)(frontend|src|app|components|hooks)\//.test(file.path)), ctx.scan.files, ctx.gitActivity, 8);
|
|
390
524
|
return [
|
|
391
525
|
"# Frontend Summary",
|
|
392
526
|
"",
|
package/package.json
CHANGED