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 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 lightweight reference signals plus file size, not just directory listings.
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 topLoadBearing(root, files, allFiles, limit = 8) {
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 corpus = textFiles.map((file) => `${file.path}\n${readIfText(path.join(root, file.path)) || ""}`);
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 base = path.basename(file.path).replace(/\.[^.]+$/, "");
240
- const importName = base.replace(/[-.]/g, "_");
241
- const refs = corpus.reduce((sum, text) => sum + (text.includes(base) || text.includes(importName) ? 1 : 0), 0);
242
- return { file, refs };
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.refs - a.refs || b.file.size - a.file.size)
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "getprismo",
3
- "version": "0.1.9",
3
+ "version": "0.1.10",
4
4
  "description": "Local AI coding workflow scanner for Codex, Claude Code, Cursor, and token-waste diagnostics.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/shanirsh/prismodev#readme",