getprismo 0.1.9 → 0.1.11
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 +5 -2
- package/lib/prismo-dev/constants.js +12 -0
- package/lib/prismo-dev/context-optimize.js +159 -17
- package/lib/prismo-dev/firewall.js +8 -0
- package/lib/prismo-dev/report.js +19 -0
- package/lib/prismo-dev/scan.js +76 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -41,6 +41,7 @@ after you code npx getprismo cc timeline
|
|
|
41
41
|
- missing `.claudeignore` / `.cursorignore` (the biggest single fix for most repos)
|
|
42
42
|
- lockfiles entering context (`package-lock.json`, `yarn.lock`, `pnpm-lock.yaml`)
|
|
43
43
|
- generated artifacts leaking in (`__pycache__`, `dist/`, `coverage/`, `.next/`)
|
|
44
|
+
- operational source-stream dumps leaking in (`events/`, `source-streams/`, inbox/calendar/GitHub JSONL exports)
|
|
44
45
|
- oversized instruction files (`CLAUDE.md` or `AGENTS.md` over 500 tokens)
|
|
45
46
|
- tool output dominating sessions (repeated reads, large command output)
|
|
46
47
|
- long-running sessions with stale context accumulation
|
|
@@ -394,7 +395,9 @@ prismo-dev-report.md full diagnostic report
|
|
|
394
395
|
|
|
395
396
|
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
397
|
|
|
397
|
-
backend and frontend summaries include load-bearing candidates ranked by
|
|
398
|
+
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.
|
|
399
|
+
|
|
400
|
+
prismo also flags source-stream dumps separately from normal build artifacts. large inbox/calendar/github/event payload files are treated as operational noise because they often get summarized once, written near the repo, and then accidentally re-read by later coding sessions.
|
|
398
401
|
|
|
399
402
|
what doctor never touches:
|
|
400
403
|
|
|
@@ -656,7 +659,7 @@ then your team can run `npm run ai:doctor` without remembering the full command.
|
|
|
656
659
|
- openai codex
|
|
657
660
|
- cursor
|
|
658
661
|
- any tool that respects `.claudeignore` or `.cursorignore`
|
|
659
|
-
- any repo (node, python, go, rust, monorepos, whatever)
|
|
662
|
+
- any repo (node, python, go, rust, vue, svelte, astro, monorepos, whatever)
|
|
660
663
|
|
|
661
664
|
---
|
|
662
665
|
|
|
@@ -84,6 +84,10 @@ const SOURCE_EXTENSIONS = new Set([
|
|
|
84
84
|
".html",
|
|
85
85
|
".vue",
|
|
86
86
|
".svelte",
|
|
87
|
+
".mjs",
|
|
88
|
+
".cjs",
|
|
89
|
+
".mts",
|
|
90
|
+
".cts",
|
|
87
91
|
]);
|
|
88
92
|
|
|
89
93
|
const INSTRUCTION_FILES = [
|
|
@@ -120,8 +124,15 @@ const DEFAULT_CLAUDEIGNORE = [
|
|
|
120
124
|
"package-lock.json",
|
|
121
125
|
"yarn.lock",
|
|
122
126
|
"pnpm-lock.yaml",
|
|
127
|
+
"bun.lockb",
|
|
123
128
|
"test-results/",
|
|
124
129
|
"playwright-report/",
|
|
130
|
+
"events/",
|
|
131
|
+
"event-dumps/",
|
|
132
|
+
"session-dumps/",
|
|
133
|
+
"source-streams/",
|
|
134
|
+
"inbox-dumps/",
|
|
135
|
+
"calendar-dumps/",
|
|
125
136
|
"models/",
|
|
126
137
|
"state-backups/",
|
|
127
138
|
"backups/",
|
|
@@ -158,6 +169,7 @@ const GENERATED_ARTIFACT_PATTERNS = [
|
|
|
158
169
|
/(^|\/)coverage\//,
|
|
159
170
|
/(^|\/)playwright-report\//,
|
|
160
171
|
/(^|\/)test-results\//,
|
|
172
|
+
/(^|\/)(events|event-dumps|session-dumps|source-streams|inbox-dumps|calendar-dumps)\//,
|
|
161
173
|
/(^|\/)__pycache__\//,
|
|
162
174
|
/(^|\/)\.pytest_cache\//,
|
|
163
175
|
/(^|\/)\.cache\//,
|
|
@@ -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
|
|
@@ -25,6 +26,11 @@ function detectFrameworks(root, result) {
|
|
|
25
26
|
const deps = { ...(pkg && pkg.dependencies), ...(pkg && pkg.devDependencies) };
|
|
26
27
|
if (deps.next) frameworks.add("Next.js");
|
|
27
28
|
if (deps.react) frameworks.add("React");
|
|
29
|
+
if (deps.vue || deps["@vue/runtime-core"]) frameworks.add("Vue");
|
|
30
|
+
if (deps.svelte || deps["@sveltejs/kit"]) frameworks.add(deps["@sveltejs/kit"] ? "SvelteKit" : "Svelte");
|
|
31
|
+
if (deps["solid-js"]) frameworks.add("Solid");
|
|
32
|
+
if (deps.astro) frameworks.add("Astro");
|
|
33
|
+
if (deps.nuxt) frameworks.add("Nuxt");
|
|
28
34
|
if (deps.express) frameworks.add("Express");
|
|
29
35
|
if (deps["@nestjs/core"]) frameworks.add("NestJS");
|
|
30
36
|
if (deps.prisma || deps["@prisma/client"]) frameworks.add("Prisma");
|
|
@@ -74,6 +80,11 @@ function detectFrameworks(root, result) {
|
|
|
74
80
|
if ([...textFiles.keys()].some((rel) => rel.includes("prisma/schema.prisma"))) frameworks.add("Prisma");
|
|
75
81
|
if ([...textFiles.keys()].some((rel) => rel.includes("next.config."))) frameworks.add("Next.js");
|
|
76
82
|
if ([...textFiles.keys()].some((rel) => rel.includes("vite.config."))) frameworks.add("Vite");
|
|
83
|
+
if ([...textFiles.keys()].some((rel) => rel.includes("svelte.config."))) frameworks.add("SvelteKit");
|
|
84
|
+
if ([...textFiles.keys()].some((rel) => rel.includes("astro.config."))) frameworks.add("Astro");
|
|
85
|
+
if ([...textFiles.keys()].some((rel) => rel.includes("nuxt.config."))) frameworks.add("Nuxt");
|
|
86
|
+
if ([...textFiles.keys()].some((rel) => rel.endsWith(".vue"))) frameworks.add("Vue");
|
|
87
|
+
if ([...textFiles.keys()].some((rel) => rel.endsWith(".svelte"))) frameworks.add("Svelte");
|
|
77
88
|
if ([...textFiles.keys()].some((rel) => rel.includes("tailwind.config."))) frameworks.add("Tailwind");
|
|
78
89
|
if ([...textFiles.keys()].some((rel) => rel.endsWith("tsconfig.json"))) frameworks.add("TypeScript");
|
|
79
90
|
if ([...textFiles.keys()].some((rel) => rel.includes("alembic/") || rel.endsWith("alembic.ini"))) frameworks.add("Alembic");
|
|
@@ -108,6 +119,11 @@ function detectEntrypoints(result) {
|
|
|
108
119
|
"src/app/page.tsx",
|
|
109
120
|
"src/main.tsx",
|
|
110
121
|
"src/index.tsx",
|
|
122
|
+
"src/App.vue",
|
|
123
|
+
"src/App.svelte",
|
|
124
|
+
"src/routes/+page.svelte",
|
|
125
|
+
"src/pages/index.astro",
|
|
126
|
+
"app.vue",
|
|
111
127
|
"server.js",
|
|
112
128
|
"index.js",
|
|
113
129
|
"docker/docker-compose.yml",
|
|
@@ -172,12 +188,18 @@ function detectBackendPaths(result) {
|
|
|
172
188
|
}
|
|
173
189
|
|
|
174
190
|
function detectFrontendPaths(result) {
|
|
191
|
+
const appSurface = /(^|\/)(frontend\/)?src\/(app|pages|routes)\//;
|
|
192
|
+
const appFile = /(^|\/)(frontend\/)?src\/(App|main|index)\.(jsx?|tsx?|vue|svelte)$/;
|
|
193
|
+
const componentFile = /(^|\/)(frontend\/)?src\/(components|ui|widgets)\//;
|
|
194
|
+
const apiFile = /(^|\/)(frontend\/)?src\/(lib|hooks|composables|stores|services|api)\/.*(api|client|query|fetch|request|finops)/;
|
|
195
|
+
const stylingFile = /tailwind\.config|globals\.css|app\.css|\.module\.css|\.scss$|(^|\/)(frontend\/)?src\/styles?\//;
|
|
196
|
+
const stateFile = /providers?\.(tsx?|jsx?)|react-query|use[A-Z].*\.(ts|tsx|js|jsx)|(^|\/)(stores?|state|pinia|zustand|redux)\//;
|
|
175
197
|
return {
|
|
176
|
-
app: findRepoFiles(result, (rel) => !isNonSourcePath(rel) && (
|
|
177
|
-
components: findRepoFiles(result, (rel) => !isNonSourcePath(rel) && (
|
|
178
|
-
apiClient: findRepoFiles(result, (rel) => !isNonSourcePath(rel) && (
|
|
179
|
-
styling: findRepoFiles(result, (rel) => !isNonSourcePath(rel) &&
|
|
180
|
-
state: findRepoFiles(result, (rel) => !isNonSourcePath(rel) &&
|
|
198
|
+
app: findRepoFiles(result, (rel) => !isNonSourcePath(rel) && (appSurface.test(rel) || appFile.test(rel) || /(^|\/)app\.vue$/.test(rel) || /apps\/[^/]+\/(app|pages|routes|src\/app|src\/pages|src\/routes)\//.test(rel)), 24).map((f) => f.path),
|
|
199
|
+
components: findRepoFiles(result, (rel) => !isNonSourcePath(rel) && (componentFile.test(rel) || /apps\/[^/]+\/.*(components|ui|widgets)\//.test(rel) || /packages\/[^/]+\/.*(components|ui|widgets)\//.test(rel)), 24).map((f) => f.path),
|
|
200
|
+
apiClient: findRepoFiles(result, (rel) => !isNonSourcePath(rel) && (apiFile.test(rel) || /apps\/[^/]+\/.*(lib|hooks|composables|services|api)\/.*(api|client|query|fetch|request)/.test(rel)), 24).map((f) => f.path),
|
|
201
|
+
styling: findRepoFiles(result, (rel) => !isNonSourcePath(rel) && stylingFile.test(rel), 24).map((f) => f.path),
|
|
202
|
+
state: findRepoFiles(result, (rel) => !isNonSourcePath(rel) && stateFile.test(rel), 24).map((f) => f.path),
|
|
181
203
|
};
|
|
182
204
|
}
|
|
183
205
|
|
|
@@ -193,7 +215,7 @@ function createOptimizeContext(rootDir = process.cwd(), scope = null) {
|
|
|
193
215
|
frameworks.some((name) => ["FastAPI", "Django", "Flask"].includes(name)) ||
|
|
194
216
|
Boolean(backend.api.length || backend.services.length || backend.db.length || backend.auth.length);
|
|
195
217
|
const frontendDetected = folders.includes("frontend") ||
|
|
196
|
-
frameworks.some((name) => ["Next.js", "React", "Vite"].includes(name)) ||
|
|
218
|
+
frameworks.some((name) => ["Next.js", "React", "Vite", "Vue", "Svelte", "SvelteKit", "Solid", "Astro", "Nuxt"].includes(name)) ||
|
|
197
219
|
Boolean(frontend.app.length || frontend.components.length || frontend.apiClient.length || frontend.state.length);
|
|
198
220
|
const warnings = [];
|
|
199
221
|
if (scan.exposedLargeFiles.length) warnings.push(`${scan.exposedLargeFiles.length} exposed large file(s) may bloat AI context.`);
|
|
@@ -218,6 +240,7 @@ function createOptimizeContext(rootDir = process.cwd(), scope = null) {
|
|
|
218
240
|
frontendDetected,
|
|
219
241
|
backend,
|
|
220
242
|
frontend,
|
|
243
|
+
gitActivity: getGitActivity(root),
|
|
221
244
|
warnings,
|
|
222
245
|
suggestions,
|
|
223
246
|
estimatedContextReduction: scan.avoidableWaste,
|
|
@@ -230,20 +253,139 @@ function mdList(items, empty = "None detected.") {
|
|
|
230
253
|
return items.map((item) => `- \`${item}\``).join("\n");
|
|
231
254
|
}
|
|
232
255
|
|
|
233
|
-
function
|
|
234
|
-
|
|
235
|
-
|
|
256
|
+
function moduleNameForPath(rel) {
|
|
257
|
+
return rel
|
|
258
|
+
.replace(/\.[^.]+$/, "")
|
|
259
|
+
.replace(/\/__init__$/, "")
|
|
260
|
+
.replace(/\/index$/, "")
|
|
261
|
+
.replace(/\//g, ".");
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function basenameStem(rel) {
|
|
265
|
+
return path.basename(rel).replace(/\.[^.]+$/, "");
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function extractPythonImports(text) {
|
|
269
|
+
const imports = new Set();
|
|
270
|
+
const importRe = /^\s*import\s+([a-zA-Z0-9_.,\s]+)/gm;
|
|
271
|
+
const fromRe = /^\s*from\s+([a-zA-Z0-9_.]+)\s+import\s+/gm;
|
|
272
|
+
let match;
|
|
273
|
+
while ((match = importRe.exec(text))) {
|
|
274
|
+
match[1].split(",").map((part) => part.trim().split(/\s+as\s+/)[0]).filter(Boolean).forEach((name) => imports.add(name));
|
|
275
|
+
}
|
|
276
|
+
while ((match = fromRe.exec(text))) imports.add(match[1]);
|
|
277
|
+
return imports;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function resolveJsImport(fromRel, specifier, sourcePaths) {
|
|
281
|
+
if (!specifier.startsWith(".")) return null;
|
|
282
|
+
const fromDir = path.posix.dirname(fromRel);
|
|
283
|
+
const base = path.posix.normalize(path.posix.join(fromDir, specifier));
|
|
284
|
+
const candidates = [
|
|
285
|
+
base,
|
|
286
|
+
`${base}.ts`,
|
|
287
|
+
`${base}.tsx`,
|
|
288
|
+
`${base}.js`,
|
|
289
|
+
`${base}.jsx`,
|
|
290
|
+
`${base}/index.ts`,
|
|
291
|
+
`${base}/index.tsx`,
|
|
292
|
+
`${base}/index.js`,
|
|
293
|
+
`${base}/index.jsx`,
|
|
294
|
+
];
|
|
295
|
+
return candidates.find((candidate) => sourcePaths.has(candidate)) || null;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function extractJsImports(fromRel, text, sourcePaths) {
|
|
299
|
+
const imports = new Set();
|
|
300
|
+
const importRe = /\bfrom\s+["']([^"']+)["']|import\s*\(\s*["']([^"']+)["']\s*\)|require\s*\(\s*["']([^"']+)["']\s*\)/g;
|
|
301
|
+
let match;
|
|
302
|
+
while ((match = importRe.exec(text))) {
|
|
303
|
+
const specifier = match[1] || match[2] || match[3];
|
|
304
|
+
const resolved = resolveJsImport(fromRel, specifier, sourcePaths);
|
|
305
|
+
if (resolved) imports.add(resolved);
|
|
306
|
+
}
|
|
307
|
+
return imports;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function getGitActivity(root) {
|
|
311
|
+
const activity = new Map();
|
|
312
|
+
try {
|
|
313
|
+
const output = execFileSync("git", ["-C", root, "log", "--since=180 days ago", "--name-only", "--format=%ct"], {
|
|
314
|
+
encoding: "utf8",
|
|
315
|
+
maxBuffer: 8 * 1024 * 1024,
|
|
316
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
317
|
+
});
|
|
318
|
+
let currentTs = 0;
|
|
319
|
+
for (const rawLine of output.split(/\r?\n/)) {
|
|
320
|
+
const line = rawLine.trim();
|
|
321
|
+
if (!line) continue;
|
|
322
|
+
if (/^\d{9,}$/.test(line)) {
|
|
323
|
+
currentTs = Number(line);
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
const rel = line.replace(/\\/g, "/");
|
|
327
|
+
const previous = activity.get(rel) || { touches: 0, lastTouched: 0 };
|
|
328
|
+
previous.touches += 1;
|
|
329
|
+
previous.lastTouched = Math.max(previous.lastTouched, currentTs);
|
|
330
|
+
activity.set(rel, previous);
|
|
331
|
+
}
|
|
332
|
+
} catch {
|
|
333
|
+
return activity;
|
|
334
|
+
}
|
|
335
|
+
return activity;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function formatGitActivity(activity) {
|
|
339
|
+
if (!activity || !activity.touches) return "no recent git touches";
|
|
340
|
+
const date = activity.lastTouched ? new Date(activity.lastTouched * 1000).toISOString().slice(0, 10) : "unknown date";
|
|
341
|
+
return `${activity.touches} recent git touch${activity.touches === 1 ? "" : "es"}, last ${date}`;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function topLoadBearing(root, files, allFiles, gitActivity, limit = 8) {
|
|
345
|
+
const textFiles = (allFiles || []).filter((file) => !file.ignored && file.kind !== "binary" && /\.(py|tsx?|jsx?|mjs|cjs|vue|svelte)$/.test(file.path));
|
|
346
|
+
const sourcePaths = new Set(textFiles.map((file) => file.path));
|
|
347
|
+
const pythonModuleToPath = new Map();
|
|
348
|
+
for (const file of textFiles.filter((candidate) => candidate.path.endsWith(".py"))) {
|
|
349
|
+
pythonModuleToPath.set(moduleNameForPath(file.path), file.path);
|
|
350
|
+
pythonModuleToPath.set(basenameStem(file.path), file.path);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const importRefs = new Map();
|
|
354
|
+
const textRefs = new Map();
|
|
355
|
+
const corpus = textFiles.map((file) => {
|
|
356
|
+
const text = readIfText(path.join(root, file.path), 512 * 1024) || "";
|
|
357
|
+
if (file.path.endsWith(".py")) {
|
|
358
|
+
for (const imported of extractPythonImports(text)) {
|
|
359
|
+
const target = pythonModuleToPath.get(imported) || pythonModuleToPath.get(imported.split(".").pop());
|
|
360
|
+
if (target && target !== file.path) importRefs.set(target, (importRefs.get(target) || 0) + 1);
|
|
361
|
+
}
|
|
362
|
+
} else if (/\.(tsx?|jsx?|mjs|cjs|vue|svelte)$/.test(file.path)) {
|
|
363
|
+
for (const target of extractJsImports(file.path, text, sourcePaths)) {
|
|
364
|
+
if (target !== file.path) importRefs.set(target, (importRefs.get(target) || 0) + 1);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
return `${file.path}\n${text}`;
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
for (const file of files || []) {
|
|
371
|
+
const base = basenameStem(file.path);
|
|
372
|
+
const importName = base.replace(/[-.]/g, "_");
|
|
373
|
+
const refs = corpus.reduce((sum, text) => sum + (text.includes(base) || text.includes(importName) ? 1 : 0), 0);
|
|
374
|
+
textRefs.set(file.path, refs);
|
|
375
|
+
}
|
|
376
|
+
|
|
236
377
|
return [...(files || [])]
|
|
237
378
|
.filter((file) => !file.ignored && file.kind !== "binary")
|
|
238
379
|
.map((file) => {
|
|
239
|
-
const
|
|
240
|
-
const
|
|
241
|
-
const
|
|
242
|
-
|
|
380
|
+
const imports = importRefs.get(file.path) || 0;
|
|
381
|
+
const refs = textRefs.get(file.path) || 0;
|
|
382
|
+
const git = gitActivity.get(file.path) || { touches: 0, lastTouched: 0 };
|
|
383
|
+
const score = imports * 1000 + refs * 25 + Math.min(git.touches, 20) * 10 + Math.log10(file.size + 1);
|
|
384
|
+
return { file, imports, refs, git, score };
|
|
243
385
|
})
|
|
244
|
-
.sort((a, b) => b.
|
|
386
|
+
.sort((a, b) => b.score - a.score || b.file.size - a.file.size)
|
|
245
387
|
.slice(0, limit)
|
|
246
|
-
.map(({ file, refs }) => `${file.path} (${refs} reference signal${refs === 1 ? "" : "s"}, ${formatBytes(file.size)})`);
|
|
388
|
+
.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
389
|
}
|
|
248
390
|
|
|
249
391
|
function inferGaps(ctx) {
|
|
@@ -355,7 +497,7 @@ function renderArchitectureSummary(ctx) {
|
|
|
355
497
|
}
|
|
356
498
|
|
|
357
499
|
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);
|
|
500
|
+
const backendCandidates = topLoadBearing(ctx.root, ctx.scan.files.filter((file) => file.path.endsWith(".py") && !isNonSourcePath(file.path)), ctx.scan.files, ctx.gitActivity, 8);
|
|
359
501
|
return [
|
|
360
502
|
"# Backend Summary",
|
|
361
503
|
"",
|
|
@@ -399,7 +541,7 @@ function renderBackendSummary(ctx) {
|
|
|
399
541
|
}
|
|
400
542
|
|
|
401
543
|
function renderFrontendSummary(ctx) {
|
|
402
|
-
const frontendCandidates = topLoadBearing(ctx.root, ctx.scan.files.filter((file) => /\.(tsx?|jsx
|
|
544
|
+
const frontendCandidates = topLoadBearing(ctx.root, ctx.scan.files.filter((file) => /\.(tsx?|jsx?|mjs|cjs|vue|svelte)$/.test(file.path) && /(^|\/)(frontend|src|app|pages|routes|components|ui|hooks|composables|stores)\//.test(file.path)), ctx.scan.files, ctx.gitActivity, 8);
|
|
403
545
|
return [
|
|
404
546
|
"# Frontend Summary",
|
|
405
547
|
"",
|
|
@@ -78,7 +78,15 @@ function buildBlockedContext(ctx) {
|
|
|
78
78
|
"playwright-report/**",
|
|
79
79
|
"test-results/**",
|
|
80
80
|
"logs/**",
|
|
81
|
+
"events/**",
|
|
82
|
+
"event-dumps/**",
|
|
83
|
+
"session-dumps/**",
|
|
84
|
+
"source-streams/**",
|
|
85
|
+
"inbox-dumps/**",
|
|
86
|
+
"calendar-dumps/**",
|
|
81
87
|
"**/*.log",
|
|
88
|
+
"**/*.jsonl",
|
|
89
|
+
"**/*.ndjson",
|
|
82
90
|
"**/*.map",
|
|
83
91
|
"**/__pycache__/**",
|
|
84
92
|
"package-lock.json",
|
package/lib/prismo-dev/report.js
CHANGED
|
@@ -68,6 +68,13 @@ function renderTerminalReport(result, options = {}) {
|
|
|
68
68
|
if (result.toolOutputRisk.exposedNoisyDirectories.length) lines.push(`- Exposed noisy dirs: ${result.toolOutputRisk.exposedNoisyDirectories.slice(0, 6).join(", ")}`);
|
|
69
69
|
if (result.toolOutputRisk.exposedNoisyFiles.length) lines.push(`- Exposed noisy files: ${result.toolOutputRisk.exposedNoisyFiles.slice(0, 4).map((file) => file.path).join(", ")}`);
|
|
70
70
|
lines.push("");
|
|
71
|
+
if (result.operationalNoise && result.operationalNoise.level !== "Low") {
|
|
72
|
+
lines.push(color("Operational Noise Risk", "bold", useColor));
|
|
73
|
+
lines.push(`- Level: ${result.operationalNoise.level}`);
|
|
74
|
+
lines.push(`- ${result.operationalNoise.summary}`);
|
|
75
|
+
result.operationalNoise.files.slice(0, 4).forEach((file) => lines.push(`- ${file.path}: ${file.signals.join(", ")}`));
|
|
76
|
+
lines.push("");
|
|
77
|
+
}
|
|
71
78
|
lines.push(color("Prismo Proxy Tracking", "bold", useColor));
|
|
72
79
|
lines.push("- Exact API tracking: available when traffic uses the Prismo OpenAI/Anthropic base URL");
|
|
73
80
|
lines.push(`- Codex API/base-url mode: ${result.proxyTrackingReadiness.codingAgentBaseUrlMode.codex}`);
|
|
@@ -233,6 +240,18 @@ function renderMarkdownReport(result) {
|
|
|
233
240
|
});
|
|
234
241
|
}
|
|
235
242
|
lines.push("");
|
|
243
|
+
lines.push("## Operational Source-Stream Noise");
|
|
244
|
+
lines.push("");
|
|
245
|
+
if (result.operationalNoise && result.operationalNoise.files.length) {
|
|
246
|
+
lines.push(`- Level: ${result.operationalNoise.level}`);
|
|
247
|
+
lines.push(`- ${result.operationalNoise.summary}`);
|
|
248
|
+
result.operationalNoise.files.slice(0, 20).forEach((file) => {
|
|
249
|
+
lines.push(`- \`${file.path}\` - ${formatBytes(file.sizeBytes)} - ${file.signals.join(", ")} - ~${file.estimatedTokensIfRead.toLocaleString()} tokens if read`);
|
|
250
|
+
});
|
|
251
|
+
} else {
|
|
252
|
+
lines.push("- No obvious inbox/calendar/GitHub/source-stream dumps detected.");
|
|
253
|
+
}
|
|
254
|
+
lines.push("");
|
|
236
255
|
lines.push("## Prismo Proxy Tracking Readiness");
|
|
237
256
|
lines.push("");
|
|
238
257
|
lines.push("- Exact API tracking: available when app/tool traffic uses the Prismo OpenAI/Anthropic base URL.");
|
package/lib/prismo-dev/scan.js
CHANGED
|
@@ -364,6 +364,57 @@ function detectToolOutputRisk({ exposedLargeFiles, exposedHighRiskDirs, highRisk
|
|
|
364
364
|
};
|
|
365
365
|
}
|
|
366
366
|
|
|
367
|
+
function detectOperationalNoise(files) {
|
|
368
|
+
const candidates = files
|
|
369
|
+
.filter((file) => !file.ignored && file.size >= 16 * 1024 && file.kind !== "binary")
|
|
370
|
+
.filter((file) => /\.(json|jsonl|ndjson|log|md|txt)$/i.test(file.path) || /(events?|inbox|calendar|github|issues?|heartbeat|poll|source-stream|session-dump|activity|notifications?)/i.test(file.path))
|
|
371
|
+
.slice(0, 120);
|
|
372
|
+
const findings = [];
|
|
373
|
+
|
|
374
|
+
for (const file of candidates) {
|
|
375
|
+
const text = readIfText(file.fullPath, 512 * 1024) || "";
|
|
376
|
+
if (!text) continue;
|
|
377
|
+
const signals = [];
|
|
378
|
+
const timestampCount = (text.match(/\b20\d{2}-\d{2}-\d{2}[T ][0-2]\d:/g) || []).length;
|
|
379
|
+
const emailCount = (text.match(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/gi) || []).length;
|
|
380
|
+
const eventKeyCount = (text.match(/"(event|type|timestamp|created_at|updated_at|attendees|organizer|sender|subject|body|issue|pull_request|repository|payload)"\s*:/gi) || []).length;
|
|
381
|
+
const markdownEventCount = (text.match(/\b(calendar|inbox|email|github|issue|pull request|notification|attendee|heartbeat|poll(ed|ing)?)\b/gi) || []).length;
|
|
382
|
+
const repeatedObjectCount = (text.match(/^\s*\{.*\}\s*$/gm) || []).length;
|
|
383
|
+
|
|
384
|
+
if (timestampCount >= 12) signals.push(`${timestampCount} timestamps`);
|
|
385
|
+
if (emailCount >= 8) signals.push(`${emailCount} email-like strings`);
|
|
386
|
+
if (eventKeyCount >= 30) signals.push(`${eventKeyCount} event-shaped JSON keys`);
|
|
387
|
+
if (markdownEventCount >= 20) signals.push(`${markdownEventCount} operational keywords`);
|
|
388
|
+
if (repeatedObjectCount >= 15) signals.push(`${repeatedObjectCount} JSONL-style objects`);
|
|
389
|
+
|
|
390
|
+
if (signals.length >= 2 || eventKeyCount >= 60 || (timestampCount >= 20 && markdownEventCount >= 15)) {
|
|
391
|
+
findings.push({
|
|
392
|
+
path: file.path,
|
|
393
|
+
sizeBytes: file.size,
|
|
394
|
+
estimatedTokensIfRead: estimateTokens(file.size),
|
|
395
|
+
signals: signals.slice(0, 4),
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const estimatedExposureTokens = findings.reduce((sum, item) => sum + item.estimatedTokensIfRead, 0);
|
|
401
|
+
let level = "Low";
|
|
402
|
+
if (findings.length >= 3 || estimatedExposureTokens >= 150000) level = "High";
|
|
403
|
+
else if (findings.length || estimatedExposureTokens >= 25000) level = "Medium";
|
|
404
|
+
|
|
405
|
+
return {
|
|
406
|
+
level,
|
|
407
|
+
files: findings.slice(0, 12),
|
|
408
|
+
estimatedExposureTokens,
|
|
409
|
+
summary:
|
|
410
|
+
level === "High"
|
|
411
|
+
? "Operational source-stream dumps may be leaking inbox/calendar/GitHub-style noise back into coding context."
|
|
412
|
+
: level === "Medium"
|
|
413
|
+
? "Possible operational source-stream dumps detected; these can become second-order context leaks."
|
|
414
|
+
: "No obvious operational source-stream dumps detected.",
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
|
|
367
418
|
function buildProxyTrackingReadiness({ codexConfig, claudeConfig, realUsage }) {
|
|
368
419
|
return {
|
|
369
420
|
exactApiTracking: {
|
|
@@ -578,7 +629,7 @@ function addIssue(issues, severity, category, title, description, recommendation
|
|
|
578
629
|
});
|
|
579
630
|
}
|
|
580
631
|
|
|
581
|
-
function buildRecommendations({ hasClaudeIgnore, gitignorePatterns, exposedHighRiskDirs, largeFiles, instructionFiles, claudeConfig, toolOutputRisk, agentReadiness }) {
|
|
632
|
+
function buildRecommendations({ hasClaudeIgnore, gitignorePatterns, exposedHighRiskDirs, largeFiles, instructionFiles, claudeConfig, toolOutputRisk, operationalNoise, agentReadiness }) {
|
|
582
633
|
const recs = [];
|
|
583
634
|
if (!hasClaudeIgnore) {
|
|
584
635
|
recs.push("Create .claudeignore with generated/cache folders and large artifacts excluded.");
|
|
@@ -595,6 +646,9 @@ function buildRecommendations({ hasClaudeIgnore, gitignorePatterns, exposedHighR
|
|
|
595
646
|
if (toolOutputRisk && toolOutputRisk.level !== "Low") {
|
|
596
647
|
recs.push("Use command-output filtering or narrower shell commands for noisy tests, logs, diffs, and generated reports.");
|
|
597
648
|
}
|
|
649
|
+
if (operationalNoise && operationalNoise.level !== "Low") {
|
|
650
|
+
recs.push("Keep inbox/calendar/GitHub polling dumps out of coding-agent context; summarize them outside the repo or add them to AI ignore files.");
|
|
651
|
+
}
|
|
598
652
|
if (instructionFiles.some((file) => file.isClaude && file.tokens > 1500)) {
|
|
599
653
|
recs.push("Review CLAUDE.md for content that could move to linked docs; keep persistent instructions focused on durable rules.");
|
|
600
654
|
}
|
|
@@ -665,7 +719,7 @@ function calculateReductionPercent(beforeTokens, afterTokens) {
|
|
|
665
719
|
|
|
666
720
|
function chooseRecommendedScope(ctx) {
|
|
667
721
|
if (ctx.scope) return ctx.scope;
|
|
668
|
-
if (ctx.frameworks.some((name) => ["Next.js", "React", "Vite"].includes(name))) return "frontend";
|
|
722
|
+
if (ctx.frameworks.some((name) => ["Next.js", "React", "Vite", "Vue", "Svelte", "SvelteKit", "Solid", "Astro", "Nuxt"].includes(name))) return "frontend";
|
|
669
723
|
if (ctx.frameworks.some((name) => ["FastAPI", "Django", "Flask", "Python"].includes(name))) return "backend";
|
|
670
724
|
return null;
|
|
671
725
|
}
|
|
@@ -720,6 +774,7 @@ function toJsonPayload(result) {
|
|
|
720
774
|
agentReadiness: result.agentReadiness,
|
|
721
775
|
optimizationStack: result.optimizationStack,
|
|
722
776
|
toolOutputRisk: result.toolOutputRisk,
|
|
777
|
+
operationalNoise: result.operationalNoise,
|
|
723
778
|
proxyTrackingReadiness: result.proxyTrackingReadiness,
|
|
724
779
|
suggestedClaudeIgnore: result.recommendedClaudeIgnore,
|
|
725
780
|
suggestedCursorIgnore: result.recommendedCursorIgnore,
|
|
@@ -1010,6 +1065,7 @@ function scanRepo(rootDir = process.cwd(), options = {}) {
|
|
|
1010
1065
|
const optimizationStack = detectOptimizationStack(root, claudeConfig, codexConfig);
|
|
1011
1066
|
const agentReadiness = detectAgentReadiness(root, claudeConfig, codexConfig, realUsage);
|
|
1012
1067
|
const toolOutputRisk = detectToolOutputRisk({ exposedLargeFiles, exposedHighRiskDirs, highRiskDirs });
|
|
1068
|
+
const operationalNoise = detectOperationalNoise(files);
|
|
1013
1069
|
const proxyTrackingReadiness = buildProxyTrackingReadiness({ codexConfig, claudeConfig, realUsage });
|
|
1014
1070
|
|
|
1015
1071
|
if (toolOutputRisk.level !== "Low") {
|
|
@@ -1026,6 +1082,20 @@ function scanRepo(rootDir = process.cwd(), options = {}) {
|
|
|
1026
1082
|
);
|
|
1027
1083
|
}
|
|
1028
1084
|
|
|
1085
|
+
if (operationalNoise.level !== "Low") {
|
|
1086
|
+
addIssue(
|
|
1087
|
+
issues,
|
|
1088
|
+
operationalNoise.level === "High" ? "high" : "medium",
|
|
1089
|
+
"operational_noise",
|
|
1090
|
+
`${operationalNoise.files.length} source-stream dump${operationalNoise.files.length === 1 ? "" : "s"} may leak into context`,
|
|
1091
|
+
operationalNoise.files.slice(0, 5).map((file) => `${file.path} (${file.signals.join(", ")})`).join(", "),
|
|
1092
|
+
"Do not feed raw inbox/calendar/GitHub/event dumps back into coding sessions; summarize externally or add them to .claudeignore/.cursorignore.",
|
|
1093
|
+
operationalNoise.estimatedExposureTokens
|
|
1094
|
+
? `Likely avoidable token exposure: up to ~${operationalNoise.estimatedExposureTokens.toLocaleString()} tokens from operational noise files.`
|
|
1095
|
+
: "Potential savings estimate: prevents source-stream dumps from becoming recurring context."
|
|
1096
|
+
);
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1029
1099
|
if (optimizationStack.mcpServerTotal >= 8) {
|
|
1030
1100
|
addIssue(
|
|
1031
1101
|
issues,
|
|
@@ -1074,10 +1144,12 @@ function scanRepo(rootDir = process.cwd(), options = {}) {
|
|
|
1074
1144
|
const largeFileSuggestions = exposedLargeFiles
|
|
1075
1145
|
.filter((file) => file.size >= 1024 * 1024 || ["log", "json", "minified", "lock/generated"].includes(file.kind))
|
|
1076
1146
|
.map((file) => file.path);
|
|
1147
|
+
const operationalNoiseSuggestions = operationalNoise.files.map((file) => file.path);
|
|
1077
1148
|
const recommendedClaudeIgnore = Array.from(new Set([
|
|
1078
1149
|
...DEFAULT_CLAUDEIGNORE,
|
|
1079
1150
|
...gitignorePatterns.filter((line) => !line.startsWith("!")),
|
|
1080
1151
|
...largeFileSuggestions,
|
|
1152
|
+
...operationalNoiseSuggestions,
|
|
1081
1153
|
]));
|
|
1082
1154
|
const recommendedCursorIgnore = Array.from(new Set([
|
|
1083
1155
|
...recommendedClaudeIgnore,
|
|
@@ -1094,6 +1166,7 @@ function scanRepo(rootDir = process.cwd(), options = {}) {
|
|
|
1094
1166
|
instructionFiles,
|
|
1095
1167
|
claudeConfig,
|
|
1096
1168
|
toolOutputRisk,
|
|
1169
|
+
operationalNoise,
|
|
1097
1170
|
agentReadiness,
|
|
1098
1171
|
});
|
|
1099
1172
|
buildRealUsageRecommendations(realUsage).forEach((rec) => recommendations.push(rec));
|
|
@@ -1109,6 +1182,7 @@ function scanRepo(rootDir = process.cwd(), options = {}) {
|
|
|
1109
1182
|
agentReadiness,
|
|
1110
1183
|
optimizationStack,
|
|
1111
1184
|
toolOutputRisk,
|
|
1185
|
+
operationalNoise,
|
|
1112
1186
|
proxyTrackingReadiness,
|
|
1113
1187
|
frameworks,
|
|
1114
1188
|
files,
|
package/package.json
CHANGED