getprismo 0.1.24 → 0.1.26
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 +14 -1
- package/docs/mcp.md +1 -0
- package/lib/prismo-dev/context-detect.js +342 -0
- package/lib/prismo-dev/context-optimize.js +11 -324
- package/lib/prismo-dev/doctor.js +12 -0
- package/lib/prismo-dev/mcp.js +24 -2
- package/lib/prismo-dev/report.js +24 -0
- package/lib/prismo-dev/scan-detect.js +306 -0
- package/lib/prismo-dev/scan-score.js +473 -0
- package/lib/prismo-dev/scan.js +29 -739
- package/lib/prismo-dev/usage-watch.js +23 -20
- package/lib/prismo-dev/watch-live.js +103 -0
- package/lib/prismo-dev/watch-render.js +65 -0
- package/lib/prismo-dev-scan.js +6 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -552,6 +552,16 @@ watch reads local session logs from codex and claude code. it detects:
|
|
|
552
552
|
|
|
553
553
|
watch tells you the single most useful action to take right now. usually: start a fresh session, or switch to a scoped context pack.
|
|
554
554
|
|
|
555
|
+
if you run multiple agents in the same repo, use:
|
|
556
|
+
|
|
557
|
+
```bash
|
|
558
|
+
npx getprismo watch --agents
|
|
559
|
+
```
|
|
560
|
+
|
|
561
|
+
multi-agent watch shows every visible local Codex/Claude Code session for the repo, ranks each agent by context pressure, and flags coordination risks like two agents repeatedly loading the same file, shared artifact leaks, multiple high-pressure sessions, or agents that should move noisy commands into `shield`.
|
|
562
|
+
|
|
563
|
+
the same multi-agent coordination signal is included in `usage --json`, `scan --usage --json`, doctor output, and the generated markdown report whenever multiple local sessions are visible for the repo.
|
|
564
|
+
|
|
555
565
|
`watch --rescue` prints a paste-ready prompt for the active coding session. use it when the agent is looping, reading too many files, or flooding context with logs:
|
|
556
566
|
|
|
557
567
|
```bash
|
|
@@ -644,6 +654,8 @@ npx getprismo doctor --json # machine-readable output
|
|
|
644
654
|
```bash
|
|
645
655
|
npx getprismo watch # live refresh
|
|
646
656
|
npx getprismo watch --once # single snapshot
|
|
657
|
+
npx getprismo watch --agents # multi-agent coordination view
|
|
658
|
+
npx getprismo watch --agents --json # machine-readable multi-agent state
|
|
647
659
|
npx getprismo watch --once --report # write .prismo/watch-report.md
|
|
648
660
|
npx getprismo watch --once --json # machine-readable
|
|
649
661
|
npx getprismo watch --auto # guardrails + throttle + 600k budget
|
|
@@ -681,6 +693,7 @@ npx getprismo mcp /path/to/repo
|
|
|
681
693
|
- `prismo_scan`
|
|
682
694
|
- `prismo_doctor_dry_run`
|
|
683
695
|
- `prismo_watch_snapshot`
|
|
696
|
+
- `prismo_multi_agent_watch`
|
|
684
697
|
- `prismo_shield_run`
|
|
685
698
|
- `prismo_shield_search`
|
|
686
699
|
- `prismo_shield_last`
|
|
@@ -688,7 +701,7 @@ npx getprismo mcp /path/to/repo
|
|
|
688
701
|
- `prismo_firewall`
|
|
689
702
|
- `prismo_cc_timeline`
|
|
690
703
|
|
|
691
|
-
This lets an MCP-compatible agent search prior shielded test/build output, request scoped context packs,
|
|
704
|
+
This lets an MCP-compatible agent search prior shielded test/build output, request scoped context packs, inspect token-waste signals, or coordinate multiple local agents without pasting giant logs into the conversation.
|
|
692
705
|
|
|
693
706
|
Generic MCP client config:
|
|
694
707
|
|
package/docs/mcp.md
CHANGED
|
@@ -49,6 +49,7 @@ For local development from this repo:
|
|
|
49
49
|
- `prismo_scan`: scan repo context/token waste
|
|
50
50
|
- `prismo_doctor_dry_run`: preview doctor payoff without writing files
|
|
51
51
|
- `prismo_watch_snapshot`: inspect live context pressure
|
|
52
|
+
- `prismo_multi_agent_watch`: inspect coordination risks across parallel local agents
|
|
52
53
|
- `prismo_shield_run`: run a noisy command and store full output locally
|
|
53
54
|
- `prismo_shield_search`: search stored shield output
|
|
54
55
|
- `prismo_shield_last`: list recent shielded command runs
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
module.exports = function createContextDetect(deps) {
|
|
2
|
+
const { fs, path, safeReadJson, readIfText, formatBytes } = deps;
|
|
3
|
+
const { execFileSync } = require("child_process");
|
|
4
|
+
|
|
5
|
+
function findRepoFiles(result, predicate, limit = 40) {
|
|
6
|
+
return result.files
|
|
7
|
+
? result.files.filter((file) => !file.ignored && file.kind !== "binary" && predicate(file.path, file)).slice(0, limit)
|
|
8
|
+
: [];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function detectFrameworks(root, result) {
|
|
12
|
+
const frameworks = new Set();
|
|
13
|
+
const packageFiles = findRepoFiles(result, (rel) => path.basename(rel) === "package.json" && !rel.includes("node_modules/"), 12);
|
|
14
|
+
for (const file of packageFiles) {
|
|
15
|
+
const pkg = safeReadJson(path.join(root, file.path));
|
|
16
|
+
const deps = { ...(pkg && pkg.dependencies), ...(pkg && pkg.devDependencies) };
|
|
17
|
+
if (deps.next) frameworks.add("Next.js");
|
|
18
|
+
if (deps.react) frameworks.add("React");
|
|
19
|
+
if (deps.vue || deps["@vue/runtime-core"]) frameworks.add("Vue");
|
|
20
|
+
if (deps.svelte || deps["@sveltejs/kit"]) frameworks.add(deps["@sveltejs/kit"] ? "SvelteKit" : "Svelte");
|
|
21
|
+
if (deps["solid-js"]) frameworks.add("Solid");
|
|
22
|
+
if (deps.astro) frameworks.add("Astro");
|
|
23
|
+
if (deps.nuxt) frameworks.add("Nuxt");
|
|
24
|
+
if (deps.express) frameworks.add("Express");
|
|
25
|
+
if (deps["@nestjs/core"]) frameworks.add("NestJS");
|
|
26
|
+
if (deps.prisma || deps["@prisma/client"]) frameworks.add("Prisma");
|
|
27
|
+
if (deps.tailwindcss) frameworks.add("Tailwind");
|
|
28
|
+
if (deps.typescript) frameworks.add("TypeScript");
|
|
29
|
+
if (pkg) frameworks.add("Node.js");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const textFiles = new Map(result.files.map((file) => [file.path, file]));
|
|
33
|
+
const requirements = [...textFiles.keys()].filter((rel) => rel.endsWith("requirements.txt"));
|
|
34
|
+
for (const rel of requirements) {
|
|
35
|
+
const text = readIfText(path.join(root, rel)) || "";
|
|
36
|
+
if (/fastapi/i.test(text)) frameworks.add("FastAPI");
|
|
37
|
+
if (/django/i.test(text)) frameworks.add("Django");
|
|
38
|
+
if (/flask/i.test(text)) frameworks.add("Flask");
|
|
39
|
+
if (/psycopg2|asyncpg|sqlalchemy/i.test(text)) frameworks.add("PostgreSQL");
|
|
40
|
+
if (/redis/i.test(text)) frameworks.add("Redis");
|
|
41
|
+
frameworks.add("Python");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const pyprojectFiles = [...textFiles.keys()].filter((rel) => rel.endsWith("pyproject.toml") && !rel.includes("node_modules/"));
|
|
45
|
+
for (const rel of pyprojectFiles) {
|
|
46
|
+
const text = readIfText(path.join(root, rel)) || "";
|
|
47
|
+
if (/fastapi/i.test(text)) frameworks.add("FastAPI");
|
|
48
|
+
if (/django/i.test(text)) frameworks.add("Django");
|
|
49
|
+
if (/flask/i.test(text)) frameworks.add("Flask");
|
|
50
|
+
if (/sqlalchemy/i.test(text)) frameworks.add("SQLAlchemy");
|
|
51
|
+
if (/psycopg|asyncpg/i.test(text)) frameworks.add("PostgreSQL");
|
|
52
|
+
if (/redis/i.test(text)) frameworks.add("Redis");
|
|
53
|
+
if (/celery/i.test(text)) frameworks.add("Celery");
|
|
54
|
+
frameworks.add("Python");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (pyprojectFiles.length && !frameworks.has("Python")) frameworks.add("Python");
|
|
58
|
+
const pythonFiles = [...textFiles.values()].filter((file) => file.path.endsWith(".py") && !isNonSourcePath(file.path)).slice(0, 80);
|
|
59
|
+
for (const file of pythonFiles) {
|
|
60
|
+
const text = readIfText(path.join(root, file.path), 128 * 1024) || "";
|
|
61
|
+
if (/FastAPI\s*\(|from\s+fastapi\s+import|APIRouter\s*\(/.test(text)) frameworks.add("FastAPI");
|
|
62
|
+
if (/from\s+django|import\s+django|DJANGO_SETTINGS_MODULE/.test(text)) frameworks.add("Django");
|
|
63
|
+
if (/Flask\s*\(|from\s+flask\s+import/.test(text)) frameworks.add("Flask");
|
|
64
|
+
if (/sqlalchemy|create_engine|SessionLocal/i.test(text)) frameworks.add("SQLAlchemy");
|
|
65
|
+
}
|
|
66
|
+
if (pythonFiles.length) frameworks.add("Python");
|
|
67
|
+
if ([...textFiles.keys()].some((rel) => rel.endsWith("Cargo.toml"))) frameworks.add("Rust");
|
|
68
|
+
if ([...textFiles.keys()].some((rel) => rel.endsWith("go.mod"))) frameworks.add("Go");
|
|
69
|
+
if ([...textFiles.keys()].some((rel) => rel.endsWith("docker-compose.yml") || rel.endsWith("docker-compose.yaml"))) frameworks.add("Docker");
|
|
70
|
+
if ([...textFiles.keys()].some((rel) => rel.includes("prisma/schema.prisma"))) frameworks.add("Prisma");
|
|
71
|
+
if ([...textFiles.keys()].some((rel) => rel.includes("next.config."))) frameworks.add("Next.js");
|
|
72
|
+
if ([...textFiles.keys()].some((rel) => rel.includes("vite.config."))) frameworks.add("Vite");
|
|
73
|
+
if ([...textFiles.keys()].some((rel) => rel.includes("svelte.config."))) frameworks.add("SvelteKit");
|
|
74
|
+
if ([...textFiles.keys()].some((rel) => rel.includes("astro.config."))) frameworks.add("Astro");
|
|
75
|
+
if ([...textFiles.keys()].some((rel) => rel.includes("nuxt.config."))) frameworks.add("Nuxt");
|
|
76
|
+
if ([...textFiles.keys()].some((rel) => rel.endsWith(".vue"))) frameworks.add("Vue");
|
|
77
|
+
if ([...textFiles.keys()].some((rel) => rel.endsWith(".svelte"))) frameworks.add("Svelte");
|
|
78
|
+
if ([...textFiles.keys()].some((rel) => rel.includes("tailwind.config."))) frameworks.add("Tailwind");
|
|
79
|
+
if ([...textFiles.keys()].some((rel) => rel.endsWith("tsconfig.json"))) frameworks.add("TypeScript");
|
|
80
|
+
if ([...textFiles.keys()].some((rel) => rel.includes("alembic/") || rel.endsWith("alembic.ini"))) frameworks.add("Alembic");
|
|
81
|
+
if ([...textFiles.keys()].some((rel) => /postgres|postgresql/i.test(rel))) frameworks.add("PostgreSQL");
|
|
82
|
+
return Array.from(frameworks).sort();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function topLevelDirectories(root) {
|
|
86
|
+
try {
|
|
87
|
+
return fs.readdirSync(root, { withFileTypes: true })
|
|
88
|
+
.filter((entry) => entry.isDirectory() && !entry.name.startsWith(".") && entry.name !== "node_modules")
|
|
89
|
+
.map((entry) => entry.name)
|
|
90
|
+
.sort();
|
|
91
|
+
} catch {
|
|
92
|
+
return [];
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function hasPath(result, matcher) {
|
|
97
|
+
return result.files.some((file) => matcher(file.path));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function detectEntrypoints(result) {
|
|
101
|
+
const candidates = [
|
|
102
|
+
"backend/app/main.py",
|
|
103
|
+
"backend/main.py",
|
|
104
|
+
"app/main.py",
|
|
105
|
+
"main.py",
|
|
106
|
+
"manage.py",
|
|
107
|
+
"frontend/src/app/page.tsx",
|
|
108
|
+
"frontend/src/app/layout.tsx",
|
|
109
|
+
"src/app/page.tsx",
|
|
110
|
+
"src/main.tsx",
|
|
111
|
+
"src/index.tsx",
|
|
112
|
+
"src/App.vue",
|
|
113
|
+
"src/App.svelte",
|
|
114
|
+
"src/routes/+page.svelte",
|
|
115
|
+
"src/pages/index.astro",
|
|
116
|
+
"app.vue",
|
|
117
|
+
"server.js",
|
|
118
|
+
"index.js",
|
|
119
|
+
"docker/docker-compose.yml",
|
|
120
|
+
"docker-compose.yml",
|
|
121
|
+
];
|
|
122
|
+
const paths = new Set(result.files.map((file) => file.path));
|
|
123
|
+
const found = candidates.filter((candidate) => paths.has(candidate));
|
|
124
|
+
if (!found.length) {
|
|
125
|
+
const pyMain = result.files.find((f) => /^[^/]+\/__main__\.py$/.test(f.path));
|
|
126
|
+
if (pyMain) found.push(pyMain.path);
|
|
127
|
+
const pyInit = result.files.find((f) => /^[^/]+\/__init__\.py$/.test(f.path) && !f.path.includes("test"));
|
|
128
|
+
if (pyInit && !found.length) found.push(pyInit.path);
|
|
129
|
+
}
|
|
130
|
+
return found;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function isNonSourcePath(rel) {
|
|
134
|
+
if (/\.(test|spec|e2e)\.[jt]sx?$/.test(rel)) return true;
|
|
135
|
+
return /^(docs|docs_src|examples|samples|tutorials|tests|test|spec|__tests__|fixtures)\//i.test(rel) ||
|
|
136
|
+
/\/(docs|docs_src|examples|samples|tutorials|tests|test|__tests__|fixtures)\//.test(rel);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function detectBackendPaths(result) {
|
|
140
|
+
const pythonText = (file) => readIfText(path.join(result.root, file.path), 256 * 1024) || "";
|
|
141
|
+
const isRootPython = (rel) => /^[^/]+\.py$/.test(rel);
|
|
142
|
+
const isPython = (rel) => rel.endsWith(".py");
|
|
143
|
+
const hasFastApiSignal = (file) => /FastAPI\s*\(|APIRouter\s*\(|@app\.(get|post|put|patch|delete)|@router\.(get|post|put|patch|delete)/.test(pythonText(file));
|
|
144
|
+
const api = findRepoFiles(result, (rel, file) => !isNonSourcePath(rel) && (
|
|
145
|
+
/(backend|app|src)\/.*(router|routes|api)\//.test(rel) ||
|
|
146
|
+
/(backend|app|src)\/.*(router|routes).*\.py$/.test(rel) ||
|
|
147
|
+
/\/(routing|routers?)\.py$/.test(rel) ||
|
|
148
|
+
(isRootPython(rel) && hasFastApiSignal(file))
|
|
149
|
+
), 30).map((f) => f.path);
|
|
150
|
+
const services = findRepoFiles(result, (rel) => !isNonSourcePath(rel) && (
|
|
151
|
+
/(backend|app|src)\/.*(service|services|application)/.test(rel) ||
|
|
152
|
+
/\/applications?\.py$/.test(rel) ||
|
|
153
|
+
(isRootPython(rel) && /(service|worker|manager|client|heartbeat|memory|chat|tool|orchestrator|pipeline)/i.test(rel))
|
|
154
|
+
), 40).map((f) => f.path);
|
|
155
|
+
const models = findRepoFiles(result, (rel) => !isNonSourcePath(rel) && (
|
|
156
|
+
/(backend|app|src)\/.*models\.py$/.test(rel) ||
|
|
157
|
+
/(backend|app|src)\/.*schema/.test(rel) ||
|
|
158
|
+
/\/models\.py$/.test(rel) ||
|
|
159
|
+
(isRootPython(rel) && /(model|schema|entity|types?)/i.test(rel))
|
|
160
|
+
), 30).map((f) => f.path);
|
|
161
|
+
const db = findRepoFiles(result, (rel) => !isNonSourcePath(rel) && (
|
|
162
|
+
/(backend|app|src)\/.*\/(db|database|alembic|migrations)[/.]/.test(rel) ||
|
|
163
|
+
(isPython(rel) && /(sqlite|qdrant|neo4j|database|storage|repository|vector|graph|migration)/i.test(rel))
|
|
164
|
+
), 30).map((f) => f.path);
|
|
165
|
+
const config = findRepoFiles(result, (rel) => !isNonSourcePath(rel) && (
|
|
166
|
+
/(backend|app|src)\/.*(config|settings|env).*\.py$/.test(rel) ||
|
|
167
|
+
(isRootPython(rel) && /(config|settings|env)/i.test(rel)) ||
|
|
168
|
+
rel.endsWith("requirements.txt") ||
|
|
169
|
+
rel === "pyproject.toml"
|
|
170
|
+
), 20).map((f) => f.path);
|
|
171
|
+
const auth = findRepoFiles(result, (rel, file) => !isNonSourcePath(rel) && (
|
|
172
|
+
/(backend|app|src)\/.*auth/.test(rel) ||
|
|
173
|
+
/\/security\.py$/.test(rel) ||
|
|
174
|
+
(isPython(rel) && /(auth|security|permission|token|session|oauth|jwt|middleware)/i.test(rel)) ||
|
|
175
|
+
(isRootPython(rel) && /(Depends|HTTPBearer|OAuth2|JWT|Authorization)/.test(pythonText(file)))
|
|
176
|
+
), 30).map((f) => f.path);
|
|
177
|
+
return { api, services, models, db, config, auth };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function detectFrontendPaths(result) {
|
|
181
|
+
const appSurface = /(^|\/)(frontend\/)?src\/(app|pages|routes)\//;
|
|
182
|
+
const appFile = /(^|\/)(frontend\/)?src\/(App|main|index)\.(jsx?|tsx?|vue|svelte)$/;
|
|
183
|
+
const componentFile = /(^|\/)(frontend\/)?src\/(components|ui|widgets)\//;
|
|
184
|
+
const apiFile = /(^|\/)(frontend\/)?src\/(lib|hooks|composables|stores|services|api)\/.*(api|client|query|fetch|request|finops)/;
|
|
185
|
+
const stylingFile = /tailwind\.config|globals\.css|app\.css|\.module\.css|\.scss$|(^|\/)(frontend\/)?src\/styles?\//;
|
|
186
|
+
const stateFile = /providers?\.(tsx?|jsx?)|react-query|use[A-Z].*\.(ts|tsx|js|jsx)|(^|\/)(stores?|state|pinia|zustand|redux)\//;
|
|
187
|
+
return {
|
|
188
|
+
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),
|
|
189
|
+
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),
|
|
190
|
+
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),
|
|
191
|
+
styling: findRepoFiles(result, (rel) => !isNonSourcePath(rel) && stylingFile.test(rel), 24).map((f) => f.path),
|
|
192
|
+
state: findRepoFiles(result, (rel) => !isNonSourcePath(rel) && stateFile.test(rel), 24).map((f) => f.path),
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function moduleNameForPath(rel) {
|
|
197
|
+
return rel
|
|
198
|
+
.replace(/\.[^.]+$/, "")
|
|
199
|
+
.replace(/\/__init__$/, "")
|
|
200
|
+
.replace(/\/index$/, "")
|
|
201
|
+
.replace(/\//g, ".");
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function basenameStem(rel) {
|
|
205
|
+
return path.basename(rel).replace(/\.[^.]+$/, "");
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function extractPythonImports(text) {
|
|
209
|
+
const imports = new Set();
|
|
210
|
+
const importRe = /^\s*import\s+([a-zA-Z0-9_.,\s]+)/gm;
|
|
211
|
+
const fromRe = /^\s*from\s+([a-zA-Z0-9_.]+)\s+import\s+/gm;
|
|
212
|
+
let match;
|
|
213
|
+
while ((match = importRe.exec(text))) {
|
|
214
|
+
match[1].split(",").map((part) => part.trim().split(/\s+as\s+/)[0]).filter(Boolean).forEach((name) => imports.add(name));
|
|
215
|
+
}
|
|
216
|
+
while ((match = fromRe.exec(text))) imports.add(match[1]);
|
|
217
|
+
return imports;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function resolveJsImport(fromRel, specifier, sourcePaths) {
|
|
221
|
+
if (!specifier.startsWith(".")) return null;
|
|
222
|
+
const fromDir = path.posix.dirname(fromRel);
|
|
223
|
+
const base = path.posix.normalize(path.posix.join(fromDir, specifier));
|
|
224
|
+
const candidates = [
|
|
225
|
+
base,
|
|
226
|
+
`${base}.ts`,
|
|
227
|
+
`${base}.tsx`,
|
|
228
|
+
`${base}.js`,
|
|
229
|
+
`${base}.jsx`,
|
|
230
|
+
`${base}/index.ts`,
|
|
231
|
+
`${base}/index.tsx`,
|
|
232
|
+
`${base}/index.js`,
|
|
233
|
+
`${base}/index.jsx`,
|
|
234
|
+
];
|
|
235
|
+
return candidates.find((candidate) => sourcePaths.has(candidate)) || null;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function extractJsImports(fromRel, text, sourcePaths) {
|
|
239
|
+
const imports = new Set();
|
|
240
|
+
const importRe = /\bfrom\s+["']([^"']+)["']|import\s*\(\s*["']([^"']+)["']\s*\)|require\s*\(\s*["']([^"']+)["']\s*\)/g;
|
|
241
|
+
let match;
|
|
242
|
+
while ((match = importRe.exec(text))) {
|
|
243
|
+
const specifier = match[1] || match[2] || match[3];
|
|
244
|
+
const resolved = resolveJsImport(fromRel, specifier, sourcePaths);
|
|
245
|
+
if (resolved) imports.add(resolved);
|
|
246
|
+
}
|
|
247
|
+
return imports;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function getGitActivity(root) {
|
|
251
|
+
const activity = new Map();
|
|
252
|
+
try {
|
|
253
|
+
const output = execFileSync("git", ["-C", root, "log", "--since=180 days ago", "--name-only", "--format=%ct"], {
|
|
254
|
+
encoding: "utf8",
|
|
255
|
+
maxBuffer: 8 * 1024 * 1024,
|
|
256
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
257
|
+
});
|
|
258
|
+
let currentTs = 0;
|
|
259
|
+
for (const rawLine of output.split(/\r?\n/)) {
|
|
260
|
+
const line = rawLine.trim();
|
|
261
|
+
if (!line) continue;
|
|
262
|
+
if (/^\d{9,}$/.test(line)) {
|
|
263
|
+
currentTs = Number(line);
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
const rel = line.replace(/\\/g, "/");
|
|
267
|
+
const previous = activity.get(rel) || { touches: 0, lastTouched: 0 };
|
|
268
|
+
previous.touches += 1;
|
|
269
|
+
previous.lastTouched = Math.max(previous.lastTouched, currentTs);
|
|
270
|
+
activity.set(rel, previous);
|
|
271
|
+
}
|
|
272
|
+
} catch {
|
|
273
|
+
return activity;
|
|
274
|
+
}
|
|
275
|
+
return activity;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function formatGitActivity(activity) {
|
|
279
|
+
if (!activity || !activity.touches) return "no recent git touches";
|
|
280
|
+
const date = activity.lastTouched ? new Date(activity.lastTouched * 1000).toISOString().slice(0, 10) : "unknown date";
|
|
281
|
+
return `${activity.touches} recent git touch${activity.touches === 1 ? "" : "es"}, last ${date}`;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function topLoadBearing(root, files, allFiles, gitActivity, limit = 8) {
|
|
285
|
+
const textFiles = (allFiles || []).filter((file) => !file.ignored && file.kind !== "binary" && /\.(py|tsx?|jsx?|mjs|cjs|vue|svelte)$/.test(file.path));
|
|
286
|
+
const sourcePaths = new Set(textFiles.map((file) => file.path));
|
|
287
|
+
const pythonModuleToPath = new Map();
|
|
288
|
+
for (const file of textFiles.filter((candidate) => candidate.path.endsWith(".py"))) {
|
|
289
|
+
pythonModuleToPath.set(moduleNameForPath(file.path), file.path);
|
|
290
|
+
pythonModuleToPath.set(basenameStem(file.path), file.path);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const importRefs = new Map();
|
|
294
|
+
const textRefs = new Map();
|
|
295
|
+
const corpus = textFiles.map((file) => {
|
|
296
|
+
const text = readIfText(path.join(root, file.path), 512 * 1024) || "";
|
|
297
|
+
if (file.path.endsWith(".py")) {
|
|
298
|
+
for (const imported of extractPythonImports(text)) {
|
|
299
|
+
const target = pythonModuleToPath.get(imported) || pythonModuleToPath.get(imported.split(".").pop());
|
|
300
|
+
if (target && target !== file.path) importRefs.set(target, (importRefs.get(target) || 0) + 1);
|
|
301
|
+
}
|
|
302
|
+
} else if (/\.(tsx?|jsx?|mjs|cjs|vue|svelte)$/.test(file.path)) {
|
|
303
|
+
for (const target of extractJsImports(file.path, text, sourcePaths)) {
|
|
304
|
+
if (target !== file.path) importRefs.set(target, (importRefs.get(target) || 0) + 1);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
return `${file.path}\n${text}`;
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
for (const file of files || []) {
|
|
311
|
+
const base = basenameStem(file.path);
|
|
312
|
+
const importName = base.replace(/[-.]/g, "_");
|
|
313
|
+
const refs = corpus.reduce((sum, text) => sum + (text.includes(base) || text.includes(importName) ? 1 : 0), 0);
|
|
314
|
+
textRefs.set(file.path, refs);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
return [...(files || [])]
|
|
318
|
+
.filter((file) => !file.ignored && file.kind !== "binary")
|
|
319
|
+
.map((file) => {
|
|
320
|
+
const imports = importRefs.get(file.path) || 0;
|
|
321
|
+
const refs = textRefs.get(file.path) || 0;
|
|
322
|
+
const git = gitActivity.get(file.path) || { touches: 0, lastTouched: 0 };
|
|
323
|
+
const score = imports * 1000 + refs * 25 + Math.min(git.touches, 20) * 10 + Math.log10(file.size + 1);
|
|
324
|
+
return { file, imports, refs, git, score };
|
|
325
|
+
})
|
|
326
|
+
.sort((a, b) => b.score - a.score || b.file.size - a.file.size)
|
|
327
|
+
.slice(0, limit)
|
|
328
|
+
.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)})`);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return {
|
|
332
|
+
detectBackendPaths,
|
|
333
|
+
detectEntrypoints,
|
|
334
|
+
detectFrameworks,
|
|
335
|
+
detectFrontendPaths,
|
|
336
|
+
getGitActivity,
|
|
337
|
+
hasPath,
|
|
338
|
+
isNonSourcePath,
|
|
339
|
+
topLevelDirectories,
|
|
340
|
+
topLoadBearing,
|
|
341
|
+
};
|
|
342
|
+
};
|