carto-md 1.1.2 → 1.1.4
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/CONTRIBUTING.md +42 -13
- package/README.md +271 -223
- package/package.json +1 -1
- package/src/agents/domains.js +31 -3
- package/src/detector/framework.js +24 -12
- package/src/engine/carto.js +22 -6
- package/src/extractors/imports.js +75 -0
- package/src/extractors/languages/javascript.js +119 -0
- package/src/extractors/routes.js +41 -1
package/CONTRIBUTING.md
CHANGED
|
@@ -10,23 +10,32 @@ Carto is free, open source, and community-maintained. The core team owns the mer
|
|
|
10
10
|
|
|
11
11
|
New language support lives in `src/extractors/languages/`. Each language is an isolated module.
|
|
12
12
|
|
|
13
|
-
Currently supported: JavaScript/TypeScript, Python, R.
|
|
13
|
+
Currently supported: JavaScript/TypeScript, Python, Go, R.
|
|
14
14
|
|
|
15
|
-
Wanted:
|
|
15
|
+
Wanted: Rust, Ruby, Java, PHP, C#, Swift, Kotlin.
|
|
16
16
|
|
|
17
17
|
### Tier 2 — Framework extractors (safe to add, easy to review)
|
|
18
18
|
|
|
19
19
|
Framework-specific route and model extraction lives in `src/extractors/`. Each framework is an isolated module.
|
|
20
20
|
|
|
21
|
-
Currently supported:
|
|
21
|
+
Currently supported:
|
|
22
|
+
- **JS/TS**: Express, Next.js (App + Pages Router), tRPC, Drizzle, Zod
|
|
23
|
+
- **Python**: FastAPI, Flask, Pydantic, SQLAlchemy, Django (models + URLs)
|
|
24
|
+
- **Go**: Gin, Echo, Chi, Fiber, net/http — routes, structs, import graph
|
|
25
|
+
- **Schema**: Prisma
|
|
26
|
+
- **Frontend**: HTML fetch()
|
|
27
|
+
- **R**: Plumber, Shiny, R6, S7
|
|
22
28
|
|
|
23
|
-
Wanted:
|
|
29
|
+
Wanted: Rails, Laravel, NestJS, Hono, Spring, Fastify.
|
|
24
30
|
|
|
25
31
|
### Tier 3 — Core (review carefully before merging)
|
|
26
32
|
|
|
27
33
|
- `src/agents/merger.js` — merger logic. One bad merge = developer loses manual notes = project dies.
|
|
28
34
|
- `src/agents/domains.js` — graph-based domain clustering. Wrong clusters = wrong context files.
|
|
35
|
+
- `src/engine/carto.js` — programmatic module API. Breaking changes affect tools that import Carto.
|
|
29
36
|
- `src/mcp/server.js` — MCP server tools. Breaking changes affect Kiro/Cursor/Claude integration.
|
|
37
|
+
- `src/engine/incremental.js` — incremental graph update engine. Bugs here cause stale graphs.
|
|
38
|
+
- `src/cache/` — file hash + graph cache. Bugs here cause wrong re-index behavior.
|
|
30
39
|
- `src/detector/` — framework detection logic.
|
|
31
40
|
- `src/cli/` — CLI commands.
|
|
32
41
|
|
|
@@ -36,23 +45,27 @@ Wanted: Django, Rails, Laravel, NestJS, Hono, Gin, Spring.
|
|
|
36
45
|
|
|
37
46
|
1. Create `src/extractors/languages/yourlanguage.js`
|
|
38
47
|
2. Export a plugin object:
|
|
48
|
+
|
|
39
49
|
```js
|
|
40
50
|
module.exports = {
|
|
41
51
|
name: 'yourlanguage',
|
|
42
52
|
extensions: ['.ext'],
|
|
43
53
|
extract(content, relPath) {
|
|
44
54
|
return {
|
|
45
|
-
routes:
|
|
46
|
-
models:
|
|
47
|
-
functions:
|
|
48
|
-
envVars:
|
|
49
|
-
dbTables:
|
|
50
|
-
fetches:
|
|
51
|
-
storageKeys: []
|
|
55
|
+
routes: [{ method, path, functionName }],
|
|
56
|
+
models: [{ className, fields: [{ name, type }], kind: 'yourlanguage' }],
|
|
57
|
+
functions: [{ name, params, returnType }],
|
|
58
|
+
envVars: ['VAR_NAME'],
|
|
59
|
+
dbTables: [{ tableName, modelName }],
|
|
60
|
+
fetches: [],
|
|
61
|
+
storageKeys: [],
|
|
62
|
+
events: [{ type: 'listener'|'emitter', event: 'event.name' }],
|
|
63
|
+
jobs: [{ type: 'cron'|'queue'|'interval', expression?: '* * * * *', name?: 'job-name' }],
|
|
52
64
|
};
|
|
53
65
|
}
|
|
54
66
|
};
|
|
55
67
|
```
|
|
68
|
+
|
|
56
69
|
3. The loader auto-discovers it — no changes to `loader.js` needed
|
|
57
70
|
4. Test on at least 3 real open-source projects
|
|
58
71
|
5. Open a PR with before/after AGENTS.md examples
|
|
@@ -70,12 +83,27 @@ module.exports = {
|
|
|
70
83
|
|
|
71
84
|
## How to add a domain keyword
|
|
72
85
|
|
|
73
|
-
Domain clustering lives in `src/agents/domains.js`. The `
|
|
86
|
+
Domain clustering lives in `src/agents/domains.js`. The `DEFAULT_DOMAIN_MAP` array maps keywords to domain names. If your framework creates a new domain category, add it:
|
|
74
87
|
|
|
75
88
|
```js
|
|
76
89
|
{ keywords: ['graphql', 'resolver', 'mutation'], domain: 'GRAPHQL' },
|
|
77
90
|
```
|
|
78
91
|
|
|
92
|
+
### Project-level custom domains
|
|
93
|
+
|
|
94
|
+
For non-web repos (CLIs, desktop apps, compilers), users can define their own domains in `carto.config.json` at the project root without touching `domains.js`:
|
|
95
|
+
|
|
96
|
+
```json
|
|
97
|
+
{
|
|
98
|
+
"domains": {
|
|
99
|
+
"EDITOR": ["editor", "monaco", "text"],
|
|
100
|
+
"PLATFORM": ["platform", "service", "registry"]
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Custom config overrides the default domain map entirely for that project.
|
|
106
|
+
|
|
79
107
|
---
|
|
80
108
|
|
|
81
109
|
## Ground rules
|
|
@@ -96,7 +124,7 @@ cd carto
|
|
|
96
124
|
npm install
|
|
97
125
|
node src/cli/index.js init # test in any project
|
|
98
126
|
node src/cli/index.js serve # test MCP server
|
|
99
|
-
npm test # run test suite
|
|
127
|
+
npm test # run test suite (30 tests)
|
|
100
128
|
```
|
|
101
129
|
|
|
102
130
|
---
|
|
@@ -105,6 +133,7 @@ npm test # run test suite
|
|
|
105
133
|
|
|
106
134
|
- [ ] Tested on at least 2-3 real open-source projects
|
|
107
135
|
- [ ] Before/after AGENTS.md included in PR description
|
|
136
|
+
- [ ] Plugin returns all fields including `events` and `jobs` (can be empty arrays)
|
|
108
137
|
- [ ] No changes to merger logic (unless explicitly fixing a merger bug)
|
|
109
138
|
- [ ] No network calls added
|
|
110
139
|
- [ ] `carto --version` still works
|
package/README.md
CHANGED
|
@@ -4,293 +4,351 @@
|
|
|
4
4
|
[](LICENSE)
|
|
5
5
|
[](https://www.npmjs.com/package/carto-md)
|
|
6
6
|
|
|
7
|
-
**The
|
|
7
|
+
**The structural intelligence layer for AI coding tools.**
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
10
|
npm install -g carto-md
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
-
Carto
|
|
13
|
+
Carto indexes your codebase (routes, models, import graph, blast radius, domain clusters) and keeps it live. Every AI tool you use gets accurate structural facts about your project instead of guessing.
|
|
14
14
|
|
|
15
15
|
---
|
|
16
16
|
|
|
17
|
-
##
|
|
17
|
+
## What it does in one sentence
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
**Your file changes. Carto re-indexes in ~130ms. Every AI tool instantly knows what broke.**
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Proof: Supabase repo (6,335 files)
|
|
24
|
+
|
|
25
|
+
Fresh `carto init`. No prior knowledge of the repo.
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
Detected: nextjs (javascript)
|
|
29
|
+
Indexed: 310 files in 422ms
|
|
30
|
+
Routes: 104 API endpoints
|
|
31
|
+
Models: 3,598 extracted
|
|
32
|
+
Domains: AUTH · DATABASE · PAYMENTS · EVENTS · NOTIFICATIONS · TRPC · CORE
|
|
33
|
+
Import edges: 5,248
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Then a file changes:
|
|
25
37
|
|
|
26
|
-
|
|
38
|
+
```
|
|
39
|
+
packages/ui/src/lib/utils/cn.ts updated
|
|
40
|
+
|
|
41
|
+
Re-indexed in 129ms.
|
|
27
42
|
|
|
28
|
-
|
|
43
|
+
Risk: 🔴 HIGH
|
|
44
|
+
Directly affected: 55 files
|
|
45
|
+
Potentially affected: 83 files total
|
|
46
|
+
|
|
47
|
+
Files that depend on this:
|
|
48
|
+
→ packages/ui/src/components/Button/Button.tsx
|
|
49
|
+
→ packages/ui/src/components/Modal/Modal.tsx
|
|
50
|
+
→ packages/ui/src/components/shadcn/ui/button.tsx
|
|
51
|
+
→ ...83 more
|
|
52
|
+
```
|
|
29
53
|
|
|
30
|
-
|
|
31
|
-
|---|---|---|
|
|
32
|
-
| Knows blast radius before editing | Never | Always, instantly |
|
|
33
|
-
| Knows which routes break | Never | Exact list |
|
|
34
|
-
| Plans multi-file changes | Guesses | Fully informed |
|
|
35
|
-
| Hallucinates field names | Often | Never |
|
|
36
|
-
| Understands codebase on session start | 10–20 min | 0 |
|
|
37
|
-
| Works across Kiro, Cursor, Claude, Copilot | Separately | One shared graph |
|
|
38
|
-
| Stays current as code changes | Goes stale | Live on every save |
|
|
54
|
+
One file changed. Carto told you exactly what broke. In 129ms. On a 6,335 file monorepo.
|
|
39
55
|
|
|
40
56
|
---
|
|
41
57
|
|
|
42
|
-
## Proof
|
|
58
|
+
## Proof: cal.com (800k lines)
|
|
43
59
|
|
|
44
60
|
Same task, two Claude sessions: *"Add a `notes` field to the booking model."*
|
|
45
61
|
|
|
46
62
|
**Without Carto:**
|
|
47
|
-
- Wrong API route: suggested `POST /api/bookings` → actual is `POST /v2/bookings`
|
|
48
|
-
- Wrong handler: suggested `handleNewBooking.ts` → not the creation path
|
|
49
|
-
- Wrong file paths: pointed to v1 API → v1 is legacy
|
|
50
|
-
- Wrong tRPC file: `bookings.tsx` → actual is `bookings/_router.tsx`
|
|
51
|
-
- Field list: ~15 fields guessed → missing 20+ real fields
|
|
52
63
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
64
|
+
| | What AI suggested | Reality |
|
|
65
|
+
|--|---|---|
|
|
66
|
+
| API route | `POST /api/bookings` | `POST /v2/bookings` |
|
|
67
|
+
| Handler | `handleNewBooking.ts` | Not the creation path |
|
|
68
|
+
| File path | v1 API files | v1 is legacy |
|
|
69
|
+
| tRPC file | `bookings.tsx` | `bookings/_router.tsx` |
|
|
70
|
+
| Fields found | ~15 guessed | 35+ actual fields |
|
|
59
71
|
|
|
60
|
-
**
|
|
72
|
+
**With Carto:** Correct route, correct handler, correct file, all 35+ fields. One shot. Zero follow-ups.
|
|
73
|
+
|
|
74
|
+
**4 wrong paths → 0. 20 missing fields → 0.**
|
|
61
75
|
|
|
62
76
|
Not smarter AI. The same AI with accurate facts.
|
|
63
77
|
|
|
64
78
|
---
|
|
65
79
|
|
|
80
|
+
## Performance
|
|
81
|
+
|
|
82
|
+
Tested on Supabase (6,335 files, 310 watched):
|
|
83
|
+
|
|
84
|
+
| Operation | Time |
|
|
85
|
+
|-----------|------|
|
|
86
|
+
| Cold start (first `carto init`) | 422ms |
|
|
87
|
+
| Warm start (files cached) | 66ms |
|
|
88
|
+
| One file change (incremental re-index) | ~130ms |
|
|
89
|
+
| Blast radius lookup | <5ms |
|
|
90
|
+
| Route search | <1ms |
|
|
91
|
+
|
|
92
|
+
The secret: file hashes. On warm runs, Carto skips every file whose content hasn't changed. On a file save, only that one file gets re-parsed. The rest loads from disk cache in milliseconds.
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
66
96
|
## How it works
|
|
67
97
|
|
|
68
98
|
```
|
|
69
99
|
carto init
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
100
|
+
↓
|
|
101
|
+
Hashes every file → skips unchanged on re-runs
|
|
102
|
+
Builds import graph → knows who depends on who
|
|
103
|
+
Extracts routes, models, functions, env vars
|
|
104
|
+
Clusters into domains (AUTH, PAYMENTS, DATABASE...)
|
|
105
|
+
Calculates blast radius for every file
|
|
106
|
+
Writes AGENTS.md + .carto/context/*.md
|
|
107
|
+
Auto-wires MCP into Kiro, Cursor, Claude Desktop
|
|
108
|
+
↓
|
|
109
|
+
carto watch
|
|
110
|
+
↓
|
|
111
|
+
File saved → re-parse 1 file → update graph → ~130ms
|
|
79
112
|
```
|
|
80
113
|
|
|
81
114
|
---
|
|
82
115
|
|
|
83
|
-
## MCP
|
|
116
|
+
## 12 MCP tools: AI queries your codebase live
|
|
117
|
+
|
|
118
|
+
`carto serve` exposes a local MCP server. Kiro, Cursor, and Claude query it mid-task instead of guessing.
|
|
119
|
+
|
|
120
|
+
| Tool | What it returns |
|
|
121
|
+
|------|----------------|
|
|
122
|
+
| `get_blast_radius(file)` | Risk level, all affected files, routes at risk per domain |
|
|
123
|
+
| `get_context(file)` | Everything about a file in one call: domain, blast radius, neighbors, routes, models |
|
|
124
|
+
| `get_routes()` | All API endpoints with file mapping |
|
|
125
|
+
| `get_structure()` | Import graph, entry points, high-impact files, tech stack |
|
|
126
|
+
| `get_domain(name)` | All routes, models, functions for AUTH / PAYMENTS / DATABASE / etc. |
|
|
127
|
+
| `get_neighbors(file, hops)` | Import graph neighbors: nodes and edges |
|
|
128
|
+
| `get_cross_domain()` | Import edges that cross domain boundaries |
|
|
129
|
+
| `search_routes(query)` | Search API routes by path or method |
|
|
130
|
+
| `get_models(domain?)` | All data models, optionally filtered by domain |
|
|
131
|
+
| `get_high_impact_files(n)` | Top N files by blast radius, highest-risk to change |
|
|
132
|
+
| `get_env_vars(domain?)` | All env vars with domain mapping |
|
|
133
|
+
| `get_domains_list()` | All detected domains with file, route, model counts |
|
|
84
134
|
|
|
85
|
-
|
|
135
|
+
---
|
|
86
136
|
|
|
87
|
-
|
|
88
|
-
```
|
|
89
|
-
Files affected:
|
|
90
|
-
→ apps/web/app/api/checkout/route.ts
|
|
91
|
-
→ apps/web/app/api/webhook/route.ts
|
|
92
|
-
→ packages/trpc/routers/billing.ts
|
|
93
|
-
|
|
94
|
-
Routes at risk:
|
|
95
|
-
→ POST /api/checkout
|
|
96
|
-
→ POST /api/webhook
|
|
97
|
-
→ POST /trpc/createSubscription
|
|
98
|
-
```
|
|
137
|
+
## What gets extracted
|
|
99
138
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
|
103
|
-
|
|
104
|
-
|
|
|
105
|
-
|
|
|
106
|
-
|
|
|
107
|
-
|
|
|
108
|
-
|
|
|
109
|
-
|
|
139
|
+
| Category | What Carto finds |
|
|
140
|
+
|----------|-----------------|
|
|
141
|
+
| **Routes** | FastAPI, Flask, Express, Next.js App/Pages Router, React Router (JSX + createBrowserRouter), tRPC procedures, Django URLs, Gin/Echo/Chi/Fiber (Go) |
|
|
142
|
+
| **Models** | Prisma, Pydantic, SQLAlchemy, Django ORM, TypeScript interfaces/types, Zod schemas, Drizzle tables, Go structs |
|
|
143
|
+
| **Graph** | Full import graph: who imports what, transitive dependencies up to 5 hops — JS/TS, Python, Go, R |
|
|
144
|
+
| **Blast radius** | Risk level (HIGH/MEDIUM/LOW) per file and per route |
|
|
145
|
+
| **Domains** | AUTO-clustered from imports. Defaults: AUTH, PAYMENTS, DATABASE, EVENTS, TRPC, NOTIFICATIONS, CORE. Custom domains via `carto.config.json`. |
|
|
146
|
+
| **Events** | EventEmitter listeners, webhook handlers, queue jobs, cron schedules |
|
|
147
|
+
| **Env vars** | Every `process.env` / `os.Getenv` call (names only, never values) |
|
|
148
|
+
| **Functions** | Signatures with param names and return types |
|
|
110
149
|
|
|
111
|
-
|
|
112
|
-
Returns `AUTH.md` — all auth routes, session models, JWT functions, env vars.
|
|
150
|
+
---
|
|
113
151
|
|
|
114
|
-
|
|
115
|
-
Returns import graph, entry points, high impact files, tech stack.
|
|
152
|
+
## Languages and frameworks
|
|
116
153
|
|
|
117
|
-
|
|
154
|
+
| Language | Frameworks |
|
|
155
|
+
|----------|------------|
|
|
156
|
+
| TypeScript / JavaScript | Express, Next.js (App + Pages Router), React Router, tRPC, Drizzle, Zod |
|
|
157
|
+
| Python | FastAPI, Flask, Pydantic, SQLAlchemy, Django |
|
|
158
|
+
| Go | Gin, Echo, Chi, Fiber, net/http — including full import graph |
|
|
159
|
+
| R | Plumber, Shiny, R6, S7 |
|
|
160
|
+
| Schema | Prisma |
|
|
161
|
+
| HTML | fetch() calls |
|
|
118
162
|
|
|
119
|
-
|
|
120
|
-
```json
|
|
121
|
-
{
|
|
122
|
-
"mcpServers": {
|
|
123
|
-
"carto": {
|
|
124
|
-
"command": "carto",
|
|
125
|
-
"args": ["serve"],
|
|
126
|
-
"cwd": "/path/to/your/project"
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
```
|
|
163
|
+
More via community. See [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
131
164
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
"command": "carto",
|
|
138
|
-
"args": ["serve"],
|
|
139
|
-
"cwd": "/path/to/your/project"
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
```
|
|
165
|
+
---
|
|
166
|
+
|
|
167
|
+
## Custom domains (`carto.config.json`)
|
|
168
|
+
|
|
169
|
+
By default Carto clusters into web-app domains (AUTH, PAYMENTS, etc.). For any other architecture — desktop apps, CLIs, compilers, monorepos — define your own:
|
|
144
170
|
|
|
145
|
-
**Claude Desktop** — add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
|
|
146
171
|
```json
|
|
147
172
|
{
|
|
148
|
-
"
|
|
149
|
-
"
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
}
|
|
173
|
+
"domains": {
|
|
174
|
+
"EDITOR": ["editor", "monaco", "text", "cursor"],
|
|
175
|
+
"WORKBENCH": ["workbench", "layout", "panel", "sidebar"],
|
|
176
|
+
"PLATFORM": ["platform", "service", "registry"],
|
|
177
|
+
"BASE": ["base", "common", "util"]
|
|
154
178
|
}
|
|
155
179
|
}
|
|
156
180
|
```
|
|
157
181
|
|
|
158
|
-
|
|
182
|
+
Drop `carto.config.json` in your project root. Carto picks it up on the next `carto init` or `carto sync`. The import graph and blast radius always work regardless — custom domains only affect how files are clustered and labeled.
|
|
159
183
|
|
|
160
184
|
---
|
|
161
185
|
|
|
162
|
-
##
|
|
186
|
+
## Commands
|
|
163
187
|
|
|
164
|
-
|
|
188
|
+
| Command | What it does |
|
|
189
|
+
|---------|-------------|
|
|
190
|
+
| `carto init` | Detect project, index codebase, generate AGENTS.md, wire MCP |
|
|
191
|
+
| `carto watch` | Incremental live re-index on every file save (~130ms) |
|
|
192
|
+
| `carto sync` | One-time manual re-index |
|
|
193
|
+
| `carto impact <file>` | Blast radius: risk level, affected files, routes at risk |
|
|
194
|
+
| `carto check` | Cross-domain violations, high-risk uncommitted changes, domain health |
|
|
195
|
+
| `carto serve` | Start MCP server for Kiro / Cursor / Claude |
|
|
196
|
+
| `carto remove` | Remove AGENTS.md and .carto/ from project |
|
|
197
|
+
| `carto --version` | Show version |
|
|
165
198
|
|
|
166
|
-
|
|
199
|
+
---
|
|
167
200
|
|
|
168
|
-
|
|
169
|
-
AGENTS.md → 79 lines, always loaded
|
|
170
|
-
.carto/context/
|
|
171
|
-
AUTH.md → auth routes, session models, JWT functions
|
|
172
|
-
PAYMENTS.md → Stripe routes, billing models
|
|
173
|
-
TRPC.md → all tRPC procedures
|
|
174
|
-
DATABASE.md → every model, schema, table
|
|
175
|
-
EVENTS.md → webhooks, queues, cron jobs
|
|
176
|
-
CORE.md → shared utilities
|
|
177
|
-
```
|
|
201
|
+
## `carto check`
|
|
178
202
|
|
|
179
|
-
|
|
203
|
+
Run before committing. Tells you what's risky before you push.
|
|
180
204
|
|
|
181
|
-
|
|
205
|
+
```
|
|
206
|
+
── Carto Check ───────────────────────────────────────
|
|
182
207
|
|
|
183
|
-
|
|
208
|
+
Files indexed : 310
|
|
209
|
+
Routes found : 104
|
|
210
|
+
Import edges : 5,248
|
|
211
|
+
Domains : AUTH · DATABASE · PAYMENTS · EVENTS · CORE
|
|
184
212
|
|
|
185
|
-
|
|
213
|
+
⚠️ High-risk uncommitted changes (1):
|
|
214
|
+
🔴 src/lib/auth.service.ts
|
|
215
|
+
11 files depend on this, blast risk: HIGH
|
|
186
216
|
|
|
187
|
-
|
|
188
|
-
carto impact apps/web/app/api/auth/signup/route.ts
|
|
189
|
-
|
|
190
|
-
# Impact analysis: apps/web/app/api/auth/signup/route.ts
|
|
191
|
-
#
|
|
192
|
-
# Imported by:
|
|
193
|
-
# → apps/web/app/api/auth/signup/handlers/calcomSignupHandler.ts
|
|
194
|
-
# → apps/web/app/api/auth/signup/handlers/selfHostedHandler.ts
|
|
195
|
-
#
|
|
196
|
-
# Routes at risk:
|
|
197
|
-
# → POST /api/auth/signup
|
|
198
|
-
# → ALL /api/auth/signup/handlers
|
|
199
|
-
#
|
|
200
|
-
# Risk: MEDIUM
|
|
201
|
-
```
|
|
217
|
+
✅ No cross-domain dependency violations
|
|
202
218
|
|
|
203
|
-
|
|
219
|
+
🔥 Top high-impact files:
|
|
220
|
+
83 dependents - packages/ui/src/lib/utils/cn.ts
|
|
221
|
+
35 dependents - packages/pg-meta/src/pg-format/index.ts
|
|
222
|
+
34 dependents - packages/icons/src/createSupabaseIcon.ts
|
|
223
|
+
```
|
|
204
224
|
|
|
205
225
|
---
|
|
206
226
|
|
|
207
|
-
##
|
|
227
|
+
## `carto impact`
|
|
208
228
|
|
|
209
229
|
```bash
|
|
210
|
-
|
|
211
|
-
```
|
|
230
|
+
carto impact src/middleware.ts
|
|
212
231
|
|
|
213
|
-
|
|
232
|
+
Impact analysis: src/middleware.ts
|
|
214
233
|
|
|
215
|
-
|
|
216
|
-
|
|
234
|
+
Risk: 🔴 HIGH
|
|
235
|
+
Directly affected: 11 files across 2 domain(s)
|
|
236
|
+
Domains impacted: AUTH, PAYMENTS
|
|
237
|
+
|
|
238
|
+
Files that depend on this (11):
|
|
239
|
+
→ src/routes/auth.ts
|
|
240
|
+
→ src/routes/billing.ts
|
|
241
|
+
→ ...
|
|
242
|
+
|
|
243
|
+
Routes at risk (4):
|
|
244
|
+
🔴 POST /api/auth/login
|
|
245
|
+
🔴 POST /api/billing/checkout
|
|
246
|
+
🟡 GET /api/users/me
|
|
247
|
+
🟢 GET /api/health
|
|
217
248
|
```
|
|
218
249
|
|
|
219
250
|
---
|
|
220
251
|
|
|
221
|
-
##
|
|
252
|
+
## Programmatic API
|
|
222
253
|
|
|
223
|
-
|
|
224
|
-
cd your-project
|
|
225
|
-
carto init
|
|
226
|
-
```
|
|
254
|
+
Use Carto as a module, no CLI required. This is how tools embed it.
|
|
227
255
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
- Generates AGENTS.md + domain context files
|
|
231
|
-
- Auto-wires MCP into Kiro, Cursor, Claude Desktop
|
|
232
|
-
- Installs a git hook — syncs on every commit
|
|
256
|
+
```js
|
|
257
|
+
const { Carto } = require('carto-md');
|
|
233
258
|
|
|
234
|
-
|
|
235
|
-
|
|
259
|
+
const carto = new Carto();
|
|
260
|
+
await carto.index('/path/to/project');
|
|
236
261
|
|
|
237
|
-
|
|
262
|
+
// Everything about a file in one call
|
|
263
|
+
const ctx = carto.getContextForFile('src/auth/auth.service.ts');
|
|
264
|
+
// {
|
|
265
|
+
// domain: 'AUTH',
|
|
266
|
+
// routes: ['POST /api/auth/login', 'GET /api/auth/me'],
|
|
267
|
+
// models: ['User', 'Session'],
|
|
268
|
+
// blastRadius: { risk: 'HIGH', directlyAffected: { files: 8, domains: 2 } },
|
|
269
|
+
// neighbors: { nodes: [...], edges: [...] }, // React Flow compatible
|
|
270
|
+
// crossDomainDeps: [...],
|
|
271
|
+
// domainContext: '...AUTH.md content...'
|
|
272
|
+
// }
|
|
238
273
|
|
|
239
|
-
|
|
274
|
+
// Live updates
|
|
275
|
+
carto.on('updated', ({ file, blastRadius }) => {
|
|
276
|
+
console.log(`${file} changed, blast risk: ${blastRadius.risk}`);
|
|
277
|
+
});
|
|
240
278
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
279
|
+
await carto.reindex('src/auth/auth.service.ts'); // ~130ms
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
**Full API:**
|
|
283
|
+
|
|
284
|
+
```js
|
|
285
|
+
carto.getBlastRadius(file) // Risk + affected files + routes
|
|
286
|
+
carto.getNeighbors(file, hops) // Import graph, React Flow nodes/edges
|
|
287
|
+
carto.getCrossDomainDeps() // Cross-boundary import edges
|
|
288
|
+
carto.getHighImpactFiles(n) // Top N by blast radius
|
|
289
|
+
carto.searchRoutes(query) // Route search
|
|
290
|
+
carto.getRoutes() // All API routes
|
|
291
|
+
carto.getDomain(name) // Domain cluster + context file
|
|
292
|
+
carto.getDomainsList() // All domains with counts
|
|
293
|
+
carto.getModels(domain?) // All models
|
|
294
|
+
carto.getEnvVars(domain?) // Env vars with domain mapping
|
|
295
|
+
carto.getMeta() // Index stats
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
Events: `status` · `indexed` · `updated`
|
|
250
299
|
|
|
251
300
|
---
|
|
252
301
|
|
|
253
|
-
##
|
|
302
|
+
## Domain context files
|
|
254
303
|
|
|
255
|
-
|
|
256
|
-
|----------|------------|
|
|
257
|
-
| Python | FastAPI, Pydantic |
|
|
258
|
-
| JavaScript | Express, Next.js |
|
|
259
|
-
| TypeScript | Express, Next.js, Prisma, tRPC |
|
|
260
|
-
| R | Plumber, Shiny, R6, S7 |
|
|
261
|
-
| HTML | fetch() calls |
|
|
304
|
+
Large codebases kill AI accuracy. A 2,900-line AGENTS.md means the AI reads 500 lines and guesses the rest.
|
|
262
305
|
|
|
263
|
-
|
|
306
|
+
Carto splits context by domain automatically:
|
|
264
307
|
|
|
265
|
-
|
|
308
|
+
```
|
|
309
|
+
AGENTS.md → lean map, always loaded by every AI
|
|
310
|
+
.carto/context/
|
|
311
|
+
AUTH.md → auth routes, session models, JWT functions, middleware
|
|
312
|
+
PAYMENTS.md → Stripe routes, billing models, webhook handlers
|
|
313
|
+
DATABASE.md → every model, schema, table, migration pattern
|
|
314
|
+
EVENTS.md → webhooks, queues, cron jobs, event emitters
|
|
315
|
+
TRPC.md → all procedures with input/output schemas
|
|
316
|
+
CORE.md → shared utilities
|
|
317
|
+
```
|
|
266
318
|
|
|
267
|
-
|
|
319
|
+
AI reads AGENTS.md always. Then fetches only the domain file relevant to the task. 400 lines of exact context instead of 2,900 lines of everything.
|
|
268
320
|
|
|
269
|
-
|
|
270
|
-
- Data models — Pydantic, Prisma, TypeScript interfaces
|
|
271
|
-
- Function signatures — across all files
|
|
272
|
-
- Import graph — which files depend on which
|
|
273
|
-
- Domain clusters — AUTH, PAYMENTS, TRPC, DATABASE, EVENTS
|
|
274
|
-
- Blast radius — what breaks if you change a file
|
|
275
|
-
- Environment variable names — never values
|
|
276
|
-
- Database tables — SQLAlchemy, Django ORM, Prisma
|
|
321
|
+
Domain assignment runs on your import graph: files that import each other cluster together, regardless of folder names.
|
|
277
322
|
|
|
278
323
|
---
|
|
279
324
|
|
|
280
|
-
##
|
|
325
|
+
## MCP config (if auto-wire missed your IDE)
|
|
281
326
|
|
|
282
|
-
|
|
327
|
+
**Kiro**: `~/.kiro/settings/mcp.json`
|
|
328
|
+
```json
|
|
329
|
+
{ "mcpServers": { "carto": { "command": "carto", "args": ["serve"], "cwd": "/your/project" } } }
|
|
330
|
+
```
|
|
283
331
|
|
|
332
|
+
**Cursor**: `~/.cursor/mcp.json`
|
|
333
|
+
```json
|
|
334
|
+
{ "mcpServers": { "carto": { "command": "carto", "args": ["serve"], "cwd": "/your/project" } } }
|
|
284
335
|
```
|
|
285
|
-
<!-- CARTO:AUTO:START -->
|
|
286
|
-
... auto-generated content ...
|
|
287
|
-
<!-- CARTO:AUTO:END -->
|
|
288
336
|
|
|
289
|
-
|
|
337
|
+
**Claude Desktop**: `~/Library/Application Support/Claude/claude_desktop_config.json`
|
|
338
|
+
```json
|
|
339
|
+
{ "mcpServers": { "carto": { "command": "carto", "args": ["serve"], "cwd": "/your/project" } } }
|
|
290
340
|
```
|
|
291
341
|
|
|
292
342
|
---
|
|
293
343
|
|
|
344
|
+
## AI tools that read AGENTS.md natively
|
|
345
|
+
|
|
346
|
+
Cursor · GitHub Copilot · Kiro · Claude Desktop · Claude Code · Codex · VS Code · Gemini CLI · Devin · Jules
|
|
347
|
+
|
|
348
|
+
Carto generates the file they all read. One source of truth. Every tool stays accurate.
|
|
349
|
+
|
|
350
|
+
---
|
|
351
|
+
|
|
294
352
|
## What Carto fixes
|
|
295
353
|
|
|
296
354
|
Carto fixes **factual hallucination about your own project**:
|
|
@@ -298,66 +356,56 @@ Carto fixes **factual hallucination about your own project**:
|
|
|
298
356
|
- AI guessing wrong routes → fixed
|
|
299
357
|
- AI guessing wrong field names → fixed
|
|
300
358
|
- AI assuming wrong framework → fixed
|
|
301
|
-
- AI guessing wrong DB schema → fixed
|
|
302
359
|
- AI not knowing blast radius → fixed
|
|
360
|
+
- AI losing context between sessions → fixed
|
|
361
|
+
- Rebuilding project context every session → gone
|
|
303
362
|
|
|
304
|
-
What Carto does not fix: AI reasoning badly, wrong implementation logic, misunderstanding
|
|
363
|
+
What Carto does not fix: AI reasoning badly, wrong implementation logic, misunderstanding requirements. Carto makes AI **accurate** about your project. Not smarter. Accurate. Different thing.
|
|
305
364
|
|
|
306
365
|
---
|
|
307
366
|
|
|
308
|
-
##
|
|
367
|
+
## What Carto never does
|
|
309
368
|
|
|
310
|
-
-
|
|
311
|
-
-
|
|
312
|
-
-
|
|
313
|
-
-
|
|
314
|
-
- **Claude Code** — natively
|
|
315
|
-
- **Codex** — natively
|
|
316
|
-
- **VS Code** — via workspace context
|
|
317
|
-
- **Gemini CLI** — natively
|
|
318
|
-
- **Devin** — natively
|
|
319
|
-
- **Jules** — natively
|
|
369
|
+
- Sends your code anywhere. Local only.
|
|
370
|
+
- Writes secrets into AGENTS.md. `.cartoignore` blocks `.env` and credential files by default.
|
|
371
|
+
- Touches your manual notes. It writes only between `<!-- CARTO:AUTO:START -->` and `<!-- CARTO:AUTO:END -->`.
|
|
372
|
+
- Costs money. MIT license. Free forever.
|
|
320
373
|
|
|
321
374
|
---
|
|
322
375
|
|
|
323
|
-
##
|
|
324
|
-
|
|
325
|
-
- No cloud. No servers. No telemetry. No tracking.
|
|
326
|
-
- Your code never leaves your machine.
|
|
327
|
-
- No paid tiers. Free forever. MIT license.
|
|
328
|
-
|
|
329
|
-
---
|
|
330
|
-
|
|
331
|
-
## Security
|
|
332
|
-
|
|
333
|
-
Carto never writes secrets into AGENTS.md. `.cartoignore` blocks `.env` files, secret files, key files, and credential files by default. The sanitizer strips API key patterns from extracted code.
|
|
376
|
+
## Install
|
|
334
377
|
|
|
335
|
-
|
|
378
|
+
```bash
|
|
379
|
+
npm install -g carto-md
|
|
380
|
+
```
|
|
336
381
|
|
|
337
|
-
|
|
382
|
+
```bash
|
|
383
|
+
cd your-project
|
|
384
|
+
carto init
|
|
385
|
+
```
|
|
338
386
|
|
|
339
|
-
|
|
387
|
+
That's it.
|
|
340
388
|
|
|
341
389
|
---
|
|
342
390
|
|
|
343
391
|
## Origin
|
|
344
392
|
|
|
345
|
-
I was building
|
|
393
|
+
I was building Emfirge, a cloud security agent for AWS.
|
|
346
394
|
|
|
347
|
-
To make the AI
|
|
395
|
+
To make the AI understand infrastructure, I built a module that mapped AWS resources into a graph. The AI stopped hallucinating. It worked with facts.
|
|
348
396
|
|
|
349
|
-
|
|
397
|
+
Then I switched AI tools. New session. Had to explain the whole project again from scratch.
|
|
350
398
|
|
|
351
|
-
I thought: *I just built a cartography system
|
|
399
|
+
I thought: *I just built a cartography system for infrastructure. Why doesn't this exist for codebases?*
|
|
352
400
|
|
|
353
|
-
Carto is that.
|
|
401
|
+
Carto is that.
|
|
354
402
|
|
|
355
403
|
---
|
|
356
404
|
|
|
357
405
|
## License
|
|
358
406
|
|
|
359
|
-
MIT
|
|
407
|
+
MIT. Free forever.
|
|
360
408
|
|
|
361
409
|
---
|
|
362
410
|
|
|
363
|
-
*
|
|
411
|
+
*Your code changes. Carto knows. Every AI you use knows.*
|
package/package.json
CHANGED
package/src/agents/domains.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
const path = require('path');
|
|
2
2
|
|
|
3
|
-
const
|
|
3
|
+
const DEFAULT_DOMAIN_MAP = [
|
|
4
4
|
{ keywords: ['auth', 'login', 'session', 'oauth', 'token', 'jwt', 'password', 'credential'], domain: 'AUTH' },
|
|
5
5
|
{ keywords: ['payment', 'billing', 'stripe', 'invoice', 'charge', 'subscription', 'checkout'], domain: 'PAYMENTS' },
|
|
6
6
|
{ keywords: ['trpc', 'router', 'routers', 'procedure'], domain: 'TRPC' },
|
|
@@ -9,6 +9,29 @@ const DOMAIN_MAP = [
|
|
|
9
9
|
{ keywords: ['email', 'notification', 'mail', 'sms', 'alert'], domain: 'NOTIFICATIONS' },
|
|
10
10
|
];
|
|
11
11
|
|
|
12
|
+
// Active domain map — replaced by setDomainMap() when carto.config.json is present
|
|
13
|
+
let DOMAIN_MAP = DEFAULT_DOMAIN_MAP;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* setDomainMap(customDomains)
|
|
17
|
+
* Override the domain map from carto.config.json.
|
|
18
|
+
*
|
|
19
|
+
* customDomains format:
|
|
20
|
+
* { "EDITOR": ["editor", "monaco"], "WORKBENCH": ["workbench", "panel"] }
|
|
21
|
+
*
|
|
22
|
+
* Pass null to reset to defaults.
|
|
23
|
+
*/
|
|
24
|
+
function setDomainMap(customDomains) {
|
|
25
|
+
if (!customDomains || typeof customDomains !== 'object' || Array.isArray(customDomains)) {
|
|
26
|
+
DOMAIN_MAP = DEFAULT_DOMAIN_MAP;
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
DOMAIN_MAP = Object.entries(customDomains).map(([domain, keywords]) => ({
|
|
30
|
+
domain: domain.toUpperCase(),
|
|
31
|
+
keywords: keywords.map(k => String(k).toLowerCase()),
|
|
32
|
+
}));
|
|
33
|
+
}
|
|
34
|
+
|
|
12
35
|
/**
|
|
13
36
|
* getDomainForFile(relPath) → domain string or null
|
|
14
37
|
* Returns domain if path matches a keyword, null if no match.
|
|
@@ -112,7 +135,7 @@ function clusterByDomain(data) {
|
|
|
112
135
|
|
|
113
136
|
function getCluster(domain) {
|
|
114
137
|
if (!clusters[domain]) {
|
|
115
|
-
clusters[domain] = { routes: [], models: [], functions: {}, envVars: [], dbTables: [], fileMap: [] };
|
|
138
|
+
clusters[domain] = { routes: [], models: [], functions: {}, envVars: [], dbTables: [], fileMap: [], files: [] };
|
|
116
139
|
}
|
|
117
140
|
return clusters[domain];
|
|
118
141
|
}
|
|
@@ -182,7 +205,12 @@ function clusterByDomain(data) {
|
|
|
182
205
|
getCluster(domain).fileMap.push(entry);
|
|
183
206
|
}
|
|
184
207
|
|
|
208
|
+
// Files — populate from assignments so cluster.files is always set
|
|
209
|
+
for (const [file, domain] of assignments.entries()) {
|
|
210
|
+
getCluster(domain).files.push(file);
|
|
211
|
+
}
|
|
212
|
+
|
|
185
213
|
return clusters;
|
|
186
214
|
}
|
|
187
215
|
|
|
188
|
-
module.exports = { clusterByDomain, getDomainForFile, buildFileAssignments };
|
|
216
|
+
module.exports = { clusterByDomain, getDomainForFile, buildFileAssignments, setDomainMap };
|
|
@@ -7,6 +7,7 @@ const IGNORE_DIRS = new Set(['node_modules', '.git', '__pycache__', '.venv', 've
|
|
|
7
7
|
const PYTHON_PRIORITY = ['fastapi', 'django', 'flask', 'python-generic'];
|
|
8
8
|
const JS_PRIORITY = ['nextjs', 'express', 'react', 'node-generic'];
|
|
9
9
|
const R_PRIORITY = ['plumber', 'shiny', 'r-generic'];
|
|
10
|
+
const GO_PRIORITY = ['gin', 'echo', 'chi', 'fiber', 'go-generic'];
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* detectFramework(projectRoot) → { framework, language, confidence, secondaryFramework?, secondaryLanguage? }
|
|
@@ -17,7 +18,7 @@ const R_PRIORITY = ['plumber', 'shiny', 'r-generic'];
|
|
|
17
18
|
* (primary = highest priority overall, secondary = the other language).
|
|
18
19
|
*/
|
|
19
20
|
function detectFramework(projectRoot) {
|
|
20
|
-
const candidates = findFile(projectRoot, ['requirements.txt', 'package.json', 'pyproject.toml', 'DESCRIPTION'], 3);
|
|
21
|
+
const candidates = findFile(projectRoot, ['requirements.txt', 'package.json', 'pyproject.toml', 'DESCRIPTION', 'go.mod'], 3);
|
|
21
22
|
|
|
22
23
|
const pythonDetections = new Set();
|
|
23
24
|
const jsDetections = new Set();
|
|
@@ -43,9 +44,15 @@ function detectFramework(projectRoot) {
|
|
|
43
44
|
for (const r of detectAllFromRFiles(projectRoot)) rDetections.add(r);
|
|
44
45
|
}
|
|
45
46
|
|
|
47
|
+
const goDetections = new Set();
|
|
48
|
+
for (const f of candidates.filter(f => path.basename(f) === 'go.mod')) {
|
|
49
|
+
for (const r of detectAllFromGoMod(f)) goDetections.add(r);
|
|
50
|
+
}
|
|
51
|
+
|
|
46
52
|
const bestPython = PYTHON_PRIORITY.find(fw => pythonDetections.has(fw)) || null;
|
|
47
53
|
const bestJS = JS_PRIORITY.find(fw => jsDetections.has(fw)) || null;
|
|
48
54
|
const bestR = R_PRIORITY.find(fw => rDetections.has(fw)) || null;
|
|
55
|
+
const bestGo = GO_PRIORITY.find(fw => goDetections.has(fw)) || null;
|
|
49
56
|
|
|
50
57
|
if (bestPython && bestJS) {
|
|
51
58
|
return {
|
|
@@ -57,17 +64,10 @@ function detectFramework(projectRoot) {
|
|
|
57
64
|
};
|
|
58
65
|
}
|
|
59
66
|
|
|
60
|
-
if (bestPython) {
|
|
61
|
-
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
if (bestJS) {
|
|
65
|
-
return { framework: bestJS, language: 'javascript', confidence: 'high' };
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
if (bestR) {
|
|
69
|
-
return { framework: bestR, language: 'r', confidence: 'high' };
|
|
70
|
-
}
|
|
67
|
+
if (bestPython) return { framework: bestPython, language: 'python', confidence: 'high' };
|
|
68
|
+
if (bestJS) return { framework: bestJS, language: 'javascript', confidence: 'high' };
|
|
69
|
+
if (bestGo) return { framework: bestGo, language: 'go', confidence: 'high' };
|
|
70
|
+
if (bestR) return { framework: bestR, language: 'r', confidence: 'high' };
|
|
71
71
|
|
|
72
72
|
return { framework: 'unknown', language: 'unknown', confidence: 'none' };
|
|
73
73
|
}
|
|
@@ -141,6 +141,18 @@ function detectAllFromPackageJson(filePath) {
|
|
|
141
141
|
return detected;
|
|
142
142
|
}
|
|
143
143
|
|
|
144
|
+
function detectAllFromGoMod(filePath) {
|
|
145
|
+
const detected = [];
|
|
146
|
+
let content;
|
|
147
|
+
try { content = fs.readFileSync(filePath, 'utf-8').toLowerCase(); } catch { return detected; }
|
|
148
|
+
if (content.includes('gin-gonic/gin')) detected.push('gin');
|
|
149
|
+
if (content.includes('labstack/echo')) detected.push('echo');
|
|
150
|
+
if (content.includes('go-chi/chi')) detected.push('chi');
|
|
151
|
+
if (content.includes('gofiber/fiber')) detected.push('fiber');
|
|
152
|
+
if (!detected.length) detected.push('go-generic');
|
|
153
|
+
return detected;
|
|
154
|
+
}
|
|
155
|
+
|
|
144
156
|
function detectAllFromRDescription(filePath) {
|
|
145
157
|
const detected = [];
|
|
146
158
|
let content;
|
package/src/engine/carto.js
CHANGED
|
@@ -11,7 +11,7 @@ const { WorkerPool } = require('./worker-pool');
|
|
|
11
11
|
const { loadLanguagePlugins, getPluginForFile } = require('../extractors/loader');
|
|
12
12
|
const { buildImportGraph } = require('../extractors/imports');
|
|
13
13
|
const { buildStackLine } = require('../extractors/stack');
|
|
14
|
-
const { getDomainForFile, buildFileAssignments } = require('../agents/domains');
|
|
14
|
+
const { getDomainForFile, buildFileAssignments, setDomainMap } = require('../agents/domains');
|
|
15
15
|
const { extractImports } = require('../extractors/imports');
|
|
16
16
|
|
|
17
17
|
const plugins = loadLanguagePlugins();
|
|
@@ -39,6 +39,7 @@ class Carto extends EventEmitter {
|
|
|
39
39
|
this._cache = null;
|
|
40
40
|
this._projectRoot = null;
|
|
41
41
|
this._pool = null;
|
|
42
|
+
this._domainByFile = null; // reverse map: relPath → domainName
|
|
42
43
|
}
|
|
43
44
|
|
|
44
45
|
// ─── Indexing ────────────────────────────────────────────────────────────
|
|
@@ -54,6 +55,14 @@ class Carto extends EventEmitter {
|
|
|
54
55
|
|
|
55
56
|
this.emit('status', { state: 'indexing', progress: 0 });
|
|
56
57
|
|
|
58
|
+
// Load custom domain config if present
|
|
59
|
+
try {
|
|
60
|
+
const config = JSON.parse(fs.readFileSync(path.join(projectRoot, 'carto.config.json'), 'utf-8'));
|
|
61
|
+
setDomainMap(config.domains || null);
|
|
62
|
+
} catch {
|
|
63
|
+
setDomainMap(null); // reset to defaults
|
|
64
|
+
}
|
|
65
|
+
|
|
57
66
|
const cartoDir = path.join(projectRoot, '.carto');
|
|
58
67
|
try { fs.mkdirSync(cartoDir, { recursive: true }); } catch {}
|
|
59
68
|
|
|
@@ -146,6 +155,7 @@ class Carto extends EventEmitter {
|
|
|
146
155
|
}
|
|
147
156
|
|
|
148
157
|
recomputeGraphMetrics(this._cache);
|
|
158
|
+
this._buildDomainMap();
|
|
149
159
|
this._cache.meta.indexDuration = Date.now() - start;
|
|
150
160
|
this._cache.meta.lastIndexed = new Date().toISOString();
|
|
151
161
|
this._cache.generated = new Date().toISOString();
|
|
@@ -185,6 +195,7 @@ class Carto extends EventEmitter {
|
|
|
185
195
|
updateFileHash(this._projectRoot, relPath, content);
|
|
186
196
|
saveGraphCache(this._projectRoot, this._cache);
|
|
187
197
|
|
|
198
|
+
this._buildDomainMap();
|
|
188
199
|
const blastRadius = this.getBlastRadius(relPath);
|
|
189
200
|
this.emit('status', { state: 'ready' });
|
|
190
201
|
this.emit('updated', { file: relPath, blastRadius });
|
|
@@ -555,12 +566,17 @@ class Carto extends EventEmitter {
|
|
|
555
566
|
return null;
|
|
556
567
|
}
|
|
557
568
|
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
for (const [name, cluster] of Object.entries(domains)) {
|
|
561
|
-
|
|
569
|
+
_buildDomainMap() {
|
|
570
|
+
this._domainByFile = {};
|
|
571
|
+
for (const [name, cluster] of Object.entries(this._cache.domains || {})) {
|
|
572
|
+
for (const file of (cluster.files || [])) {
|
|
573
|
+
this._domainByFile[file] = name;
|
|
574
|
+
}
|
|
562
575
|
}
|
|
563
|
-
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
_getDomainForFile(relPath) {
|
|
579
|
+
return this._domainByFile ? (this._domainByFile[relPath] || null) : null;
|
|
564
580
|
}
|
|
565
581
|
|
|
566
582
|
_discoverFiles(projectRoot) {
|
|
@@ -39,6 +39,8 @@ function extractImports(content, filePath, projectRoot) {
|
|
|
39
39
|
rawImports = extractPythonImports(content, filePath, projectRoot);
|
|
40
40
|
} else if (ext === '.r') {
|
|
41
41
|
return extractRImports(content, filePath, projectRoot);
|
|
42
|
+
} else if (ext === '.go') {
|
|
43
|
+
return extractGoImports(content, filePath, projectRoot);
|
|
42
44
|
}
|
|
43
45
|
|
|
44
46
|
// Resolve and deduplicate
|
|
@@ -220,6 +222,79 @@ function resolveImportPath(importPath, fileDir, projectRoot, sourceExt) {
|
|
|
220
222
|
return null;
|
|
221
223
|
}
|
|
222
224
|
|
|
225
|
+
// ─── Go imports ──────────────────────────────────────────────────────────────
|
|
226
|
+
|
|
227
|
+
// Cache go.mod module name per projectRoot — read once, reuse
|
|
228
|
+
const _goModCache = new Map();
|
|
229
|
+
|
|
230
|
+
function _getGoModuleName(projectRoot) {
|
|
231
|
+
if (_goModCache.has(projectRoot)) return _goModCache.get(projectRoot);
|
|
232
|
+
try {
|
|
233
|
+
const content = fs.readFileSync(path.join(projectRoot, 'go.mod'), 'utf-8');
|
|
234
|
+
const m = content.match(/^module\s+(\S+)/m);
|
|
235
|
+
const name = m ? m[1] : null;
|
|
236
|
+
_goModCache.set(projectRoot, name);
|
|
237
|
+
return name;
|
|
238
|
+
} catch {
|
|
239
|
+
_goModCache.set(projectRoot, null);
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* extractGoImports(content, filePath, projectRoot) → Array<string>
|
|
246
|
+
*
|
|
247
|
+
* Resolves local Go imports (same module) to actual .go files.
|
|
248
|
+
* Requires go.mod at projectRoot to determine the module name.
|
|
249
|
+
*
|
|
250
|
+
* Handles:
|
|
251
|
+
* import "module/path/pkg"
|
|
252
|
+
* import ( "module/path/pkg1" \n "module/path/pkg2" )
|
|
253
|
+
* import alias "module/path/pkg"
|
|
254
|
+
*/
|
|
255
|
+
function extractGoImports(content, filePath, projectRoot) {
|
|
256
|
+
const moduleName = _getGoModuleName(projectRoot);
|
|
257
|
+
if (!moduleName) return [];
|
|
258
|
+
|
|
259
|
+
const results = new Set();
|
|
260
|
+
|
|
261
|
+
// Collect all import paths from both single and block imports
|
|
262
|
+
const importPaths = [];
|
|
263
|
+
|
|
264
|
+
// Single-line: import "path" or import alias "path"
|
|
265
|
+
const singleRe = /^import\s+(?:\w+\s+)?"([^"]+)"/gm;
|
|
266
|
+
let m;
|
|
267
|
+
while ((m = singleRe.exec(content)) !== null) importPaths.push(m[1]);
|
|
268
|
+
|
|
269
|
+
// Block: import ( ... )
|
|
270
|
+
const blockRe = /import\s*\(([^)]+)\)/g;
|
|
271
|
+
while ((m = blockRe.exec(content)) !== null) {
|
|
272
|
+
const block = m[1];
|
|
273
|
+
const lineRe = /"([^"]+)"/g;
|
|
274
|
+
let lm;
|
|
275
|
+
while ((lm = lineRe.exec(block)) !== null) importPaths.push(lm[1]);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Resolve local imports only (starts with this module's name)
|
|
279
|
+
const prefix = moduleName + '/';
|
|
280
|
+
for (const imp of importPaths) {
|
|
281
|
+
if (!imp.startsWith(prefix)) continue;
|
|
282
|
+
const localPkg = imp.slice(prefix.length); // e.g. "internal/auth"
|
|
283
|
+
const pkgDir = path.join(projectRoot, localPkg);
|
|
284
|
+
|
|
285
|
+
// Find first non-test .go file in the package directory
|
|
286
|
+
try {
|
|
287
|
+
const entries = fs.readdirSync(pkgDir);
|
|
288
|
+
const goFile = entries.find(e => e.endsWith('.go') && !e.endsWith('_test.go'));
|
|
289
|
+
if (goFile) {
|
|
290
|
+
results.add(path.relative(projectRoot, path.join(pkgDir, goFile)));
|
|
291
|
+
}
|
|
292
|
+
} catch { /* directory doesn't exist or can't be read */ }
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return [...results].sort();
|
|
296
|
+
}
|
|
297
|
+
|
|
223
298
|
/**
|
|
224
299
|
* buildImportGraph(fileContents, projectRoot) → { 'relative/path.js': ['relative/dep.js', ...] }
|
|
225
300
|
*
|
|
@@ -38,6 +38,7 @@ module.exports = {
|
|
|
38
38
|
|
|
39
39
|
// Expose internals for typescript.js to reuse
|
|
40
40
|
_extractExpressRoutes: extractExpressRoutes,
|
|
41
|
+
_extractReactRouterRoutes: extractReactRouterRoutes,
|
|
41
42
|
_extractProcessEnv: extractProcessEnv,
|
|
42
43
|
_extractJSFetches: extractJSFetches,
|
|
43
44
|
_extractJSFunctions: extractJSFunctions,
|
|
@@ -77,6 +78,10 @@ function extractExpressRoutes(ast, filename) {
|
|
|
77
78
|
const nextRoutes = extractNextJSPagesRoutes(ast, filename);
|
|
78
79
|
routes.push(...nextRoutes);
|
|
79
80
|
|
|
81
|
+
// React Router (JSX <Route> + createBrowserRouter/createHashRouter/createMemoryRouter)
|
|
82
|
+
const reactRoutes = extractReactRouterRoutes(ast);
|
|
83
|
+
routes.push(...reactRoutes);
|
|
84
|
+
|
|
80
85
|
walk(ast, (node) => {
|
|
81
86
|
if (node.type !== 'CallExpression') return;
|
|
82
87
|
if (!node.callee || node.callee.type !== 'MemberExpression') return;
|
|
@@ -134,6 +139,120 @@ function extractExpressRoutes(ast, filename) {
|
|
|
134
139
|
});
|
|
135
140
|
}
|
|
136
141
|
|
|
142
|
+
// ---------------------------------------------------------------------------
|
|
143
|
+
// React Router extraction
|
|
144
|
+
// ---------------------------------------------------------------------------
|
|
145
|
+
|
|
146
|
+
const REACT_ROUTER_CREATORS = new Set(['createBrowserRouter', 'createHashRouter', 'createMemoryRouter', 'useRoutes']);
|
|
147
|
+
|
|
148
|
+
function extractReactRouterRoutes(ast) {
|
|
149
|
+
const routes = [];
|
|
150
|
+
|
|
151
|
+
// 1. JSX <Route path="..." component={X} /> or element={<X />}
|
|
152
|
+
walk(ast, (node) => {
|
|
153
|
+
if (node.type !== 'JSXOpeningElement') return;
|
|
154
|
+
if (!node.name || node.name.name !== 'Route') return;
|
|
155
|
+
|
|
156
|
+
let routePath = null;
|
|
157
|
+
let componentName = '[anonymous]';
|
|
158
|
+
|
|
159
|
+
for (const attr of (node.attributes || [])) {
|
|
160
|
+
if (attr.type !== 'JSXAttribute' || !attr.name) continue;
|
|
161
|
+
const attrName = attr.name.name;
|
|
162
|
+
|
|
163
|
+
if (attrName === 'path' && attr.value) {
|
|
164
|
+
if (attr.value.type === 'StringLiteral') {
|
|
165
|
+
routePath = attr.value.value;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if ((attrName === 'component' || attrName === 'element') && attr.value) {
|
|
170
|
+
if (attr.value.type === 'JSXExpressionContainer' && attr.value.expression) {
|
|
171
|
+
const expr = attr.value.expression;
|
|
172
|
+
if (expr.type === 'Identifier') {
|
|
173
|
+
componentName = expr.name;
|
|
174
|
+
} else if (expr.type === 'JSXElement' && expr.openingElement && expr.openingElement.name) {
|
|
175
|
+
componentName = expr.openingElement.name.name || '[anonymous]';
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (routePath !== null) {
|
|
182
|
+
routes.push({ method: 'VIEW', path: routePath, functionName: componentName });
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
// 2. createBrowserRouter / createHashRouter / createMemoryRouter / useRoutes
|
|
187
|
+
walk(ast, (node) => {
|
|
188
|
+
if (node.type !== 'CallExpression') return;
|
|
189
|
+
const callee = node.callee;
|
|
190
|
+
if (!callee) return;
|
|
191
|
+
const calleeName = callee.type === 'Identifier' ? callee.name
|
|
192
|
+
: (callee.type === 'MemberExpression' && callee.property) ? callee.property.name
|
|
193
|
+
: null;
|
|
194
|
+
if (!calleeName || !REACT_ROUTER_CREATORS.has(calleeName)) return;
|
|
195
|
+
if (!node.arguments || node.arguments.length === 0) return;
|
|
196
|
+
|
|
197
|
+
const firstArg = node.arguments[0];
|
|
198
|
+
if (firstArg.type === 'ArrayExpression') {
|
|
199
|
+
extractRoutesFromRouteArray(firstArg, routes);
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
const seen = new Set();
|
|
204
|
+
return routes.filter(r => {
|
|
205
|
+
const key = `VIEW::${r.path}`;
|
|
206
|
+
if (seen.has(key)) return false;
|
|
207
|
+
seen.add(key);
|
|
208
|
+
return true;
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function extractRoutesFromRouteArray(arrayNode, routes) {
|
|
213
|
+
for (const element of (arrayNode.elements || [])) {
|
|
214
|
+
if (!element || element.type !== 'ObjectExpression') continue;
|
|
215
|
+
extractRouteFromRouteObject(element, routes);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function extractRouteFromRouteObject(objNode, routes) {
|
|
220
|
+
let routePath = null;
|
|
221
|
+
let componentName = '[anonymous]';
|
|
222
|
+
let childrenNode = null;
|
|
223
|
+
|
|
224
|
+
for (const prop of (objNode.properties || [])) {
|
|
225
|
+
if (prop.type !== 'ObjectProperty' && prop.type !== 'Property') continue;
|
|
226
|
+
if (!prop.key) continue;
|
|
227
|
+
const key = prop.key.name || prop.key.value;
|
|
228
|
+
|
|
229
|
+
if (key === 'path' && prop.value && prop.value.type === 'StringLiteral') {
|
|
230
|
+
routePath = prop.value.value;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if ((key === 'element' || key === 'component') && prop.value) {
|
|
234
|
+
const val = prop.value;
|
|
235
|
+
if (val.type === 'Identifier') {
|
|
236
|
+
componentName = val.name;
|
|
237
|
+
} else if (val.type === 'JSXElement' && val.openingElement && val.openingElement.name) {
|
|
238
|
+
componentName = val.openingElement.name.name || '[anonymous]';
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (key === 'children' && prop.value && prop.value.type === 'ArrayExpression') {
|
|
243
|
+
childrenNode = prop.value;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (routePath !== null) {
|
|
248
|
+
routes.push({ method: 'VIEW', path: routePath, functionName: componentName });
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (childrenNode) {
|
|
252
|
+
extractRoutesFromRouteArray(childrenNode, routes);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
137
256
|
/**
|
|
138
257
|
* Detects Next.js Pages Router pattern:
|
|
139
258
|
* export default function handler(req, res) in files under pages/api/
|
package/src/extractors/routes.js
CHANGED
|
@@ -25,11 +25,12 @@ function collapseMultilineDecorators(content) {
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
/**
|
|
28
|
-
* Extracts HTTP routes from FastAPI and Django files.
|
|
28
|
+
* Extracts HTTP routes from FastAPI, Flask, and Django files.
|
|
29
29
|
*/
|
|
30
30
|
function extractRoutes(content) {
|
|
31
31
|
return [
|
|
32
32
|
...extractFastAPIRoutes(content),
|
|
33
|
+
...extractFlaskRoutes(content),
|
|
33
34
|
...extractDjangoRoutes(content),
|
|
34
35
|
];
|
|
35
36
|
}
|
|
@@ -61,6 +62,45 @@ function extractFastAPIRoutes(content) {
|
|
|
61
62
|
return routes;
|
|
62
63
|
}
|
|
63
64
|
|
|
65
|
+
// ─── Flask ───────────────────────────────────────────────────────────────────
|
|
66
|
+
|
|
67
|
+
function extractFlaskRoutes(content) {
|
|
68
|
+
const routes = [];
|
|
69
|
+
|
|
70
|
+
if (!content.includes('.route(') && !content.includes('from flask')) return routes;
|
|
71
|
+
|
|
72
|
+
// @app.route('/path') or @bp.route('/path', methods=['GET', 'POST'])
|
|
73
|
+
// Also handles: @api.route, @blueprint.route, any @x.route pattern
|
|
74
|
+
const decoratorRe = /@\w+\.route\s*\(\s*['"]([^'"]+)['"]\s*(?:,\s*methods\s*=\s*\[([^\]]+)\])?\s*\)/g;
|
|
75
|
+
const funcRe = /(?:async\s+)?def\s+(\w+)/;
|
|
76
|
+
|
|
77
|
+
const lines = content.split('\n');
|
|
78
|
+
|
|
79
|
+
for (let i = 0; i < lines.length; i++) {
|
|
80
|
+
decoratorRe.lastIndex = 0;
|
|
81
|
+
const match = decoratorRe.exec(lines[i]);
|
|
82
|
+
if (!match) continue;
|
|
83
|
+
|
|
84
|
+
const routePath = match[1];
|
|
85
|
+
const methodsRaw = match[2];
|
|
86
|
+
const methods = methodsRaw
|
|
87
|
+
? methodsRaw.split(',').map(m => m.trim().replace(/['"]/g, '').toUpperCase()).filter(Boolean)
|
|
88
|
+
: ['GET'];
|
|
89
|
+
|
|
90
|
+
let functionName = '[anonymous]';
|
|
91
|
+
for (let j = i + 1; j < Math.min(i + 5, lines.length); j++) {
|
|
92
|
+
const fm = lines[j].match(funcRe);
|
|
93
|
+
if (fm) { functionName = fm[1]; break; }
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
for (const method of methods) {
|
|
97
|
+
routes.push({ method, path: routePath, functionName });
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return routes;
|
|
102
|
+
}
|
|
103
|
+
|
|
64
104
|
// ─── Django ───────────────────────────────────────────────────────────────────
|
|
65
105
|
|
|
66
106
|
function extractDjangoRoutes(content) {
|