getprismo 0.1.9 → 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 +131 -10
- 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
|
|
@@ -218,6 +219,7 @@ function createOptimizeContext(rootDir = process.cwd(), scope = null) {
|
|
|
218
219
|
frontendDetected,
|
|
219
220
|
backend,
|
|
220
221
|
frontend,
|
|
222
|
+
gitActivity: getGitActivity(root),
|
|
221
223
|
warnings,
|
|
222
224
|
suggestions,
|
|
223
225
|
estimatedContextReduction: scan.avoidableWaste,
|
|
@@ -230,20 +232,139 @@ function mdList(items, empty = "None detected.") {
|
|
|
230
232
|
return items.map((item) => `- \`${item}\``).join("\n");
|
|
231
233
|
}
|
|
232
234
|
|
|
233
|
-
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) {
|
|
234
324
|
const textFiles = (allFiles || []).filter((file) => !file.ignored && file.kind !== "binary" && /\.(py|tsx?|jsx?)$/.test(file.path));
|
|
235
|
-
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
|
+
|
|
236
356
|
return [...(files || [])]
|
|
237
357
|
.filter((file) => !file.ignored && file.kind !== "binary")
|
|
238
358
|
.map((file) => {
|
|
239
|
-
const
|
|
240
|
-
const
|
|
241
|
-
const
|
|
242
|
-
|
|
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 };
|
|
243
364
|
})
|
|
244
|
-
.sort((a, b) => b.
|
|
365
|
+
.sort((a, b) => b.score - a.score || b.file.size - a.file.size)
|
|
245
366
|
.slice(0, limit)
|
|
246
|
-
.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)})`);
|
|
247
368
|
}
|
|
248
369
|
|
|
249
370
|
function inferGaps(ctx) {
|
|
@@ -355,7 +476,7 @@ function renderArchitectureSummary(ctx) {
|
|
|
355
476
|
}
|
|
356
477
|
|
|
357
478
|
function renderBackendSummary(ctx) {
|
|
358
|
-
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);
|
|
359
480
|
return [
|
|
360
481
|
"# Backend Summary",
|
|
361
482
|
"",
|
|
@@ -399,7 +520,7 @@ function renderBackendSummary(ctx) {
|
|
|
399
520
|
}
|
|
400
521
|
|
|
401
522
|
function renderFrontendSummary(ctx) {
|
|
402
|
-
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);
|
|
403
524
|
return [
|
|
404
525
|
"# Frontend Summary",
|
|
405
526
|
"",
|
package/package.json
CHANGED