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 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
@@ -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 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) {
221
324
  const textFiles = (allFiles || []).filter((file) => !file.ignored && file.kind !== "binary" && /\.(py|tsx?|jsx?)$/.test(file.path));
222
- 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
+
223
356
  return [...(files || [])]
224
357
  .filter((file) => !file.ignored && file.kind !== "binary")
225
358
  .map((file) => {
226
- const base = path.basename(file.path).replace(/\.[^.]+$/, "");
227
- const importName = base.replace(/[-.]/g, "_");
228
- const refs = corpus.reduce((sum, text) => sum + (text.includes(base) || text.includes(importName) ? 1 : 0), 0);
229
- 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 };
230
364
  })
231
- .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)
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "getprismo",
3
- "version": "0.1.8",
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",