mapra 0.1.0
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/LICENSE +21 -0
- package/README.md +144 -0
- package/dist/analyzer/blast-radius.d.ts +27 -0
- package/dist/analyzer/blast-radius.d.ts.map +1 -0
- package/dist/analyzer/blast-radius.js +58 -0
- package/dist/analyzer/blast-radius.js.map +1 -0
- package/dist/analyzer/churn.d.ts +26 -0
- package/dist/analyzer/churn.d.ts.map +1 -0
- package/dist/analyzer/churn.js +96 -0
- package/dist/analyzer/churn.js.map +1 -0
- package/dist/analyzer/co-change.d.ts +60 -0
- package/dist/analyzer/co-change.d.ts.map +1 -0
- package/dist/analyzer/co-change.js +153 -0
- package/dist/analyzer/co-change.js.map +1 -0
- package/dist/analyzer/conventions.d.ts +22 -0
- package/dist/analyzer/conventions.d.ts.map +1 -0
- package/dist/analyzer/conventions.js +81 -0
- package/dist/analyzer/conventions.js.map +1 -0
- package/dist/analyzer/git-hash.d.ts +11 -0
- package/dist/analyzer/git-hash.d.ts.map +1 -0
- package/dist/analyzer/git-hash.js +25 -0
- package/dist/analyzer/git-hash.js.map +1 -0
- package/dist/analyzer/graph-utils.d.ts +46 -0
- package/dist/analyzer/graph-utils.d.ts.map +1 -0
- package/dist/analyzer/graph-utils.js +145 -0
- package/dist/analyzer/graph-utils.js.map +1 -0
- package/dist/analyzer/index.d.ts +34 -0
- package/dist/analyzer/index.d.ts.map +1 -0
- package/dist/analyzer/index.js +63 -0
- package/dist/analyzer/index.js.map +1 -0
- package/dist/cli/hooks.d.ts +24 -0
- package/dist/cli/hooks.d.ts.map +1 -0
- package/dist/cli/hooks.js +126 -0
- package/dist/cli/hooks.js.map +1 -0
- package/dist/cli/index.d.ts +18 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +818 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/plan-parser.d.ts +20 -0
- package/dist/cli/plan-parser.d.ts.map +1 -0
- package/dist/cli/plan-parser.js +104 -0
- package/dist/cli/plan-parser.js.map +1 -0
- package/dist/cli/shim.d.ts +3 -0
- package/dist/cli/shim.d.ts.map +1 -0
- package/dist/cli/shim.js +33 -0
- package/dist/cli/shim.js.map +1 -0
- package/dist/cli/templates.d.ts +28 -0
- package/dist/cli/templates.d.ts.map +1 -0
- package/dist/cli/templates.js +91 -0
- package/dist/cli/templates.js.map +1 -0
- package/dist/encoder/encode.d.ts +17 -0
- package/dist/encoder/encode.d.ts.map +1 -0
- package/dist/encoder/encode.js +270 -0
- package/dist/encoder/encode.js.map +1 -0
- package/dist/encoder/layer-infrastructure.d.ts +17 -0
- package/dist/encoder/layer-infrastructure.d.ts.map +1 -0
- package/dist/encoder/layer-infrastructure.js +232 -0
- package/dist/encoder/layer-infrastructure.js.map +1 -0
- package/dist/encoder/layer-labels.d.ts +18 -0
- package/dist/encoder/layer-labels.d.ts.map +1 -0
- package/dist/encoder/layer-labels.js +172 -0
- package/dist/encoder/layer-labels.js.map +1 -0
- package/dist/encoder/layer-terrain.d.ts +18 -0
- package/dist/encoder/layer-terrain.d.ts.map +1 -0
- package/dist/encoder/layer-terrain.js +135 -0
- package/dist/encoder/layer-terrain.js.map +1 -0
- package/dist/encoder/layout.d.ts +53 -0
- package/dist/encoder/layout.d.ts.map +1 -0
- package/dist/encoder/layout.js +178 -0
- package/dist/encoder/layout.js.map +1 -0
- package/dist/encoder/parse-strand-header.d.ts +29 -0
- package/dist/encoder/parse-strand-header.d.ts.map +1 -0
- package/dist/encoder/parse-strand-header.js +59 -0
- package/dist/encoder/parse-strand-header.js.map +1 -0
- package/dist/encoder/spatial-text-encode.d.ts +22 -0
- package/dist/encoder/spatial-text-encode.d.ts.map +1 -0
- package/dist/encoder/spatial-text-encode.js +199 -0
- package/dist/encoder/spatial-text-encode.js.map +1 -0
- package/dist/encoder/strand-format-encode-v1.d.ts +16 -0
- package/dist/encoder/strand-format-encode-v1.d.ts.map +1 -0
- package/dist/encoder/strand-format-encode-v1.js +296 -0
- package/dist/encoder/strand-format-encode-v1.js.map +1 -0
- package/dist/encoder/strand-format-encode.d.ts +21 -0
- package/dist/encoder/strand-format-encode.d.ts.map +1 -0
- package/dist/encoder/strand-format-encode.js +562 -0
- package/dist/encoder/strand-format-encode.js.map +1 -0
- package/dist/encoder/text-encode.d.ts +13 -0
- package/dist/encoder/text-encode.d.ts.map +1 -0
- package/dist/encoder/text-encode.js +123 -0
- package/dist/encoder/text-encode.js.map +1 -0
- package/dist/query/blast-radius.d.ts +14 -0
- package/dist/query/blast-radius.d.ts.map +1 -0
- package/dist/query/blast-radius.js +81 -0
- package/dist/query/blast-radius.js.map +1 -0
- package/dist/query/cache.d.ts +29 -0
- package/dist/query/cache.d.ts.map +1 -0
- package/dist/query/cache.js +138 -0
- package/dist/query/cache.js.map +1 -0
- package/dist/query/index.d.ts +2 -0
- package/dist/query/index.d.ts.map +1 -0
- package/dist/query/index.js +46 -0
- package/dist/query/index.js.map +1 -0
- package/dist/query/resolve.d.ts +7 -0
- package/dist/query/resolve.d.ts.map +1 -0
- package/dist/query/resolve.js +24 -0
- package/dist/query/resolve.js.map +1 -0
- package/dist/query/risk-profile.d.ts +30 -0
- package/dist/query/risk-profile.d.ts.map +1 -0
- package/dist/query/risk-profile.js +94 -0
- package/dist/query/risk-profile.js.map +1 -0
- package/dist/query/test-map.d.ts +13 -0
- package/dist/query/test-map.d.ts.map +1 -0
- package/dist/query/test-map.js +43 -0
- package/dist/query/test-map.js.map +1 -0
- package/dist/scanner/index.d.ts +51 -0
- package/dist/scanner/index.d.ts.map +1 -0
- package/dist/scanner/index.js +480 -0
- package/dist/scanner/index.js.map +1 -0
- package/dist/scanner/workspace.d.ts +11 -0
- package/dist/scanner/workspace.d.ts.map +1 -0
- package/dist/scanner/workspace.js +243 -0
- package/dist/scanner/workspace.js.map +1 -0
- package/package.json +52 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Joel Lopez Berrum
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# mapra
|
|
2
|
+
|
|
3
|
+
**Stop exploring. Start building.**
|
|
4
|
+
|
|
5
|
+
Mapra encodes your codebase's structure, risk, and complexity into a single version-controlled file — so you and your AI tools understand the whole picture before touching any code.
|
|
6
|
+
|
|
7
|
+
## What does it do?
|
|
8
|
+
|
|
9
|
+
AI coding agents spend significant time exploring codebases — listing directories, reading files to understand structure, figuring out what depends on what. In testing, a typical structural question required **45 tool calls** without mapra. With mapra: **zero**.
|
|
10
|
+
|
|
11
|
+
Mapra scans your codebase and produces a `.mapra` file that captures:
|
|
12
|
+
|
|
13
|
+
- **Risk** — blast radius, cascade depth, and hidden amplifiers (files where a small change breaks many things)
|
|
14
|
+
- **Churn** — what's been changing and how fast
|
|
15
|
+
- **Hotspots** — complexity scores for files that need the most care
|
|
16
|
+
- **Infrastructure** — how your modules connect
|
|
17
|
+
- **Conventions** — patterns your codebase already follows
|
|
18
|
+
|
|
19
|
+
One file read gives your AI agent instant structural awareness.
|
|
20
|
+
|
|
21
|
+
## Quick Start
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npx mapra
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
That's it. This scans your codebase, generates `.mapra`, wires it into your `CLAUDE.md`, and installs git hooks to keep it fresh.
|
|
28
|
+
|
|
29
|
+
### Requirements
|
|
30
|
+
|
|
31
|
+
- Node.js >= 18
|
|
32
|
+
- A git repository
|
|
33
|
+
|
|
34
|
+
## What you get
|
|
35
|
+
|
|
36
|
+
Here's a real `.mapra` file from a Next.js app (300 files, 53K lines):
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
MAPRA v3 | myapp | Nextjs | 300 files | 53,081 lines | generated 2026-03-06
|
|
40
|
+
LEGEND: ×N=imported by N files | ×A→B=A direct, B total affected | dN=cascade depth | [AMP]=amplification>=2x
|
|
41
|
+
|
|
42
|
+
─── RISK (blast radius — modifying these cascades broadly) ─
|
|
43
|
+
[AMP] amp7.0 ×3→21 d3 3mod T5 src/lib/constants.ts
|
|
44
|
+
exports: ORDER_ONLINE_URL
|
|
45
|
+
[AMP] amp3.6 ×7→25 d4 3mod T4 src/lib/ordering-server.ts
|
|
46
|
+
exports: PeriodAvailability
|
|
47
|
+
+55 more with blast radius > 1
|
|
48
|
+
|
|
49
|
+
─── CHURN (last 30 days, top movers) ─────────────────────
|
|
50
|
+
20 commits +1159 -1011 src/app/api/orders/route.ts "feat: pre-order core"
|
|
51
|
+
12 commits +306 -36 src/lib/ordering.ts "feat: pre-order foundation"
|
|
52
|
+
|
|
53
|
+
─── HOTSPOTS (complexity > 0.3) ─────────────────────────
|
|
54
|
+
0.79 src/batch/runner.ts 543L 13imp
|
|
55
|
+
0.75 src/cli/index.ts 833L 5imp
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
The RISK section is mapra's unique value — it shows **hidden amplifiers**: files with few direct importers but high cascade impact. `constants.ts` above has only 3 direct importers but affects 21 files (amp 7.0). No other tool surfaces this.
|
|
59
|
+
|
|
60
|
+
### Reading the output
|
|
61
|
+
|
|
62
|
+
The `.mapra` header includes a LEGEND that decodes the compact notation:
|
|
63
|
+
|
|
64
|
+
| Symbol | Meaning | Example |
|
|
65
|
+
|--------|---------|---------|
|
|
66
|
+
| `×N` | Imported by N files | `×3` = 3 files import this |
|
|
67
|
+
| `×A→B` | A direct importers, B total affected (cascade) | `×3→21` = 3 direct, 21 total |
|
|
68
|
+
| `[AMP]` | Hidden amplifier — amplification ratio >= 2x | Few importers but large cascade |
|
|
69
|
+
| `ampN` | Amplification ratio (affected / direct) | `amp7.0` = 7x amplification |
|
|
70
|
+
| `dN` | Cascade depth (longest chain) | `d3` = 3 hops max |
|
|
71
|
+
| `Nmod` | Number of modules affected | `3mod` = crosses 3 module boundaries |
|
|
72
|
+
| `TN` | Number of test files covering this file | `T5` = 5 test files |
|
|
73
|
+
| `NL` | Lines of code | `543L` = 543 lines |
|
|
74
|
+
|
|
75
|
+
## Commands
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
mapra First-time setup (generate + wire + hooks)
|
|
79
|
+
mapra update Regenerate .mapra after code changes
|
|
80
|
+
mapra status Check .mapra freshness, hook state, wiring
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Setup details
|
|
84
|
+
|
|
85
|
+
`mapra setup` does four things:
|
|
86
|
+
|
|
87
|
+
1. Scans your codebase and writes `.mapra`
|
|
88
|
+
2. Adds an `@.mapra` reference to your `CLAUDE.md`
|
|
89
|
+
3. Installs git hooks (post-commit, post-merge, post-checkout) for auto-update
|
|
90
|
+
4. Adds a `prepare` script to `package.json` so teammates get hooks on `npm install`
|
|
91
|
+
|
|
92
|
+
### Other commands
|
|
93
|
+
|
|
94
|
+
```
|
|
95
|
+
mapra generate [path] Scan and write .mapra (without wiring CLAUDE.md)
|
|
96
|
+
mapra init [path] Wire @.mapra into CLAUDE.md (without regenerating)
|
|
97
|
+
mapra install-hooks [path] Install git hooks manually
|
|
98
|
+
mapra uninstall-hooks [path] Remove mapra git hooks
|
|
99
|
+
mapra validate-plan <file> Cross-reference a plan's file paths against .mapra
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Auto-Update
|
|
103
|
+
|
|
104
|
+
After setup, `.mapra` stays fresh automatically:
|
|
105
|
+
|
|
106
|
+
- **Git hooks** regenerate `.mapra` silently after every commit, merge, and branch switch
|
|
107
|
+
- **Teammates** get hooks automatically via `npm install` (prepare script)
|
|
108
|
+
- **Safe** — regeneration runs in the background; if it fails, your existing `.mapra` stays intact
|
|
109
|
+
|
|
110
|
+
Run `mapra status` to verify everything is working. To remove: `mapra uninstall-hooks`.
|
|
111
|
+
|
|
112
|
+
## Troubleshooting
|
|
113
|
+
|
|
114
|
+
### Shallow clones
|
|
115
|
+
|
|
116
|
+
If you cloned with `--depth`, the CHURN section will be empty or incomplete. Fix with:
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
git fetch --unshallow
|
|
120
|
+
mapra update
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### `.mapra` in `.gitignore`
|
|
124
|
+
|
|
125
|
+
`.mapra` should be committed — it's how your team shares structural awareness. If `mapra status` warns about `.gitignore`, remove the `.mapra` entry.
|
|
126
|
+
|
|
127
|
+
### Stale `.mapra`
|
|
128
|
+
|
|
129
|
+
If `mapra status` shows "may be stale", run `mapra update`. With auto-update hooks installed, this shouldn't happen.
|
|
130
|
+
|
|
131
|
+
## How It Works
|
|
132
|
+
|
|
133
|
+
1. **Scan** — parses source files, extracting imports, exports, and module boundaries
|
|
134
|
+
2. **Analyze** — computes blast radius, churn velocity, dead code, and convention patterns
|
|
135
|
+
3. **Encode** — compresses everything into a compact `.mapra` file (~1-2K tokens for most projects)
|
|
136
|
+
4. **Wire** — adds an `@.mapra` reference to your `CLAUDE.md` so AI tools load the map automatically
|
|
137
|
+
|
|
138
|
+
## Language Support
|
|
139
|
+
|
|
140
|
+
Currently supports TypeScript and JavaScript codebases (`.ts`, `.tsx`, `.js`, `.jsx`, `.mjs`, `.cjs`). Prisma schemas are also parsed for database relationships.
|
|
141
|
+
|
|
142
|
+
## License
|
|
143
|
+
|
|
144
|
+
MIT
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Blast Radius Analyzer — computes transitive impact of changing a file.
|
|
3
|
+
*
|
|
4
|
+
* Uses reverse adjacency (who imports X?) + BFS to find all transitively
|
|
5
|
+
* affected files. Signal attenuation (0.7^depth) weights nearby files higher.
|
|
6
|
+
*/
|
|
7
|
+
export interface BlastResult {
|
|
8
|
+
nodeId: string;
|
|
9
|
+
directImporters: number;
|
|
10
|
+
affectedCount: number;
|
|
11
|
+
weightedImpact: number;
|
|
12
|
+
modulesAffected: number;
|
|
13
|
+
affectedModuleNames: string[];
|
|
14
|
+
maxDepth: number;
|
|
15
|
+
amplificationRatio: number;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Compute blast radius for a single node.
|
|
19
|
+
* reverseAdj should already exclude test edges.
|
|
20
|
+
*/
|
|
21
|
+
export declare function computeBlastRadius(nodeId: string, reverseAdj: Map<string, Set<string>>): BlastResult;
|
|
22
|
+
/**
|
|
23
|
+
* Compute blast radius for all nodes that have at least one importer.
|
|
24
|
+
* Returns Map sorted by weightedImpact descending.
|
|
25
|
+
*/
|
|
26
|
+
export declare function computeAllBlastRadii(reverseAdj: Map<string, Set<string>>): Map<string, BlastResult>;
|
|
27
|
+
//# sourceMappingURL=blast-radius.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"blast-radius.d.ts","sourceRoot":"","sources":["../../src/analyzer/blast-radius.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,eAAe,EAAE,MAAM,CAAC;IACxB,aAAa,EAAE,MAAM,CAAC;IACtB,cAAc,EAAE,MAAM,CAAC;IACvB,eAAe,EAAE,MAAM,CAAC;IACxB,mBAAmB,EAAE,MAAM,EAAE,CAAC;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,kBAAkB,EAAE,MAAM,CAAC;CAC5B;AAID;;;GAGG;AACH,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,MAAM,EACd,UAAU,EAAE,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC,GACnC,WAAW,CAmCb;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAClC,UAAU,EAAE,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC,GACnC,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC,CAY1B"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Blast Radius Analyzer — computes transitive impact of changing a file.
|
|
3
|
+
*
|
|
4
|
+
* Uses reverse adjacency (who imports X?) + BFS to find all transitively
|
|
5
|
+
* affected files. Signal attenuation (0.7^depth) weights nearby files higher.
|
|
6
|
+
*/
|
|
7
|
+
import { bfs, getModuleId } from "./graph-utils.js";
|
|
8
|
+
const ATTENUATION = 0.7;
|
|
9
|
+
/**
|
|
10
|
+
* Compute blast radius for a single node.
|
|
11
|
+
* reverseAdj should already exclude test edges.
|
|
12
|
+
*/
|
|
13
|
+
export function computeBlastRadius(nodeId, reverseAdj) {
|
|
14
|
+
const directImporters = reverseAdj.get(nodeId)?.size ?? 0;
|
|
15
|
+
// BFS through reverse adjacency (who imports this → who imports them → ...)
|
|
16
|
+
const depths = bfs(nodeId, reverseAdj);
|
|
17
|
+
const affectedCount = depths.size;
|
|
18
|
+
let weightedImpact = 0;
|
|
19
|
+
let maxDepth = 0;
|
|
20
|
+
for (const depth of depths.values()) {
|
|
21
|
+
weightedImpact += Math.pow(ATTENUATION, depth);
|
|
22
|
+
if (depth > maxDepth)
|
|
23
|
+
maxDepth = depth;
|
|
24
|
+
}
|
|
25
|
+
const affectedModuleSet = new Set();
|
|
26
|
+
for (const id of depths.keys()) {
|
|
27
|
+
affectedModuleSet.add(getModuleId(id));
|
|
28
|
+
}
|
|
29
|
+
const modulesAffected = affectedModuleSet.size;
|
|
30
|
+
const affectedModuleNames = [...affectedModuleSet].sort();
|
|
31
|
+
const amplificationRatio = directImporters > 0 ? affectedCount / directImporters : 0;
|
|
32
|
+
return {
|
|
33
|
+
nodeId,
|
|
34
|
+
directImporters,
|
|
35
|
+
affectedCount,
|
|
36
|
+
weightedImpact: Math.round(weightedImpact * 100) / 100,
|
|
37
|
+
modulesAffected,
|
|
38
|
+
affectedModuleNames,
|
|
39
|
+
maxDepth,
|
|
40
|
+
amplificationRatio: Math.round(amplificationRatio * 10) / 10,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Compute blast radius for all nodes that have at least one importer.
|
|
45
|
+
* Returns Map sorted by weightedImpact descending.
|
|
46
|
+
*/
|
|
47
|
+
export function computeAllBlastRadii(reverseAdj) {
|
|
48
|
+
const results = new Map();
|
|
49
|
+
for (const nodeId of reverseAdj.keys()) {
|
|
50
|
+
const result = computeBlastRadius(nodeId, reverseAdj);
|
|
51
|
+
// Skip nodes with no transitive impact (only imported by 0 or directly by 1 with no cascade)
|
|
52
|
+
if (result.affectedCount > 1) {
|
|
53
|
+
results.set(nodeId, result);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return results;
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=blast-radius.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"blast-radius.js","sourceRoot":"","sources":["../../src/analyzer/blast-radius.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,GAAG,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAapD,MAAM,WAAW,GAAG,GAAG,CAAC;AAExB;;;GAGG;AACH,MAAM,UAAU,kBAAkB,CAChC,MAAc,EACd,UAAoC;IAEpC,MAAM,eAAe,GAAG,UAAU,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,IAAI,IAAI,CAAC,CAAC;IAE1D,4EAA4E;IAC5E,MAAM,MAAM,GAAG,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;IAEvC,MAAM,aAAa,GAAG,MAAM,CAAC,IAAI,CAAC;IAClC,IAAI,cAAc,GAAG,CAAC,CAAC;IACvB,IAAI,QAAQ,GAAG,CAAC,CAAC;IAEjB,KAAK,MAAM,KAAK,IAAI,MAAM,CAAC,MAAM,EAAE,EAAE,CAAC;QACpC,cAAc,IAAI,IAAI,CAAC,GAAG,CAAC,WAAW,EAAE,KAAK,CAAC,CAAC;QAC/C,IAAI,KAAK,GAAG,QAAQ;YAAE,QAAQ,GAAG,KAAK,CAAC;IACzC,CAAC;IAED,MAAM,iBAAiB,GAAG,IAAI,GAAG,EAAU,CAAC;IAC5C,KAAK,MAAM,EAAE,IAAI,MAAM,CAAC,IAAI,EAAE,EAAE,CAAC;QAC/B,iBAAiB,CAAC,GAAG,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC,CAAC;IACzC,CAAC;IACD,MAAM,eAAe,GAAG,iBAAiB,CAAC,IAAI,CAAC;IAC/C,MAAM,mBAAmB,GAAG,CAAC,GAAG,iBAAiB,CAAC,CAAC,IAAI,EAAE,CAAC;IAE1D,MAAM,kBAAkB,GACtB,eAAe,GAAG,CAAC,CAAC,CAAC,CAAC,aAAa,GAAG,eAAe,CAAC,CAAC,CAAC,CAAC,CAAC;IAE5D,OAAO;QACL,MAAM;QACN,eAAe;QACf,aAAa;QACb,cAAc,EAAE,IAAI,CAAC,KAAK,CAAC,cAAc,GAAG,GAAG,CAAC,GAAG,GAAG;QACtD,eAAe;QACf,mBAAmB;QACnB,QAAQ;QACR,kBAAkB,EAAE,IAAI,CAAC,KAAK,CAAC,kBAAkB,GAAG,EAAE,CAAC,GAAG,EAAE;KAC7D,CAAC;AACJ,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,oBAAoB,CAClC,UAAoC;IAEpC,MAAM,OAAO,GAAG,IAAI,GAAG,EAAuB,CAAC;IAE/C,KAAK,MAAM,MAAM,IAAI,UAAU,CAAC,IAAI,EAAE,EAAE,CAAC;QACvC,MAAM,MAAM,GAAG,kBAAkB,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;QACtD,6FAA6F;QAC7F,IAAI,MAAM,CAAC,aAAa,GAAG,CAAC,EAAE,CAAC;YAC7B,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QAC9B,CAAC;IACH,CAAC;IAED,OAAO,OAAO,CAAC;AACjB,CAAC"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git Churn Analyzer — computes per-file change frequency from git history.
|
|
3
|
+
*
|
|
4
|
+
* Shells out to `git log --numstat` once for the entire repo,
|
|
5
|
+
* parses the output, and returns per-file churn metrics.
|
|
6
|
+
*/
|
|
7
|
+
export interface ChurnResult {
|
|
8
|
+
nodeId: string;
|
|
9
|
+
commits30d: number;
|
|
10
|
+
linesAdded30d: number;
|
|
11
|
+
linesRemoved30d: number;
|
|
12
|
+
lastCommitHash: string;
|
|
13
|
+
lastCommitDate: string;
|
|
14
|
+
lastCommitMsg: string;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Parse raw `git log --numstat --format="%h|%aI|%s"` output
|
|
18
|
+
* into per-file churn metrics.
|
|
19
|
+
*/
|
|
20
|
+
export declare function parseGitLogOutput(raw: string): Map<string, ChurnResult>;
|
|
21
|
+
/**
|
|
22
|
+
* Compute churn for all files in a git repo.
|
|
23
|
+
* Returns empty map if not in a git repo or git is unavailable.
|
|
24
|
+
*/
|
|
25
|
+
export declare function computeChurn(rootDir: string): Map<string, ChurnResult>;
|
|
26
|
+
//# sourceMappingURL=churn.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"churn.d.ts","sourceRoot":"","sources":["../../src/analyzer/churn.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,EAAE,MAAM,CAAC;IACtB,eAAe,EAAE,MAAM,CAAC;IACxB,cAAc,EAAE,MAAM,CAAC;IACvB,cAAc,EAAE,MAAM,CAAC;IACvB,aAAa,EAAE,MAAM,CAAC;CACvB;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC,CAqDvE;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC,CAmCtE"}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git Churn Analyzer — computes per-file change frequency from git history.
|
|
3
|
+
*
|
|
4
|
+
* Shells out to `git log --numstat` once for the entire repo,
|
|
5
|
+
* parses the output, and returns per-file churn metrics.
|
|
6
|
+
*/
|
|
7
|
+
import { execSync } from "child_process";
|
|
8
|
+
/**
|
|
9
|
+
* Parse raw `git log --numstat --format="%h|%aI|%s"` output
|
|
10
|
+
* into per-file churn metrics.
|
|
11
|
+
*/
|
|
12
|
+
export function parseGitLogOutput(raw) {
|
|
13
|
+
const results = new Map();
|
|
14
|
+
if (!raw.trim())
|
|
15
|
+
return results;
|
|
16
|
+
const lines = raw.split("\n");
|
|
17
|
+
let currentHash = "";
|
|
18
|
+
let currentDate = "";
|
|
19
|
+
let currentMsg = "";
|
|
20
|
+
for (const line of lines) {
|
|
21
|
+
// Header line: hash|date|message
|
|
22
|
+
const headerMatch = line.match(/^([a-f0-9]+)\|([^|]+)\|(.+)$/);
|
|
23
|
+
if (headerMatch) {
|
|
24
|
+
currentHash = headerMatch[1];
|
|
25
|
+
currentDate = headerMatch[2];
|
|
26
|
+
currentMsg = headerMatch[3];
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
// Numstat line: added\tremoved\tpath
|
|
30
|
+
const statMatch = line.match(/^(\d+|-)\t(\d+|-)\t(.+)$/);
|
|
31
|
+
if (statMatch) {
|
|
32
|
+
const added = statMatch[1];
|
|
33
|
+
const removed = statMatch[2];
|
|
34
|
+
const filePath = statMatch[3];
|
|
35
|
+
// Skip binary files (- - in numstat)
|
|
36
|
+
if (added === "-" || removed === "-")
|
|
37
|
+
continue;
|
|
38
|
+
// Normalize Windows backslashes
|
|
39
|
+
const normalized = filePath.replace(/\\/g, "/");
|
|
40
|
+
const existing = results.get(normalized);
|
|
41
|
+
if (existing) {
|
|
42
|
+
existing.commits30d++;
|
|
43
|
+
existing.linesAdded30d += parseInt(added, 10);
|
|
44
|
+
existing.linesRemoved30d += parseInt(removed, 10);
|
|
45
|
+
// Keep the first (most recent) commit info since git log is newest-first
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
results.set(normalized, {
|
|
49
|
+
nodeId: normalized,
|
|
50
|
+
commits30d: 1,
|
|
51
|
+
linesAdded30d: parseInt(added, 10),
|
|
52
|
+
linesRemoved30d: parseInt(removed, 10),
|
|
53
|
+
lastCommitHash: currentHash,
|
|
54
|
+
lastCommitDate: currentDate,
|
|
55
|
+
lastCommitMsg: currentMsg,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return results;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Compute churn for all files in a git repo.
|
|
64
|
+
* Returns empty map if not in a git repo or git is unavailable.
|
|
65
|
+
*/
|
|
66
|
+
export function computeChurn(rootDir) {
|
|
67
|
+
try {
|
|
68
|
+
// Detect shallow clone — churn data will be incomplete/empty
|
|
69
|
+
try {
|
|
70
|
+
const isShallow = execSync("git rev-parse --is-shallow-repository", {
|
|
71
|
+
cwd: rootDir,
|
|
72
|
+
encoding: "utf-8",
|
|
73
|
+
timeout: 5000,
|
|
74
|
+
}).trim();
|
|
75
|
+
if (isShallow === "true") {
|
|
76
|
+
console.warn("Warning: shallow clone detected — churn data will be incomplete.");
|
|
77
|
+
console.warn(" Run `git fetch --unshallow` for accurate CHURN section.\n");
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
// git rev-parse failed — proceed anyway
|
|
82
|
+
}
|
|
83
|
+
const raw = execSync(`git log --numstat --format="%h|%aI|%s" --since="30 days ago"`, {
|
|
84
|
+
cwd: rootDir,
|
|
85
|
+
encoding: "utf-8",
|
|
86
|
+
maxBuffer: 10 * 1024 * 1024, // 10MB
|
|
87
|
+
timeout: 15000, // 15s
|
|
88
|
+
});
|
|
89
|
+
return parseGitLogOutput(raw);
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
// Not a git repo or git unavailable — churn is optional
|
|
93
|
+
return new Map();
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
//# sourceMappingURL=churn.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"churn.js","sourceRoot":"","sources":["../../src/analyzer/churn.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Co-change Analyzer — finds files that frequently change together in git history.
|
|
3
|
+
*
|
|
4
|
+
* Builds a co-occurrence matrix from `git log --name-only` across recent commits,
|
|
5
|
+
* then surfaces pairs with high co-change frequency but low import-graph proximity
|
|
6
|
+
* (the most surprising signal: files that change together but don't import each other).
|
|
7
|
+
*/
|
|
8
|
+
export interface CoChangePair {
|
|
9
|
+
fileA: string;
|
|
10
|
+
fileB: string;
|
|
11
|
+
coChangeCount: number;
|
|
12
|
+
totalCommitsA: number;
|
|
13
|
+
totalCommitsB: number;
|
|
14
|
+
confidence: number;
|
|
15
|
+
importConnected: boolean;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Parse `git log --name-only --format="%h"` output into per-commit file lists.
|
|
19
|
+
* Returns an array of sets, each set being the files changed in one commit.
|
|
20
|
+
*/
|
|
21
|
+
export declare function parseGitLogNameOnly(raw: string): Set<string>[];
|
|
22
|
+
/**
|
|
23
|
+
* Build co-occurrence counts from commit file sets.
|
|
24
|
+
* Only considers commits with <= maxFilesPerCommit files to filter out
|
|
25
|
+
* large merges/reformats that would create noise.
|
|
26
|
+
*
|
|
27
|
+
* Returns a map of "fileA\0fileB" -> count (canonical order: fileA < fileB).
|
|
28
|
+
*/
|
|
29
|
+
export declare function buildCoOccurrenceMatrix(commits: Set<string>[], maxFilesPerCommit?: number): {
|
|
30
|
+
pairs: Map<string, number>;
|
|
31
|
+
fileCounts: Map<string, number>;
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* Compute co-change pairs, prioritizing those with high co-change frequency
|
|
35
|
+
* but low import-graph proximity (surprising co-changes).
|
|
36
|
+
*
|
|
37
|
+
* @param commits Per-commit file sets from parseGitLogNameOnly
|
|
38
|
+
* @param importEdges Set of "from\0to" keys representing import relationships
|
|
39
|
+
* @param topN Maximum number of pairs to return
|
|
40
|
+
* @param minCoChanges Minimum co-change count to consider
|
|
41
|
+
*/
|
|
42
|
+
export declare function findCoChangePairs(commits: Set<string>[], importEdges: Set<string>, topN?: number, minCoChanges?: number): CoChangePair[];
|
|
43
|
+
/**
|
|
44
|
+
* Build a set of import edge keys from graph edges for import-proximity checking.
|
|
45
|
+
*/
|
|
46
|
+
export declare function buildImportEdgeSet(edges: Array<{
|
|
47
|
+
from: string;
|
|
48
|
+
to: string;
|
|
49
|
+
type: string;
|
|
50
|
+
}>): Set<string>;
|
|
51
|
+
/**
|
|
52
|
+
* Shell out to git and compute co-change pairs for a repository.
|
|
53
|
+
* Returns empty array if not in a git repo or git is unavailable.
|
|
54
|
+
*/
|
|
55
|
+
export declare function computeCoChanges(rootDir: string, edges: Array<{
|
|
56
|
+
from: string;
|
|
57
|
+
to: string;
|
|
58
|
+
type: string;
|
|
59
|
+
}>): CoChangePair[];
|
|
60
|
+
//# sourceMappingURL=co-change.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"co-change.d.ts","sourceRoot":"","sources":["../../src/analyzer/co-change.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAIH,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,aAAa,EAAE,MAAM,CAAC;IACtB,aAAa,EAAE,MAAM,CAAC;IACtB,aAAa,EAAE,MAAM,CAAC;IACtB,UAAU,EAAE,MAAM,CAAC;IACnB,eAAe,EAAE,OAAO,CAAC;CAC1B;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,EAAE,CAuC9D;AAED;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CACrC,OAAO,EAAE,GAAG,CAAC,MAAM,CAAC,EAAE,EACtB,iBAAiB,SAAK,GACrB;IAAE,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAAC,UAAU,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAAE,CAyBjE;AAED;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,GAAG,CAAC,MAAM,CAAC,EAAE,EACtB,WAAW,EAAE,GAAG,CAAC,MAAM,CAAC,EACxB,IAAI,SAAI,EACR,YAAY,SAAI,GACf,YAAY,EAAE,CAwChB;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAChC,KAAK,EAAE,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,GACvD,GAAG,CAAC,MAAM,CAAC,CAOb;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,GACvD,YAAY,EAAE,CAmBhB"}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Co-change Analyzer — finds files that frequently change together in git history.
|
|
3
|
+
*
|
|
4
|
+
* Builds a co-occurrence matrix from `git log --name-only` across recent commits,
|
|
5
|
+
* then surfaces pairs with high co-change frequency but low import-graph proximity
|
|
6
|
+
* (the most surprising signal: files that change together but don't import each other).
|
|
7
|
+
*/
|
|
8
|
+
import { execSync } from "child_process";
|
|
9
|
+
/**
|
|
10
|
+
* Parse `git log --name-only --format="%h"` output into per-commit file lists.
|
|
11
|
+
* Returns an array of sets, each set being the files changed in one commit.
|
|
12
|
+
*/
|
|
13
|
+
export function parseGitLogNameOnly(raw) {
|
|
14
|
+
const commits = [];
|
|
15
|
+
if (!raw.trim())
|
|
16
|
+
return commits;
|
|
17
|
+
let currentFiles = null;
|
|
18
|
+
for (const line of raw.split("\n")) {
|
|
19
|
+
const trimmed = line.trim();
|
|
20
|
+
if (!trimmed) {
|
|
21
|
+
// Blank line = end of commit block (only if we have files)
|
|
22
|
+
if (currentFiles && currentFiles.size > 0) {
|
|
23
|
+
commits.push(currentFiles);
|
|
24
|
+
currentFiles = null;
|
|
25
|
+
}
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
// Header line: short hash
|
|
29
|
+
if (/^[a-f0-9]{7,40}$/.test(trimmed)) {
|
|
30
|
+
if (currentFiles && currentFiles.size > 0) {
|
|
31
|
+
commits.push(currentFiles);
|
|
32
|
+
}
|
|
33
|
+
currentFiles = new Set();
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
// File path line
|
|
37
|
+
if (currentFiles) {
|
|
38
|
+
// Normalize Windows backslashes
|
|
39
|
+
currentFiles.add(trimmed.replace(/\\/g, "/"));
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
// Don't forget the last commit block
|
|
43
|
+
if (currentFiles && currentFiles.size > 0) {
|
|
44
|
+
commits.push(currentFiles);
|
|
45
|
+
}
|
|
46
|
+
return commits;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Build co-occurrence counts from commit file sets.
|
|
50
|
+
* Only considers commits with <= maxFilesPerCommit files to filter out
|
|
51
|
+
* large merges/reformats that would create noise.
|
|
52
|
+
*
|
|
53
|
+
* Returns a map of "fileA\0fileB" -> count (canonical order: fileA < fileB).
|
|
54
|
+
*/
|
|
55
|
+
export function buildCoOccurrenceMatrix(commits, maxFilesPerCommit = 20) {
|
|
56
|
+
const pairs = new Map();
|
|
57
|
+
const fileCounts = new Map();
|
|
58
|
+
for (const files of commits) {
|
|
59
|
+
// Skip large commits (merges, reformats, CI-generated changes)
|
|
60
|
+
if (files.size > maxFilesPerCommit || files.size < 2)
|
|
61
|
+
continue;
|
|
62
|
+
const sorted = [...files].sort();
|
|
63
|
+
// Count per-file occurrences
|
|
64
|
+
for (const f of sorted) {
|
|
65
|
+
fileCounts.set(f, (fileCounts.get(f) ?? 0) + 1);
|
|
66
|
+
}
|
|
67
|
+
// Count pairwise co-occurrences
|
|
68
|
+
for (let i = 0; i < sorted.length; i++) {
|
|
69
|
+
for (let j = i + 1; j < sorted.length; j++) {
|
|
70
|
+
const key = `${sorted[i]}\0${sorted[j]}`;
|
|
71
|
+
pairs.set(key, (pairs.get(key) ?? 0) + 1);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return { pairs, fileCounts };
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Compute co-change pairs, prioritizing those with high co-change frequency
|
|
79
|
+
* but low import-graph proximity (surprising co-changes).
|
|
80
|
+
*
|
|
81
|
+
* @param commits Per-commit file sets from parseGitLogNameOnly
|
|
82
|
+
* @param importEdges Set of "from\0to" keys representing import relationships
|
|
83
|
+
* @param topN Maximum number of pairs to return
|
|
84
|
+
* @param minCoChanges Minimum co-change count to consider
|
|
85
|
+
*/
|
|
86
|
+
export function findCoChangePairs(commits, importEdges, topN = 8, minCoChanges = 3) {
|
|
87
|
+
const { pairs, fileCounts } = buildCoOccurrenceMatrix(commits);
|
|
88
|
+
const results = [];
|
|
89
|
+
for (const [key, count] of pairs) {
|
|
90
|
+
if (count < minCoChanges)
|
|
91
|
+
continue;
|
|
92
|
+
const [fileA, fileB] = key.split("\0");
|
|
93
|
+
const totalA = fileCounts.get(fileA) ?? count;
|
|
94
|
+
const totalB = fileCounts.get(fileB) ?? count;
|
|
95
|
+
const confidence = count / Math.min(totalA, totalB);
|
|
96
|
+
// Check if there's a direct import relationship
|
|
97
|
+
const importConnected = importEdges.has(`${fileA}\0${fileB}`) ||
|
|
98
|
+
importEdges.has(`${fileB}\0${fileA}`);
|
|
99
|
+
results.push({
|
|
100
|
+
fileA,
|
|
101
|
+
fileB,
|
|
102
|
+
coChangeCount: count,
|
|
103
|
+
totalCommitsA: totalA,
|
|
104
|
+
totalCommitsB: totalB,
|
|
105
|
+
confidence,
|
|
106
|
+
importConnected,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
// Sort: prioritize unconnected pairs (surprising), then by confidence * count
|
|
110
|
+
results.sort((a, b) => {
|
|
111
|
+
// Unconnected pairs first (most surprising signal)
|
|
112
|
+
if (a.importConnected !== b.importConnected) {
|
|
113
|
+
return a.importConnected ? 1 : -1;
|
|
114
|
+
}
|
|
115
|
+
// Then by confidence * coChangeCount (frequency-weighted)
|
|
116
|
+
return b.confidence * b.coChangeCount - a.confidence * a.coChangeCount;
|
|
117
|
+
});
|
|
118
|
+
return results.slice(0, topN);
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Build a set of import edge keys from graph edges for import-proximity checking.
|
|
122
|
+
*/
|
|
123
|
+
export function buildImportEdgeSet(edges) {
|
|
124
|
+
const set = new Set();
|
|
125
|
+
for (const edge of edges) {
|
|
126
|
+
if (edge.type === "tests")
|
|
127
|
+
continue;
|
|
128
|
+
set.add(`${edge.from}\0${edge.to}`);
|
|
129
|
+
}
|
|
130
|
+
return set;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Shell out to git and compute co-change pairs for a repository.
|
|
134
|
+
* Returns empty array if not in a git repo or git is unavailable.
|
|
135
|
+
*/
|
|
136
|
+
export function computeCoChanges(rootDir, edges) {
|
|
137
|
+
try {
|
|
138
|
+
const raw = execSync(`git log --name-only --format="%h" --since="30 days ago"`, {
|
|
139
|
+
cwd: rootDir,
|
|
140
|
+
encoding: "utf-8",
|
|
141
|
+
maxBuffer: 10 * 1024 * 1024, // 10MB
|
|
142
|
+
timeout: 15000, // 15s
|
|
143
|
+
});
|
|
144
|
+
const commits = parseGitLogNameOnly(raw);
|
|
145
|
+
const importEdges = buildImportEdgeSet(edges);
|
|
146
|
+
return findCoChangePairs(commits, importEdges);
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
// Not a git repo or git unavailable — co-change is optional
|
|
150
|
+
return [];
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
//# sourceMappingURL=co-change.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"co-change.js","sourceRoot":"","sources":["../../src/analyzer/co-change.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Convention Detector — identifies import patterns repeated across files of the same type.
|
|
3
|
+
*
|
|
4
|
+
* A "convention" is a dependency imported by >= 60% of files with a given type
|
|
5
|
+
* (e.g., 8/12 API routes import Sentry). Minimum 3 files of that type required.
|
|
6
|
+
*/
|
|
7
|
+
import type { StrandNode, StrandEdge } from "../scanner/index.js";
|
|
8
|
+
export interface Convention {
|
|
9
|
+
anchorFile: string;
|
|
10
|
+
anchorExports: string[];
|
|
11
|
+
consumerType: string;
|
|
12
|
+
adoption: number;
|
|
13
|
+
total: number;
|
|
14
|
+
coverage: number;
|
|
15
|
+
violators: string[];
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Detect import conventions from graph data.
|
|
19
|
+
* Returns conventions sorted by coverage descending.
|
|
20
|
+
*/
|
|
21
|
+
export declare function detectConventions(nodes: StrandNode[], edges: StrandEdge[]): Convention[];
|
|
22
|
+
//# sourceMappingURL=conventions.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"conventions.d.ts","sourceRoot":"","sources":["../../src/analyzer/conventions.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAMlE,MAAM,WAAW,UAAU;IACzB,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,EAAE,CAAC;CACrB;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,UAAU,EAAE,EACnB,KAAK,EAAE,UAAU,EAAE,GAClB,UAAU,EAAE,CAsEd"}
|