@mneme-ai/xray 2.150.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/README.md +71 -0
- package/dist/battery/age.d.ts +3 -0
- package/dist/battery/age.d.ts.map +1 -0
- package/dist/battery/age.js +65 -0
- package/dist/battery/age.js.map +1 -0
- package/dist/battery/busfactor.d.ts +3 -0
- package/dist/battery/busfactor.d.ts.map +1 -0
- package/dist/battery/busfactor.js +92 -0
- package/dist/battery/busfactor.js.map +1 -0
- package/dist/battery/complexity.d.ts +3 -0
- package/dist/battery/complexity.d.ts.map +1 -0
- package/dist/battery/complexity.js +50 -0
- package/dist/battery/complexity.js.map +1 -0
- package/dist/battery/deps.d.ts +15 -0
- package/dist/battery/deps.d.ts.map +1 -0
- package/dist/battery/deps.js +107 -0
- package/dist/battery/deps.js.map +1 -0
- package/dist/battery/hotspots.d.ts +3 -0
- package/dist/battery/hotspots.d.ts.map +1 -0
- package/dist/battery/hotspots.js +61 -0
- package/dist/battery/hotspots.js.map +1 -0
- package/dist/battery/secrets.d.ts +3 -0
- package/dist/battery/secrets.d.ts.map +1 -0
- package/dist/battery/secrets.js +64 -0
- package/dist/battery/secrets.js.map +1 -0
- package/dist/bin.d.ts +3 -0
- package/dist/bin.d.ts.map +1 -0
- package/dist/bin.js +76 -0
- package/dist/bin.js.map +1 -0
- package/dist/clone.d.ts +13 -0
- package/dist/clone.d.ts.map +1 -0
- package/dist/clone.js +42 -0
- package/dist/clone.js.map +1 -0
- package/dist/cosmic.d.ts +35 -0
- package/dist/cosmic.d.ts.map +1 -0
- package/dist/cosmic.js +122 -0
- package/dist/cosmic.js.map +1 -0
- package/dist/engine.d.ts +8 -0
- package/dist/engine.d.ts.map +1 -0
- package/dist/engine.js +138 -0
- package/dist/engine.js.map +1 -0
- package/dist/gauntlet.d.ts +9 -0
- package/dist/gauntlet.d.ts.map +1 -0
- package/dist/gauntlet.js +47 -0
- package/dist/gauntlet.js.map +1 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +21 -0
- package/dist/index.js.map +1 -0
- package/dist/privacy.d.ts +12 -0
- package/dist/privacy.d.ts.map +1 -0
- package/dist/privacy.js +43 -0
- package/dist/privacy.js.map +1 -0
- package/dist/publish.d.ts +9 -0
- package/dist/publish.d.ts.map +1 -0
- package/dist/publish.js +28 -0
- package/dist/publish.js.map +1 -0
- package/dist/server.d.ts +29 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +482 -0
- package/dist/server.js.map +1 -0
- package/dist/sign.d.ts +7 -0
- package/dist/sign.d.ts.map +1 -0
- package/dist/sign.js +33 -0
- package/dist/sign.js.map +1 -0
- package/dist/types.d.ts +148 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +16 -0
- package/dist/types.js.map +1 -0
- package/dist/util.d.ts +21 -0
- package/dist/util.d.ts.map +1 -0
- package/dist/util.js +111 -0
- package/dist/util.js.map +1 -0
- package/package.json +55 -0
- package/public/card.js +45 -0
- package/public/cosmic.html +74 -0
- package/public/favicon.svg +1 -0
- package/public/index.html +294 -0
- package/public/report.html +76 -0
package/README.md
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# @mneme-ai/xray — Repo X-Ray
|
|
2
|
+
|
|
3
|
+
**Live: https://xray.mneme-ai.space** — paste any public repo, get a signed health X-Ray in seconds (no install).
|
|
4
|
+
|
|
5
|
+
A **signed, raw-free, deterministic X-Ray of any repo.** Paste a public git URL (or point the CLI at a local path) and get a graded report: dependency mortality, secret leaks, bus factor, vitality, and complexity hotspots.
|
|
6
|
+
|
|
7
|
+
Three guarantees, by construction:
|
|
8
|
+
|
|
9
|
+
1. **Accurate.** Every number comes from a deterministic `@mneme-ai/core` analyzer (git history · AST outline · npm registry metadata · regex secret scan). **No LLM guesses anything** — the same repo at the same commit always produces the same report.
|
|
10
|
+
2. **Private.** Public repos are shallow-cloned to a temp dir, analysed, and **deleted**. The report is **raw-free** — it carries only metrics, counts, line numbers, symbol names, and hashes, never a line of source. `xrayLeaksRaw()` proves it (gauntlet-enforced). Private repos never leave your machine: run the CLI locally.
|
|
11
|
+
3. **Verifiable.** The whole report is sealed with an **Ed25519 NOTARY receipt** any third party verifies **offline** with the embedded public key — no Mneme instance, no network, no shared secret.
|
|
12
|
+
|
|
13
|
+
## CLI (local / private repos — nothing uploaded)
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npx @mneme-ai/xray . # local folder (git OR not) — analysed in place
|
|
17
|
+
npx @mneme-ai/xray https://github.com/owner/repo # public repo
|
|
18
|
+
npx @mneme-ai/xray ./private-repo --publish \
|
|
19
|
+
--server https://xray.mneme-ai.space --token YOUR_KEY # send ONLY the signed, raw-free report
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
The CLI works on any local folder — including one that isn't a git repo (git
|
|
23
|
+
signals are simply skipped). Source never leaves your machine; `--publish` sends
|
|
24
|
+
only the raw-free, signed report to your private dashboard.
|
|
25
|
+
|
|
26
|
+
## Embed a badge
|
|
27
|
+
|
|
28
|
+
A signed, self-updating grade for any README — links back to the full report:
|
|
29
|
+
|
|
30
|
+
```md
|
|
31
|
+
[](https://xray.mneme-ai.space/r/<fingerprint>)
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Server (the "Lighthouse")
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
npm run -w @mneme-ai/xray serve # http://0.0.0.0:8787
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
| Endpoint | |
|
|
41
|
+
|---|---|
|
|
42
|
+
| `POST /api/xray` `{gitUrl}` | clone public repo → battery → raw-free gate → NOTARY seal → report |
|
|
43
|
+
| `POST /api/verify` `{signed}` | verify a report's receipt offline |
|
|
44
|
+
| `GET /api/board` | recent public X-Rays |
|
|
45
|
+
| `GET /api/health` | liveness |
|
|
46
|
+
| `GET /` | the clean white UI |
|
|
47
|
+
|
|
48
|
+
Env: `PORT` (8787) · `HOST` (0.0.0.0) · `XRAY_DATA_DIR` (./.xray-data).
|
|
49
|
+
|
|
50
|
+
## Deploy 24/7 on DigitalOcean
|
|
51
|
+
|
|
52
|
+
**One click (no command line)** — authorize GitHub, get a public `…ondigitalocean.app` URL:
|
|
53
|
+
|
|
54
|
+
[](https://cloud.digitalocean.com/apps/new?repo=https://github.com/patsa2561-art/mneme-ai/tree/main)
|
|
55
|
+
|
|
56
|
+
**App Platform (CLI):** `doctl apps create --spec packages/xray/.do/app.yaml` (auto-deploys on push to `main`).
|
|
57
|
+
|
|
58
|
+
**Droplet (durable board):**
|
|
59
|
+
```bash
|
|
60
|
+
docker build -f packages/xray/Dockerfile -t mneme-xray .
|
|
61
|
+
docker run -d --restart=always -p 80:8787 -v /srv/xray-data:/data mneme-xray
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Architecture — Lighthouse + Reactor
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
[ your machine: code ] --mneme-xray ./path--> raw-free signed report (private repos: never leaves)
|
|
68
|
+
[ public git URL ] --POST /api/xray-----> Lighthouse (DigitalOcean): clone → analyse → delete → sign
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
The server (Lighthouse) only ever holds raw-free, signed reports. The accurate engine (Reactor) runs the same `@mneme-ai/core` functions whether local or in the cloud.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"age.d.ts","sourceRoot":"","sources":["../../src/battery/age.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAoB5C,wBAAgB,UAAU,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,QAAQ,CAwClE"}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Age / vitality signal — computed directly from `git log` for accuracy.
|
|
3
|
+
*
|
|
4
|
+
* NOTE: we deliberately do NOT use funeral.collectEulogyStats here — as of
|
|
5
|
+
* v2.150 that helper reports bornAt==diedAt (lifespan 0 days) on this repo,
|
|
6
|
+
* a core bug to fix separately. Reading the first/last commit dates ourselves
|
|
7
|
+
* is exact and dependency-free.
|
|
8
|
+
*/
|
|
9
|
+
import { git, isGitRepo } from "../util.js";
|
|
10
|
+
const DAY_MS = 1000 * 60 * 60 * 24;
|
|
11
|
+
const NOT_GIT = {
|
|
12
|
+
bornAt: "", lastCommitAt: "", lifespan: "n/a", lifespanDays: 0,
|
|
13
|
+
totalCommits: 0, totalAuthors: 0, dormant: false, vitality: "active",
|
|
14
|
+
note: "Not a git repository — history/vitality signals unavailable (deps, secrets, complexity still analysed).",
|
|
15
|
+
};
|
|
16
|
+
function humanSpan(days) {
|
|
17
|
+
if (days < 1)
|
|
18
|
+
return "less than a day";
|
|
19
|
+
const years = Math.floor(days / 365);
|
|
20
|
+
const months = Math.floor((days % 365) / 30.44);
|
|
21
|
+
const parts = [];
|
|
22
|
+
if (years)
|
|
23
|
+
parts.push(`${years} year${years > 1 ? "s" : ""}`);
|
|
24
|
+
if (months)
|
|
25
|
+
parts.push(`${months} month${months > 1 ? "s" : ""}`);
|
|
26
|
+
if (!years && !months)
|
|
27
|
+
parts.push(`${Math.round(days)} day${Math.round(days) === 1 ? "" : "s"}`);
|
|
28
|
+
return parts.join(", ");
|
|
29
|
+
}
|
|
30
|
+
export function analyzeAge(repoPath, now) {
|
|
31
|
+
if (!isGitRepo(repoPath))
|
|
32
|
+
return NOT_GIT;
|
|
33
|
+
const first = git(repoPath, ["log", "--reverse", "--format=%aI", "--max-parents=0"]).split("\n")[0]?.trim()
|
|
34
|
+
|| git(repoPath, ["log", "--reverse", "--format=%aI"]).split("\n")[0]?.trim();
|
|
35
|
+
const last = git(repoPath, ["log", "-1", "--format=%aI"]).trim();
|
|
36
|
+
const totalCommits = parseInt(git(repoPath, ["rev-list", "--count", "HEAD"]).trim() || "0", 10);
|
|
37
|
+
const authors = new Set(git(repoPath, ["log", "--format=%ae"]).split("\n").map((s) => s.trim()).filter(Boolean)).size;
|
|
38
|
+
if (!first || !last || totalCommits === 0) {
|
|
39
|
+
return {
|
|
40
|
+
bornAt: "", lastCommitAt: "", lifespan: "unknown", lifespanDays: 0,
|
|
41
|
+
totalCommits: 0, totalAuthors: 0, dormant: true, vitality: "dormant",
|
|
42
|
+
note: "Could not read git history.",
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
const bornMs = Date.parse(first);
|
|
46
|
+
const lastMs = Date.parse(last);
|
|
47
|
+
const lifespanDays = Number.isFinite(bornMs) && Number.isFinite(lastMs) ? Math.max(0, (lastMs - bornMs) / DAY_MS) : 0;
|
|
48
|
+
const monthsSince = Number.isFinite(lastMs) ? (now - lastMs) / (DAY_MS * 30.44) : 999;
|
|
49
|
+
const archived = false; // we cannot know archive status from clone alone; report only what is provable
|
|
50
|
+
const vitality = monthsSince >= 12 ? "dormant" : monthsSince >= 4 ? "slowing" : "active";
|
|
51
|
+
return {
|
|
52
|
+
bornAt: first,
|
|
53
|
+
lastCommitAt: last,
|
|
54
|
+
lifespan: humanSpan(lifespanDays),
|
|
55
|
+
lifespanDays: Math.round(lifespanDays),
|
|
56
|
+
totalCommits,
|
|
57
|
+
totalAuthors: authors,
|
|
58
|
+
dormant: vitality === "dormant",
|
|
59
|
+
vitality: archived ? "archived" : vitality,
|
|
60
|
+
note: vitality === "active" ? "Actively maintained — recent commit activity."
|
|
61
|
+
: vitality === "slowing" ? "Commit cadence is slowing (no commit in 4+ months)."
|
|
62
|
+
: "Dormant — no commit in 12+ months.",
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
//# sourceMappingURL=age.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"age.js","sourceRoot":"","sources":["../../src/battery/age.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EAAE,GAAG,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAG5C,MAAM,MAAM,GAAG,IAAI,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,CAAC;AACnC,MAAM,OAAO,GAAa;IACxB,MAAM,EAAE,EAAE,EAAE,YAAY,EAAE,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE,YAAY,EAAE,CAAC;IAC9D,YAAY,EAAE,CAAC,EAAE,YAAY,EAAE,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,QAAQ;IACpE,IAAI,EAAE,yGAAyG;CAChH,CAAC;AAEF,SAAS,SAAS,CAAC,IAAY;IAC7B,IAAI,IAAI,GAAG,CAAC;QAAE,OAAO,iBAAiB,CAAC;IACvC,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,GAAG,GAAG,CAAC,CAAC;IACrC,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,IAAI,GAAG,GAAG,CAAC,GAAG,KAAK,CAAC,CAAC;IAChD,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,IAAI,KAAK;QAAE,KAAK,CAAC,IAAI,CAAC,GAAG,KAAK,QAAQ,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IAC9D,IAAI,MAAM;QAAE,KAAK,CAAC,IAAI,CAAC,GAAG,MAAM,SAAS,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IAClE,IAAI,CAAC,KAAK,IAAI,CAAC,MAAM;QAAE,KAAK,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC;IACjG,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAC1B,CAAC;AAED,MAAM,UAAU,UAAU,CAAC,QAAgB,EAAE,GAAW;IACtD,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC;QAAE,OAAO,OAAO,CAAC;IACzC,MAAM,KAAK,GAAG,GAAG,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,WAAW,EAAE,cAAc,EAAE,iBAAiB,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE;WACtG,GAAG,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,WAAW,EAAE,cAAc,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC;IAChF,MAAM,IAAI,GAAG,GAAG,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,IAAI,EAAE,cAAc,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;IACjE,MAAM,YAAY,GAAG,QAAQ,CAAC,GAAG,CAAC,QAAQ,EAAE,CAAC,UAAU,EAAE,SAAS,EAAE,MAAM,CAAC,CAAC,CAAC,IAAI,EAAE,IAAI,GAAG,EAAE,EAAE,CAAC,CAAC;IAChG,MAAM,OAAO,GAAG,IAAI,GAAG,CACrB,GAAG,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,cAAc,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CACxF,CAAC,IAAI,CAAC;IAEP,IAAI,CAAC,KAAK,IAAI,CAAC,IAAI,IAAI,YAAY,KAAK,CAAC,EAAE,CAAC;QAC1C,OAAO;YACL,MAAM,EAAE,EAAE,EAAE,YAAY,EAAE,EAAE,EAAE,QAAQ,EAAE,SAAS,EAAE,YAAY,EAAE,CAAC;YAClE,YAAY,EAAE,CAAC,EAAE,YAAY,EAAE,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,SAAS;YACpE,IAAI,EAAE,6BAA6B;SACpC,CAAC;IACJ,CAAC;IAED,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;IACjC,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAChC,MAAM,YAAY,GAAG,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,MAAM,GAAG,MAAM,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IACtH,MAAM,WAAW,GAAG,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,GAAG,MAAM,CAAC,GAAG,CAAC,MAAM,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC;IACtF,MAAM,QAAQ,GAAG,KAAK,CAAC,CAAC,+EAA+E;IACvG,MAAM,QAAQ,GACZ,WAAW,IAAI,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,WAAW,IAAI,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,QAAQ,CAAC;IAE1E,OAAO;QACL,MAAM,EAAE,KAAK;QACb,YAAY,EAAE,IAAI;QAClB,QAAQ,EAAE,SAAS,CAAC,YAAY,CAAC;QACjC,YAAY,EAAE,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC;QACtC,YAAY;QACZ,YAAY,EAAE,OAAO;QACrB,OAAO,EAAE,QAAQ,KAAK,SAAS;QAC/B,QAAQ,EAAE,QAAQ,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,QAAQ;QAC1C,IAAI,EACF,QAAQ,KAAK,QAAQ,CAAC,CAAC,CAAC,+CAA+C;YACvE,CAAC,CAAC,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,qDAAqD;gBAChF,CAAC,CAAC,oCAAoC;KACzC,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"busfactor.d.ts","sourceRoot":"","sources":["../../src/battery/busfactor.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAKlD,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,cAAc,CAsEjE"}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bus-factor signal — knowledge concentration from git authorship.
|
|
3
|
+
* Pure computation over `git log` (deterministic given repo state). For each
|
|
4
|
+
* file: the share of its commits held by its single top author. A file with
|
|
5
|
+
* one dominant author is fragile (knowledge dies if that person leaves).
|
|
6
|
+
*/
|
|
7
|
+
import { git, isGitRepo } from "../util.js";
|
|
8
|
+
const SKIP_DIR = /(^|\/)(node_modules|\.git|dist|build|vendor|\.next|coverage)(\/|$)/;
|
|
9
|
+
const DOMINANCE = 0.8; // a file is "single-owner" when its top author holds >= 80% of its commits
|
|
10
|
+
export function analyzeBusFactor(repoPath) {
|
|
11
|
+
if (!isGitRepo(repoPath))
|
|
12
|
+
return emptyBlock("Not a git repository — authorship/bus-factor signals unavailable.");
|
|
13
|
+
// One line per (commit, file): "<authorEmail>\t<file>". --no-renames keeps paths stable.
|
|
14
|
+
const raw = git(repoPath, [
|
|
15
|
+
"log", "--no-merges", "--pretty=format:C%H%x09%ae", "--name-only", "-n", "4000",
|
|
16
|
+
]);
|
|
17
|
+
if (!raw.trim()) {
|
|
18
|
+
return emptyBlock("No commit history available.");
|
|
19
|
+
}
|
|
20
|
+
const fileAuthors = new Map(); // file -> author -> commits
|
|
21
|
+
const authorCommits = new Map(); // author -> total commits
|
|
22
|
+
const allAuthors = new Set();
|
|
23
|
+
let curAuthor = "";
|
|
24
|
+
let totalCommits = 0;
|
|
25
|
+
for (const line of raw.split("\n")) {
|
|
26
|
+
if (line.startsWith("C")) {
|
|
27
|
+
const tab = line.indexOf("\t");
|
|
28
|
+
curAuthor = tab >= 0 ? line.slice(tab + 1).trim() : "";
|
|
29
|
+
if (curAuthor) {
|
|
30
|
+
allAuthors.add(curAuthor);
|
|
31
|
+
authorCommits.set(curAuthor, (authorCommits.get(curAuthor) ?? 0) + 1);
|
|
32
|
+
totalCommits++;
|
|
33
|
+
}
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
const file = line.trim();
|
|
37
|
+
if (!file || SKIP_DIR.test(file) || !curAuthor)
|
|
38
|
+
continue;
|
|
39
|
+
let m = fileAuthors.get(file);
|
|
40
|
+
if (!m) {
|
|
41
|
+
m = new Map();
|
|
42
|
+
fileAuthors.set(file, m);
|
|
43
|
+
}
|
|
44
|
+
m.set(curAuthor, (m.get(curAuthor) ?? 0) + 1);
|
|
45
|
+
}
|
|
46
|
+
if (totalCommits === 0)
|
|
47
|
+
return emptyBlock("No authored commits found.");
|
|
48
|
+
let singleOwner = 0;
|
|
49
|
+
const fragile = [];
|
|
50
|
+
for (const [file, m] of fileAuthors) {
|
|
51
|
+
let top = 0, sum = 0;
|
|
52
|
+
for (const c of m.values()) {
|
|
53
|
+
sum += c;
|
|
54
|
+
if (c > top)
|
|
55
|
+
top = c;
|
|
56
|
+
}
|
|
57
|
+
if (sum < 3)
|
|
58
|
+
continue; // ignore barely-touched files
|
|
59
|
+
const share = top / sum;
|
|
60
|
+
if (share >= DOMINANCE) {
|
|
61
|
+
singleOwner++;
|
|
62
|
+
fragile.push({ file, topAuthorShare: Math.round(share * 100) / 100, commits: sum });
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
fragile.sort((a, b) => b.commits - a.commits);
|
|
66
|
+
const consideredFiles = [...fileAuthors.values()].filter((m) => [...m.values()].reduce((s, c) => s + c, 0) >= 3).length;
|
|
67
|
+
const topContributor = Math.max(0, ...authorCommits.values());
|
|
68
|
+
const topShare = totalCommits > 0 ? topContributor / totalCommits : 0;
|
|
69
|
+
// bus factor ≈ how many top authors it takes to cover 50% of commits.
|
|
70
|
+
const sorted = [...authorCommits.values()].sort((a, b) => b - a);
|
|
71
|
+
let cum = 0, busFactor = 0;
|
|
72
|
+
for (const c of sorted) {
|
|
73
|
+
cum += c;
|
|
74
|
+
busFactor++;
|
|
75
|
+
if (cum >= totalCommits * 0.5)
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
authors: allAuthors.size,
|
|
80
|
+
singleOwnerFilePct: consideredFiles > 0 ? Math.round((singleOwner / consideredFiles) * 1000) / 10 : 0,
|
|
81
|
+
fragileFiles: fragile.slice(0, 15),
|
|
82
|
+
topContributorShare: Math.round(topShare * 1000) / 10,
|
|
83
|
+
busFactor,
|
|
84
|
+
note: busFactor <= 1
|
|
85
|
+
? "Bus factor 1 — a single person dominates this codebase. High key-person risk."
|
|
86
|
+
: `${allAuthors.size} authors; ${singleOwner} files are single-owner (>=80% one author).`,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
function emptyBlock(note) {
|
|
90
|
+
return { authors: 0, singleOwnerFilePct: 0, fragileFiles: [], topContributorShare: 0, busFactor: 0, note };
|
|
91
|
+
}
|
|
92
|
+
//# sourceMappingURL=busfactor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"busfactor.js","sourceRoot":"","sources":["../../src/battery/busfactor.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAAE,GAAG,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAG5C,MAAM,QAAQ,GAAG,oEAAoE,CAAC;AACtF,MAAM,SAAS,GAAG,GAAG,CAAC,CAAC,2EAA2E;AAElG,MAAM,UAAU,gBAAgB,CAAC,QAAgB;IAC/C,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC;QAAE,OAAO,UAAU,CAAC,mEAAmE,CAAC,CAAC;IACjH,yFAAyF;IACzF,MAAM,GAAG,GAAG,GAAG,CAAC,QAAQ,EAAE;QACxB,KAAK,EAAE,aAAa,EAAE,4BAA4B,EAAE,aAAa,EAAE,IAAI,EAAE,MAAM;KAChF,CAAC,CAAC;IACH,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,EAAE,CAAC;QAChB,OAAO,UAAU,CAAC,8BAA8B,CAAC,CAAC;IACpD,CAAC;IAED,MAAM,WAAW,GAAG,IAAI,GAAG,EAA+B,CAAC,CAAC,4BAA4B;IACxF,MAAM,aAAa,GAAG,IAAI,GAAG,EAAkB,CAAC,CAAC,0BAA0B;IAC3E,MAAM,UAAU,GAAG,IAAI,GAAG,EAAU,CAAC;IACrC,IAAI,SAAS,GAAG,EAAE,CAAC;IACnB,IAAI,YAAY,GAAG,CAAC,CAAC;IAErB,KAAK,MAAM,IAAI,IAAI,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;QACnC,IAAI,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;YACzB,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;YAC/B,SAAS,GAAG,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YACvD,IAAI,SAAS,EAAE,CAAC;gBACd,UAAU,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;gBAC1B,aAAa,CAAC,GAAG,CAAC,SAAS,EAAE,CAAC,aAAa,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;gBACtE,YAAY,EAAE,CAAC;YACjB,CAAC;YACD,SAAS;QACX,CAAC;QACD,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;QACzB,IAAI,CAAC,IAAI,IAAI,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS;YAAE,SAAS;QACzD,IAAI,CAAC,GAAG,WAAW,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QAC9B,IAAI,CAAC,CAAC,EAAE,CAAC;YAAC,CAAC,GAAG,IAAI,GAAG,EAAE,CAAC;YAAC,WAAW,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;QAAC,CAAC;QACpD,CAAC,CAAC,GAAG,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;IAChD,CAAC;IAED,IAAI,YAAY,KAAK,CAAC;QAAE,OAAO,UAAU,CAAC,4BAA4B,CAAC,CAAC;IAExE,IAAI,WAAW,GAAG,CAAC,CAAC;IACpB,MAAM,OAAO,GAAmC,EAAE,CAAC;IACnD,KAAK,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,WAAW,EAAE,CAAC;QACpC,IAAI,GAAG,GAAG,CAAC,EAAE,GAAG,GAAG,CAAC,CAAC;QACrB,KAAK,MAAM,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC;YAAC,GAAG,IAAI,CAAC,CAAC;YAAC,IAAI,CAAC,GAAG,GAAG;gBAAE,GAAG,GAAG,CAAC,CAAC;QAAC,CAAC;QAC/D,IAAI,GAAG,GAAG,CAAC;YAAE,SAAS,CAAC,8BAA8B;QACrD,MAAM,KAAK,GAAG,GAAG,GAAG,GAAG,CAAC;QACxB,IAAI,KAAK,IAAI,SAAS,EAAE,CAAC;YACvB,WAAW,EAAE,CAAC;YACd,OAAO,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,cAAc,EAAE,IAAI,CAAC,KAAK,CAAC,KAAK,GAAG,GAAG,CAAC,GAAG,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC,CAAC;QACtF,CAAC;IACH,CAAC;IACD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,GAAG,CAAC,CAAC,OAAO,CAAC,CAAC;IAE9C,MAAM,eAAe,GAAG,CAAC,GAAG,WAAW,CAAC,MAAM,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC;IACxH,MAAM,cAAc,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,GAAG,aAAa,CAAC,MAAM,EAAE,CAAC,CAAC;IAC9D,MAAM,QAAQ,GAAG,YAAY,GAAG,CAAC,CAAC,CAAC,CAAC,cAAc,GAAG,YAAY,CAAC,CAAC,CAAC,CAAC,CAAC;IAEtE,sEAAsE;IACtE,MAAM,MAAM,GAAG,CAAC,GAAG,aAAa,CAAC,MAAM,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;IACjE,IAAI,GAAG,GAAG,CAAC,EAAE,SAAS,GAAG,CAAC,CAAC;IAC3B,KAAK,MAAM,CAAC,IAAI,MAAM,EAAE,CAAC;QAAC,GAAG,IAAI,CAAC,CAAC;QAAC,SAAS,EAAE,CAAC;QAAC,IAAI,GAAG,IAAI,YAAY,GAAG,GAAG;YAAE,MAAM;IAAC,CAAC;IAExF,OAAO;QACL,OAAO,EAAE,UAAU,CAAC,IAAI;QACxB,kBAAkB,EAAE,eAAe,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,WAAW,GAAG,eAAe,CAAC,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,CAAC;QACrG,YAAY,EAAE,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC;QAClC,mBAAmB,EAAE,IAAI,CAAC,KAAK,CAAC,QAAQ,GAAG,IAAI,CAAC,GAAG,EAAE;QACrD,SAAS;QACT,IAAI,EACF,SAAS,IAAI,CAAC;YACZ,CAAC,CAAC,+EAA+E;YACjF,CAAC,CAAC,GAAG,UAAU,CAAC,IAAI,aAAa,WAAW,6CAA6C;KAC9F,CAAC;AACJ,CAAC;AAED,SAAS,UAAU,CAAC,IAAY;IAC9B,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,kBAAkB,EAAE,CAAC,EAAE,YAAY,EAAE,EAAE,EAAE,mBAAmB,EAAE,CAAC,EAAE,SAAS,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC;AAC7G,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"complexity.d.ts","sourceRoot":"","sources":["../../src/battery/complexity.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAKnD,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,eAAe,CAuCrF"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Complexity signal — wraps the real `outline.extractOutline` (deterministic
|
|
3
|
+
* AST-structural scan). Reports symbol counts, the longest functions (refactor
|
|
4
|
+
* hotspots), and max nesting depth. Symbol NAMES + signatures are structural
|
|
5
|
+
* metadata, not source bodies — bodies are never read into the report.
|
|
6
|
+
*/
|
|
7
|
+
import { outline } from "@mneme-ai/core";
|
|
8
|
+
import { listTextFiles, readText } from "../util.js";
|
|
9
|
+
const CODE_EXT = /\.(ts|tsx|js|jsx|mjs|cjs|py|go|rs|java|c|h|cpp|cc)$/i;
|
|
10
|
+
export function analyzeComplexity(repoPath, maxFiles) {
|
|
11
|
+
const { files } = listTextFiles(repoPath, maxFiles);
|
|
12
|
+
const hotspots = [];
|
|
13
|
+
let totalSymbols = 0;
|
|
14
|
+
let analysed = 0;
|
|
15
|
+
let maxDepth = 0;
|
|
16
|
+
for (const f of files) {
|
|
17
|
+
if (!CODE_EXT.test(f.rel))
|
|
18
|
+
continue;
|
|
19
|
+
const src = readText(f.abs);
|
|
20
|
+
if (!src)
|
|
21
|
+
continue;
|
|
22
|
+
let o;
|
|
23
|
+
try {
|
|
24
|
+
o = outline.extractOutline(src, { path: f.rel });
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
analysed++;
|
|
30
|
+
totalSymbols += o.symbolCount;
|
|
31
|
+
for (const sym of o.symbols) {
|
|
32
|
+
if (sym.depth > maxDepth)
|
|
33
|
+
maxDepth = sym.depth;
|
|
34
|
+
if (sym.kind === "function" || sym.kind === "method") {
|
|
35
|
+
hotspots.push({ file: f.rel, symbol: sym.name, bodyLines: sym.bodyLines, startLine: sym.startLine });
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
hotspots.sort((a, b) => b.bodyLines - a.bodyLines);
|
|
40
|
+
return {
|
|
41
|
+
filesAnalysed: analysed,
|
|
42
|
+
totalSymbols,
|
|
43
|
+
hotspots: hotspots.slice(0, 15),
|
|
44
|
+
maxDepth,
|
|
45
|
+
note: hotspots.length === 0
|
|
46
|
+
? "No code symbols extracted."
|
|
47
|
+
: `Largest function: ${hotspots[0].symbol} (${hotspots[0].bodyLines} lines). Long functions are the refactor hotspots.`,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
//# sourceMappingURL=complexity.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"complexity.js","sourceRoot":"","sources":["../../src/battery/complexity.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAC;AAEzC,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAErD,MAAM,QAAQ,GAAG,sDAAsD,CAAC;AAExE,MAAM,UAAU,iBAAiB,CAAC,QAAgB,EAAE,QAAgB;IAClE,MAAM,EAAE,KAAK,EAAE,GAAG,aAAa,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;IACpD,MAAM,QAAQ,GAAgC,EAAE,CAAC;IACjD,IAAI,YAAY,GAAG,CAAC,CAAC;IACrB,IAAI,QAAQ,GAAG,CAAC,CAAC;IACjB,IAAI,QAAQ,GAAG,CAAC,CAAC;IAEjB,KAAK,MAAM,CAAC,IAAI,KAAK,EAAE,CAAC;QACtB,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC;YAAE,SAAS;QACpC,MAAM,GAAG,GAAG,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;QAC5B,IAAI,CAAC,GAAG;YAAE,SAAS;QACnB,IAAI,CAA4C,CAAC;QACjD,IAAI,CAAC;YACH,CAAC,GAAG,OAAO,CAAC,cAAc,CAAC,GAAG,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC;QACnD,CAAC;QAAC,MAAM,CAAC;YACP,SAAS;QACX,CAAC;QACD,QAAQ,EAAE,CAAC;QACX,YAAY,IAAI,CAAC,CAAC,WAAW,CAAC;QAC9B,KAAK,MAAM,GAAG,IAAI,CAAC,CAAC,OAAO,EAAE,CAAC;YAC5B,IAAI,GAAG,CAAC,KAAK,GAAG,QAAQ;gBAAE,QAAQ,GAAG,GAAG,CAAC,KAAK,CAAC;YAC/C,IAAI,GAAG,CAAC,IAAI,KAAK,UAAU,IAAI,GAAG,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;gBACrD,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,CAAC,IAAI,EAAE,SAAS,EAAE,GAAG,CAAC,SAAS,EAAE,SAAS,EAAE,GAAG,CAAC,SAAS,EAAE,CAAC,CAAC;YACvG,CAAC;QACH,CAAC;IACH,CAAC;IAED,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,SAAS,GAAG,CAAC,CAAC,SAAS,CAAC,CAAC;IAEnD,OAAO;QACL,aAAa,EAAE,QAAQ;QACvB,YAAY;QACZ,QAAQ,EAAE,QAAQ,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC;QAC/B,QAAQ;QACR,IAAI,EACF,QAAQ,CAAC,MAAM,KAAK,CAAC;YACnB,CAAC,CAAC,4BAA4B;YAC9B,CAAC,CAAC,qBAAqB,QAAQ,CAAC,CAAC,CAAC,CAAC,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,SAAS,oDAAoD;KAC5H,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dependency-mortality signal — wraps the real `depMortality.predictMortality`.
|
|
3
|
+
* Reads the repo's package.json, fetches public npm registry metadata per dep
|
|
4
|
+
* (factual, not an LLM guess), and scores each. Metadata fetch is injectable
|
|
5
|
+
* for deterministic tests; the registry is the source of truth otherwise.
|
|
6
|
+
*/
|
|
7
|
+
import { depMortality } from "@mneme-ai/core";
|
|
8
|
+
import type { DepsBlock } from "../types.js";
|
|
9
|
+
type NpmMeta = Parameters<typeof depMortality.predictMortality>[0];
|
|
10
|
+
export type MetaFetcher = (pkg: string, now: number) => Promise<NpmMeta | null>;
|
|
11
|
+
/** Default: fetch the public npm registry document and derive mortality inputs. */
|
|
12
|
+
export declare const defaultFetcher: MetaFetcher;
|
|
13
|
+
export declare function analyzeDeps(repoPath: string, now: number, fetcher?: MetaFetcher): Promise<DepsBlock>;
|
|
14
|
+
export {};
|
|
15
|
+
//# sourceMappingURL=deps.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"deps.d.ts","sourceRoot":"","sources":["../../src/battery/deps.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAG9C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAE7C,KAAK,OAAO,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,gBAAgB,CAAC,CAAC,CAAC,CAAC,CAAC;AACnE,MAAM,MAAM,WAAW,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC,CAAC;AAIhF,mFAAmF;AACnF,eAAO,MAAM,cAAc,EAAE,WA4C5B,CAAC;AAcF,wBAAsB,WAAW,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,OAAO,GAAE,WAA4B,GAAG,OAAO,CAAC,SAAS,CAAC,CAsC1H"}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dependency-mortality signal — wraps the real `depMortality.predictMortality`.
|
|
3
|
+
* Reads the repo's package.json, fetches public npm registry metadata per dep
|
|
4
|
+
* (factual, not an LLM guess), and scores each. Metadata fetch is injectable
|
|
5
|
+
* for deterministic tests; the registry is the source of truth otherwise.
|
|
6
|
+
*/
|
|
7
|
+
import { depMortality } from "@mneme-ai/core";
|
|
8
|
+
import { readText } from "../util.js";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
const MONTH_MS = 1000 * 60 * 60 * 24 * 30.44;
|
|
11
|
+
/** Default: fetch the public npm registry document and derive mortality inputs. */
|
|
12
|
+
export const defaultFetcher = async (pkg, now) => {
|
|
13
|
+
try {
|
|
14
|
+
const res = await fetch(`https://registry.npmjs.org/${encodeURIComponent(pkg).replace("%40", "@")}`, {
|
|
15
|
+
headers: { accept: "application/vnd.npm.install-v1+json, application/json" },
|
|
16
|
+
});
|
|
17
|
+
if (!res.ok)
|
|
18
|
+
return null;
|
|
19
|
+
const doc = (await res.json());
|
|
20
|
+
const latest = doc["dist-tags"]?.latest;
|
|
21
|
+
const time = doc.time ?? {};
|
|
22
|
+
const latestAt = latest ? time[latest] : undefined;
|
|
23
|
+
const monthsSinceLatest = latestAt ? (now - Date.parse(latestAt)) / MONTH_MS : undefined;
|
|
24
|
+
// months since last feature (non-patch) release: walk versions newest→oldest
|
|
25
|
+
const versions = Object.keys(doc.versions ?? {});
|
|
26
|
+
let monthsSinceFeatureRelease;
|
|
27
|
+
if (latest) {
|
|
28
|
+
const [lMaj, lMin] = latest.split(".").map((n) => parseInt(n, 10));
|
|
29
|
+
let bestAt = 0;
|
|
30
|
+
for (const v of versions) {
|
|
31
|
+
const [maj, min, pat] = v.split(".").map((n) => parseInt(n, 10));
|
|
32
|
+
if (pat === 0 && (maj !== lMaj || min !== lMin || v === latest)) {
|
|
33
|
+
const t = Date.parse(time[v] ?? "");
|
|
34
|
+
if (Number.isFinite(t) && t > bestAt)
|
|
35
|
+
bestAt = t;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
if (bestAt > 0)
|
|
39
|
+
monthsSinceFeatureRelease = (now - bestAt) / MONTH_MS;
|
|
40
|
+
}
|
|
41
|
+
const deprecated = !!(latest && doc.versions?.[latest]?.deprecated);
|
|
42
|
+
return {
|
|
43
|
+
name: pkg,
|
|
44
|
+
latestPublishedAt: latestAt,
|
|
45
|
+
monthsSinceLatest,
|
|
46
|
+
monthsSinceFeatureRelease,
|
|
47
|
+
deprecated,
|
|
48
|
+
maintainerCount: Array.isArray(doc.maintainers) ? doc.maintainers.length : undefined,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
function depNames(repoPath) {
|
|
56
|
+
const raw = readText(join(repoPath, "package.json"));
|
|
57
|
+
if (!raw)
|
|
58
|
+
return [];
|
|
59
|
+
try {
|
|
60
|
+
const pkg = JSON.parse(raw);
|
|
61
|
+
const names = new Set([...Object.keys(pkg.dependencies ?? {}), ...Object.keys(pkg.devDependencies ?? {})]);
|
|
62
|
+
return [...names];
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return [];
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
export async function analyzeDeps(repoPath, now, fetcher = defaultFetcher) {
|
|
69
|
+
const names = depNames(repoPath);
|
|
70
|
+
const byBand = { thriving: 0, healthy: 0, watch: 0, moribund: 0, dead: 0 };
|
|
71
|
+
const atRisk = [];
|
|
72
|
+
if (names.length === 0) {
|
|
73
|
+
return { total: 0, byBand, atRisk, partial: false, note: "No package.json dependencies found (non-npm repo or no deps)." };
|
|
74
|
+
}
|
|
75
|
+
let partial = false;
|
|
76
|
+
// bounded concurrency to be a polite registry citizen
|
|
77
|
+
const LIMIT = 8;
|
|
78
|
+
for (let i = 0; i < names.length; i += LIMIT) {
|
|
79
|
+
const chunk = names.slice(i, i + LIMIT);
|
|
80
|
+
const metas = await Promise.all(chunk.map((n) => fetcher(n, now).catch(() => null)));
|
|
81
|
+
for (const meta of metas) {
|
|
82
|
+
if (!meta) {
|
|
83
|
+
partial = true;
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
const r = depMortality.predictMortality(meta);
|
|
87
|
+
byBand[r.band]++;
|
|
88
|
+
if (r.band === "watch" || r.band === "moribund" || r.band === "dead") {
|
|
89
|
+
atRisk.push({ name: r.package, band: r.band, probability18mo: Math.round(r.probability18mo * 100) / 100, successor: meta.knownSubstitute ?? null });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
atRisk.sort((a, b) => b.probability18mo - a.probability18mo);
|
|
94
|
+
const danger = byBand.moribund + byBand.dead;
|
|
95
|
+
return {
|
|
96
|
+
total: names.length,
|
|
97
|
+
byBand,
|
|
98
|
+
atRisk: atRisk.slice(0, 20),
|
|
99
|
+
partial,
|
|
100
|
+
note: danger > 0
|
|
101
|
+
? `${danger} depend<x>${danger === 1 ? "y is" : "ies are"}</x> dying (moribund/dead). Plan replacements.`.replace(/<x>|<\/x>/g, "")
|
|
102
|
+
: partial
|
|
103
|
+
? "Some packages could not be reached on the npm registry (counted as unknown)."
|
|
104
|
+
: "No dying dependencies detected.",
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
//# sourceMappingURL=deps.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"deps.js","sourceRoot":"","sources":["../../src/battery/deps.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hotspots.d.ts","sourceRoot":"","sources":["../../src/battery/hotspots.ts"],"names":[],"mappings":"AAmBA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAMjD,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,UAAU,SAAM,GAAG,aAAa,CAqC9F"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HOTSPOTS — behavioral code analysis (the research-grounded signal).
|
|
3
|
+
*
|
|
4
|
+
* Defects and maintenance cost don't spread evenly: they concentrate in files
|
|
5
|
+
* that are BOTH changed often AND large/complex. This is well established —
|
|
6
|
+
* code churn predicts defects (Nagappan & Ball, ICSE'05), and "hotspots" =
|
|
7
|
+
* change-frequency × complexity surface the highest-ROI refactoring targets
|
|
8
|
+
* (Tornhill, *Your Code as a Crime Scene*; D'Ambros & Lanza, evolutionary
|
|
9
|
+
* measures). We compute it 100% deterministically:
|
|
10
|
+
*
|
|
11
|
+
* change-frequency ← `git log --name-only` over a window (no blob fetch —
|
|
12
|
+
* works on a blobless clone; uses commit/tree metadata)
|
|
13
|
+
* complexity proxy ← current lines-of-code of the file (HEAD blob)
|
|
14
|
+
* hotspot score ← changeCount × loc, ranked
|
|
15
|
+
*
|
|
16
|
+
* The output answers "where do I refactor first?" — a question no secret scanner
|
|
17
|
+
* or dependency checker answers, and one a CTO actually pays for.
|
|
18
|
+
*/
|
|
19
|
+
import { git, readText, isGitRepo } from "../util.js";
|
|
20
|
+
import { join } from "node:path";
|
|
21
|
+
const SKIP_DIR = /(^|\/)(node_modules|\.git|dist|build|vendor|\.next|coverage|__pycache__|\.venv|target)(\/|$)/;
|
|
22
|
+
const CODE_EXT = /\.(ts|tsx|js|jsx|mjs|cjs|py|go|rs|java|c|h|cpp|cc|rb|php|cs|kt|swift|scala|vue|svelte)$/i;
|
|
23
|
+
export function analyzeHotspots(repoPath, now, windowDays = 365) {
|
|
24
|
+
if (!isGitRepo(repoPath)) {
|
|
25
|
+
return { windowDays, filesConsidered: 0, hotspots: [], note: "Not a git repository — hotspot history unavailable." };
|
|
26
|
+
}
|
|
27
|
+
const since = new Date(now - windowDays * 86_400_000).toISOString();
|
|
28
|
+
// empty pretty format → output is just the changed file paths, one per line,
|
|
29
|
+
// per commit. Counting occurrences = how many commits touched each file.
|
|
30
|
+
const raw = git(repoPath, ["log", "--since", since, "--no-merges", "--name-only", "--pretty=format:"]);
|
|
31
|
+
if (!raw.trim())
|
|
32
|
+
return { windowDays, filesConsidered: 0, hotspots: [], note: "No commit activity in the window." };
|
|
33
|
+
const changes = new Map();
|
|
34
|
+
for (const line of raw.split("\n")) {
|
|
35
|
+
const f = line.trim();
|
|
36
|
+
if (!f || SKIP_DIR.test(f) || !CODE_EXT.test(f))
|
|
37
|
+
continue;
|
|
38
|
+
changes.set(f, (changes.get(f) ?? 0) + 1);
|
|
39
|
+
}
|
|
40
|
+
if (changes.size === 0)
|
|
41
|
+
return { windowDays, filesConsidered: 0, hotspots: [], note: "No source-file changes in the window." };
|
|
42
|
+
// join change-frequency with current size (complexity proxy). Only read LOC
|
|
43
|
+
// for the most-changed files (bounded work).
|
|
44
|
+
const ranked = [...changes.entries()].sort((a, b) => b[1] - a[1]).slice(0, 80);
|
|
45
|
+
const rows = ranked.map(([file, changeCount]) => {
|
|
46
|
+
const txt = readText(join(repoPath, file));
|
|
47
|
+
const loc = txt ? txt.split("\n").length : 0;
|
|
48
|
+
return { file, changes: changeCount, loc, score: changeCount * loc };
|
|
49
|
+
}).filter((r) => r.loc > 0);
|
|
50
|
+
rows.sort((a, b) => b.score - a.score);
|
|
51
|
+
const top = rows[0];
|
|
52
|
+
return {
|
|
53
|
+
windowDays,
|
|
54
|
+
filesConsidered: changes.size,
|
|
55
|
+
hotspots: rows.slice(0, 15),
|
|
56
|
+
note: top
|
|
57
|
+
? `Hotspot: ${top.file} — changed ${top.changes}× and ${top.loc} lines. High churn × size = where defects and refactoring ROI concentrate (behavioral code analysis).`
|
|
58
|
+
: "No hotspots surfaced.",
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
//# sourceMappingURL=hotspots.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hotspots.js","sourceRoot":"","sources":["../../src/battery/hotspots.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"secrets.d.ts","sourceRoot":"","sources":["../../src/battery/secrets.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAMhD,wBAAgB,WAAW,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,YAAY,CAyC5E"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Secret-leak signal — wraps the real `egress.scanEgress` (deterministic regex).
|
|
3
|
+
* Reports kind + file + line ONLY; the secret value is never read into the report.
|
|
4
|
+
*
|
|
5
|
+
* PRECISION: a credential pattern in a TEST / FIXTURE / DOC / EXAMPLE file is
|
|
6
|
+
* almost always intentional sample data (especially in a security repo whose job
|
|
7
|
+
* is to detect secrets), NOT a real leak. We classify every hit by path and count
|
|
8
|
+
* ONLY production-code hits toward the headline + grade; test/fixture/doc hits are
|
|
9
|
+
* reported separately as `excludedTestHits` so the signal means "real leak risk",
|
|
10
|
+
* not "matched a pattern somewhere". Entropy detection stays OFF (source code is
|
|
11
|
+
* full of high-entropy tokens → noise).
|
|
12
|
+
*/
|
|
13
|
+
import { egress } from "@mneme-ai/core";
|
|
14
|
+
import { listTextFiles, readText } from "../util.js";
|
|
15
|
+
const NON_PROD = /(\.test\.|\.spec\.|[._-]fixtures?\b|__tests__|__fixtures__|__mocks__|(^|\/)(tests?|spec|fixtures?|examples?|samples?|mocks?|docs?|e2e|benchmarks?|bench)\/|\.stories\.|\.md$|\.mdx$|\.snap$|\.lock$|fixture)/i;
|
|
16
|
+
const isProd = (rel) => !NON_PROD.test(rel);
|
|
17
|
+
export function scanSecrets(repoPath, maxFiles) {
|
|
18
|
+
const { files } = listTextFiles(repoPath, maxFiles);
|
|
19
|
+
const byKind = {};
|
|
20
|
+
const hits = [];
|
|
21
|
+
let total = 0, excluded = 0, scanned = 0;
|
|
22
|
+
let worst = "ALLOW";
|
|
23
|
+
for (const f of files) {
|
|
24
|
+
const text = readText(f.abs);
|
|
25
|
+
if (!text)
|
|
26
|
+
continue;
|
|
27
|
+
scanned++;
|
|
28
|
+
const prod = isProd(f.rel);
|
|
29
|
+
const lines = text.split("\n");
|
|
30
|
+
for (let i = 0; i < lines.length; i++) {
|
|
31
|
+
const r = egress.scanEgress({ payload: lines[i], entropy: { enabled: false } });
|
|
32
|
+
if (r.findings.length === 0)
|
|
33
|
+
continue;
|
|
34
|
+
const n = r.findings.reduce((s, fnd) => s + fnd.count, 0);
|
|
35
|
+
if (!prod) {
|
|
36
|
+
excluded += n;
|
|
37
|
+
continue;
|
|
38
|
+
} // test/fixture/doc → not a leak
|
|
39
|
+
if (r.verdict === "BLOCK")
|
|
40
|
+
worst = "BLOCK";
|
|
41
|
+
else if (r.verdict === "REDACT" && worst === "ALLOW")
|
|
42
|
+
worst = "REDACT";
|
|
43
|
+
for (const finding of r.findings) {
|
|
44
|
+
byKind[finding.kind] = (byKind[finding.kind] ?? 0) + finding.count;
|
|
45
|
+
total += finding.count;
|
|
46
|
+
if (hits.length < 50)
|
|
47
|
+
hits.push({ kind: finding.kind, file: f.rel, line: i + 1 });
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
const tail = excluded > 0 ? ` (${excluded} more in test/fixture/doc files — excluded as intentional sample data)` : "";
|
|
52
|
+
return {
|
|
53
|
+
filesScanned: scanned,
|
|
54
|
+
totalFindings: total,
|
|
55
|
+
excludedTestHits: excluded,
|
|
56
|
+
byKind,
|
|
57
|
+
hits,
|
|
58
|
+
worstVerdict: worst,
|
|
59
|
+
note: total === 0
|
|
60
|
+
? `No credential patterns in production code${tail}.`
|
|
61
|
+
: `${total} credential-pattern match(es) in production code — review${tail}. Kind+file+line only; the value is never stored.`,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
//# sourceMappingURL=secrets.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"secrets.js","sourceRoot":"","sources":["../../src/battery/secrets.ts"],"names":[],"mappings":"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"}
|
package/dist/bin.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bin.d.ts","sourceRoot":"","sources":["../src/bin.ts"],"names":[],"mappings":""}
|