@bigsteele/the-prospect 0.2.0 → 0.3.1
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 +40 -17
- package/dist/check.d.ts +20 -6
- package/dist/check.js +77 -23
- package/dist/cli.js +105 -27
- package/dist/detect/costs.js +48 -6
- package/dist/detect/database.js +30 -5
- package/dist/detect/deadweight.js +60 -15
- package/dist/detect/handrolled.js +23 -14
- package/dist/detect/stack.d.ts +2 -0
- package/dist/detect/types.d.ts +33 -0
- package/dist/detect/vendors.js +95 -2
- package/dist/index.d.ts +3 -1
- package/dist/index.js +24 -1
- package/dist/report.js +28 -9
- package/dist/score.js +9 -5
- package/dist/verdicts.d.ts +80 -0
- package/dist/verdicts.js +159 -0
- package/package.json +1 -1
- package/prompt/THE-PROSPECT.md +168 -119
- package/dist/detect/deps 2.d.ts +0 -25
- package/dist/detect/deps 2.js +0 -198
- package/dist/detect/types 2.d.ts +0 -106
- package/dist/detect/types 2.js +0 -11
- package/dist/walk 2.d.ts +0 -44
- package/dist/walk 2.js +0 -123
package/dist/detect/deps 2.js
DELETED
|
@@ -1,198 +0,0 @@
|
|
|
1
|
-
const CONFIG_FILE = /(^|\/)(tailwind|postcss|vite|next|nuxt|astro|svelte|webpack|rollup|babel|jest|vitest|playwright|eslint|prettier|tsup|drizzle|prisma)[^/]*\.(config\.)?(ts|js|mjs|cjs|json)$|(^|\/)\.(eslintrc|babelrc|prettierrc)(\.[a-z]+)?$|(^|\/)(package|turbo|nx|lerna)\.json$/i;
|
|
2
|
-
/** Nested manifests that describe fixtures or vendored copies, not this software. */
|
|
3
|
-
const NOT_A_MANIFEST = /(^|\/)(test|tests|fixtures?|__fixtures__|examples?|templates?)\//i;
|
|
4
|
-
/**
|
|
5
|
-
* Peers of the frameworks whose peers are used by the framework itself, for when
|
|
6
|
-
* node_modules is not installed and the real peerDependencies cannot be read.
|
|
7
|
-
*/
|
|
8
|
-
const FRAMEWORK_PEERS = {
|
|
9
|
-
next: ["react", "react-dom"],
|
|
10
|
-
gatsby: ["react", "react-dom"],
|
|
11
|
-
"@remix-run/react": ["react", "react-dom"],
|
|
12
|
-
expo: ["react", "react-native"],
|
|
13
|
-
"@monaco-editor/react": ["monaco-editor"],
|
|
14
|
-
"@tiptap/react": ["@tiptap/pm"],
|
|
15
|
-
"@tiptap/starter-kit": ["@tiptap/pm"],
|
|
16
|
-
};
|
|
17
|
-
const escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
18
|
-
function importSpecifiers(text) {
|
|
19
|
-
const out = [];
|
|
20
|
-
const patterns = [
|
|
21
|
-
/\bimport\s+(?:[^"'`]*?\s+from\s+)?["']([^"'\n]+)["']/g,
|
|
22
|
-
/\brequire\(\s*["']([^"'\n]+)["']\s*\)/g,
|
|
23
|
-
/\bimport\(\s*["']([^"'\n]+)["']\s*\)/g,
|
|
24
|
-
/\bexport\s+[^"'`\n]*?\s+from\s+["']([^"'\n]+)["']/g,
|
|
25
|
-
];
|
|
26
|
-
for (const re of patterns) {
|
|
27
|
-
for (const m of text.matchAll(re))
|
|
28
|
-
out.push(m[1]);
|
|
29
|
-
}
|
|
30
|
-
return out;
|
|
31
|
-
}
|
|
32
|
-
/** `@scope/pkg/deep/path` -> `@scope/pkg`; `pkg/deep` -> `pkg`. Relative and URL imports return null. */
|
|
33
|
-
export function packageOf(spec) {
|
|
34
|
-
if (spec.startsWith(".") || spec.startsWith("/") || /^(node:|https?:|npm:|jsr:)/.test(spec)) {
|
|
35
|
-
// Deno-style `npm:pkg@1` still names a package worth counting.
|
|
36
|
-
const npm = /^npm:(@?[^@/]+(?:\/[^@/]+)?)/.exec(spec);
|
|
37
|
-
return npm ? npm[1] : null;
|
|
38
|
-
}
|
|
39
|
-
const parts = spec.split("/");
|
|
40
|
-
return spec.startsWith("@") ? parts.slice(0, 2).join("/") : parts[0];
|
|
41
|
-
}
|
|
42
|
-
export async function detectDeps(repo) {
|
|
43
|
-
const manifests = repo.files.filter((f) => /(^|\/)package\.json$/.test(f) && !/node_modules/.test(f) && !NOT_A_MANIFEST.test(f));
|
|
44
|
-
const declared = [];
|
|
45
|
-
const scriptsByManifest = new Map();
|
|
46
|
-
for (const m of manifests) {
|
|
47
|
-
const text = await repo.read(m);
|
|
48
|
-
if (!text)
|
|
49
|
-
continue;
|
|
50
|
-
let json;
|
|
51
|
-
try {
|
|
52
|
-
json = JSON.parse(text);
|
|
53
|
-
}
|
|
54
|
-
catch {
|
|
55
|
-
continue;
|
|
56
|
-
}
|
|
57
|
-
scriptsByManifest.set(m, Object.values(json.scripts ?? {}).filter((v) => typeof v === "string"));
|
|
58
|
-
for (const [dev, block] of [[false, json.dependencies], [true, json.devDependencies]]) {
|
|
59
|
-
for (const [name, version] of Object.entries(block ?? {})) {
|
|
60
|
-
declared.push({ name, version, manifest: m, dev, imported_by: 0, config_mentions: 0, no_reference_found: false });
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
if (declared.length === 0)
|
|
65
|
-
return [];
|
|
66
|
-
const byName = new Map();
|
|
67
|
-
for (const d of declared) {
|
|
68
|
-
const list = byName.get(d.name) ?? [];
|
|
69
|
-
list.push(d);
|
|
70
|
-
byName.set(d.name, list);
|
|
71
|
-
}
|
|
72
|
-
// Imports: every code file, tests included - a dependency only tests use is
|
|
73
|
-
// still referenced, and saying otherwise would be the lie this file bans.
|
|
74
|
-
const codeFiles = repo.files.filter((f) => /\.(ts|tsx|js|jsx|mjs|cjs|css|scss|sass|less|pcss)$/i.test(f) && !/node_modules/.test(f));
|
|
75
|
-
for (const f of codeFiles) {
|
|
76
|
-
const text = await repo.read(f);
|
|
77
|
-
if (!text)
|
|
78
|
-
continue;
|
|
79
|
-
const seen = new Set();
|
|
80
|
-
for (const spec of importSpecifiers(text)) {
|
|
81
|
-
const pkg = packageOf(spec);
|
|
82
|
-
if (pkg && !seen.has(pkg)) {
|
|
83
|
-
seen.add(pkg);
|
|
84
|
-
for (const d of byName.get(pkg) ?? [])
|
|
85
|
-
d.imported_by++;
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
// Stylesheets: `@import "tw-animate-css"`, Tailwind 4's `@plugin "pkg"`, and
|
|
89
|
-
// Sass's `@use` / `@forward`.
|
|
90
|
-
if (/\.(css|scss|sass|less|pcss)$/i.test(f)) {
|
|
91
|
-
for (const m of text.matchAll(/@(?:import|plugin|use|forward)\s+(?:url\(\s*)?["']([^"'\n]+)["']/g)) {
|
|
92
|
-
const pkg = packageOf(m[1].replace(/^~/, ""));
|
|
93
|
-
if (pkg && !seen.has(pkg)) {
|
|
94
|
-
seen.add(pkg);
|
|
95
|
-
for (const d of byName.get(pkg) ?? [])
|
|
96
|
-
d.imported_by++;
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
// Config mentions: plugins named as bare strings, and npm scripts that
|
|
102
|
-
// invoke a package's bin by name.
|
|
103
|
-
const configs = repo.files.filter((f) => CONFIG_FILE.test(f) && !/node_modules/.test(f));
|
|
104
|
-
for (const f of configs) {
|
|
105
|
-
const text = await repo.read(f);
|
|
106
|
-
if (!text)
|
|
107
|
-
continue;
|
|
108
|
-
for (const [name, list] of byName) {
|
|
109
|
-
const bare = name.replace(/^@[^/]+\//, "");
|
|
110
|
-
if (text.includes(`"${name}"`) || text.includes(`'${name}'`) || new RegExp(`\\b${escapeRe(bare)}\\b`).test(text)) {
|
|
111
|
-
for (const d of list)
|
|
112
|
-
if (d.manifest !== f)
|
|
113
|
-
d.config_mentions++;
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
// ── Used without an import ────────────────────────────────────────────────
|
|
118
|
-
const referenced = (d) => d.imported_by > 0 || d.config_mentions > 0 || !!d.required_by;
|
|
119
|
-
const installedCache = new Map();
|
|
120
|
-
const installed = async (d) => {
|
|
121
|
-
const key = `${d.manifest}|${d.name}`;
|
|
122
|
-
if (!installedCache.has(key))
|
|
123
|
-
installedCache.set(key, await repo.installed(d.manifest, d.name));
|
|
124
|
-
return installedCache.get(key);
|
|
125
|
-
};
|
|
126
|
-
// Run by bin name from the declaring manifest's own scripts. The loop above
|
|
127
|
-
// skips the declaring manifest, because its dependency list names every
|
|
128
|
-
// package, so its scripts are read here instead. The bin is not always the
|
|
129
|
-
// package name: react-email installs `email`.
|
|
130
|
-
for (const d of declared) {
|
|
131
|
-
if (referenced(d))
|
|
132
|
-
continue;
|
|
133
|
-
const scripts = scriptsByManifest.get(d.manifest) ?? [];
|
|
134
|
-
if (!scripts.length)
|
|
135
|
-
continue;
|
|
136
|
-
const bins = new Set([d.name.replace(/^@[^/]+\//, "")]);
|
|
137
|
-
const inst = await installed(d);
|
|
138
|
-
if (inst?.bin && typeof inst.bin === "object")
|
|
139
|
-
for (const b of Object.keys(inst.bin))
|
|
140
|
-
bins.add(b);
|
|
141
|
-
const hit = scripts.find((cmd) => [...bins].some((b) => new RegExp(`(^|[\\s;&|(])${escapeRe(b)}(\\s|$)`).test(cmd)));
|
|
142
|
-
if (hit)
|
|
143
|
-
d.required_by = `npm script: ${hit.length > 60 ? `${hit.slice(0, 57)}...` : hit}`;
|
|
144
|
-
}
|
|
145
|
-
// The JSX runtime. Every .tsx/.jsx file compiles to an import of `react` (or
|
|
146
|
-
// whatever jsxImportSource names) that no source file spells out.
|
|
147
|
-
if (repo.files.some((f) => /\.(tsx|jsx)$/.test(f) && !/node_modules/.test(f))) {
|
|
148
|
-
let source = "react";
|
|
149
|
-
for (const tc of repo.files.filter((f) => /(^|\/)(tsconfig|jsconfig)[^/]*\.json$/.test(f) && !NOT_A_MANIFEST.test(f))) {
|
|
150
|
-
const named = /"jsxImportSource"\s*:\s*"([^"]+)"/.exec((await repo.read(tc)) ?? "")?.[1];
|
|
151
|
-
if (named) {
|
|
152
|
-
source = packageOf(named) ?? source;
|
|
153
|
-
break;
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
for (const d of byName.get(source) ?? [])
|
|
157
|
-
if (!referenced(d))
|
|
158
|
-
d.required_by = "JSX runtime";
|
|
159
|
-
}
|
|
160
|
-
// Capacitor's native build registers every installed plugin from package.json.
|
|
161
|
-
if (repo.files.some((f) => /(^|\/)capacitor\.config\.(ts|js|json)$/.test(f))) {
|
|
162
|
-
for (const d of declared) {
|
|
163
|
-
if (!referenced(d) && /^(@capacitor\/|@capacitor-community\/|cordova-plugin-)/.test(d.name)) {
|
|
164
|
-
d.required_by = "native build (Capacitor)";
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
// Peers of packages in use. Read from the installed manifest when there is one
|
|
169
|
-
// (optional peers excluded), from the short framework list when there is not.
|
|
170
|
-
// Repeated until nothing changes, because a peer can have peers.
|
|
171
|
-
for (let changed = true; changed;) {
|
|
172
|
-
changed = false;
|
|
173
|
-
for (const user of declared) {
|
|
174
|
-
if (!referenced(user))
|
|
175
|
-
continue;
|
|
176
|
-
const inst = await installed(user);
|
|
177
|
-
const peers = inst
|
|
178
|
-
? Object.keys(inst.peerDependencies ?? {}).filter((p) => !inst.peerDependenciesMeta?.[p]?.optional)
|
|
179
|
-
: (FRAMEWORK_PEERS[user.name] ?? []);
|
|
180
|
-
for (const peer of peers) {
|
|
181
|
-
for (const d of byName.get(peer) ?? []) {
|
|
182
|
-
// Resolvable from where the user is declared: the same manifest, or the root.
|
|
183
|
-
if (referenced(d) || (d.manifest !== user.manifest && d.manifest !== "package.json"))
|
|
184
|
-
continue;
|
|
185
|
-
d.required_by = `peer of ${user.name}`;
|
|
186
|
-
changed = true;
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
for (const d of declared) {
|
|
192
|
-
// Types-only and tooling-adjacent packages are referenced by the compiler,
|
|
193
|
-
// not by an import statement; they never earn the flag.
|
|
194
|
-
const toolingShaped = /^@types\//.test(d.name) || /(^|-)(cli|eslint|prettier|typescript|vitest|vite|tsx?|husky|lint-staged|concurrently|nodemon)($|-)/.test(d.name);
|
|
195
|
-
d.no_reference_found = !referenced(d) && !toolingShaped;
|
|
196
|
-
}
|
|
197
|
-
return declared.sort((a, b) => Number(b.no_reference_found) - Number(a.no_reference_found) || a.name.localeCompare(b.name));
|
|
198
|
-
}
|
package/dist/detect/types 2.d.ts
DELETED
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The fact shapes. Family rule: a field is either read from the repository or
|
|
3
|
-
* absent. Nothing in these types is an opinion; opinions belong to the agent
|
|
4
|
-
* protocol, and every one it forms must cite a field from here.
|
|
5
|
-
*
|
|
6
|
-
* The one register rule encoded structurally: findings carry the honest verb.
|
|
7
|
-
* A dependency is not "unused", it has "no reference found" - the difference
|
|
8
|
-
* is a CLI-only tool that ships to production anyway versus a lie in a report
|
|
9
|
-
* a founder pays attention to.
|
|
10
|
-
*/
|
|
11
|
-
/** One declared dependency and every place it was actually seen. */
|
|
12
|
-
export interface DepFact {
|
|
13
|
-
name: string;
|
|
14
|
-
version: string;
|
|
15
|
-
/** Which manifest declared it, repository-relative. */
|
|
16
|
-
manifest: string;
|
|
17
|
-
dev: boolean;
|
|
18
|
-
/** Files whose import/require statements name this package. */
|
|
19
|
-
imported_by: number;
|
|
20
|
-
/** Config files that name it as a string (tailwind plugins, postcss, eslint). */
|
|
21
|
-
config_mentions: number;
|
|
22
|
-
/**
|
|
23
|
-
* Why it counts as used without an import or config mention, when that is the
|
|
24
|
-
* case: "peer of next", "npm script: email dev", "JSX runtime", "native build
|
|
25
|
-
* (Capacitor)".
|
|
26
|
-
*/
|
|
27
|
-
required_by?: string;
|
|
28
|
-
/** True when no import, config mention, script, or package requiring it was found. */
|
|
29
|
-
no_reference_found: boolean;
|
|
30
|
-
}
|
|
31
|
-
export type VendorCategory = "ai" | "email" | "sms" | "payments" | "database" | "auth" | "storage" | "crm" | "analytics" | "monitoring" | "render" | "search" | "queue" | "maps" | "calendar" | "other";
|
|
32
|
-
/** One external service the code talks to, with the evidence. */
|
|
33
|
-
export interface VendorFact {
|
|
34
|
-
service: string;
|
|
35
|
-
category: VendorCategory;
|
|
36
|
-
/** How it was seen: an SDK import, an outbound URL, an env var NAME. Values never. */
|
|
37
|
-
evidence: Array<{
|
|
38
|
-
kind: "sdk" | "url" | "env";
|
|
39
|
-
what: string;
|
|
40
|
-
file: string;
|
|
41
|
-
}>;
|
|
42
|
-
call_sites: number;
|
|
43
|
-
}
|
|
44
|
-
/** Two or more services doing the same category of work. */
|
|
45
|
-
export interface OverlapFact {
|
|
46
|
-
category: VendorCategory;
|
|
47
|
-
services: string[];
|
|
48
|
-
call_sites: number;
|
|
49
|
-
}
|
|
50
|
-
export type RailCategory = "pdf" | "rate-limiting" | "email-templating" | "auth-session" | "queue-scheduler" | "search" | "payments-logic" | "webhook-plumbing" | "parsing-ocr";
|
|
51
|
-
/** A subsystem built by hand where the market sells a rail. */
|
|
52
|
-
export interface HandrolledFact {
|
|
53
|
-
rail: RailCategory;
|
|
54
|
-
files: string[];
|
|
55
|
-
loc: number;
|
|
56
|
-
/** high: the file's name and its contents agree. low: contents only. */
|
|
57
|
-
confidence: "high" | "low";
|
|
58
|
-
/** The one line of evidence that convinced the detector, with its file. */
|
|
59
|
-
signal: {
|
|
60
|
-
file: string;
|
|
61
|
-
line: string;
|
|
62
|
-
};
|
|
63
|
-
}
|
|
64
|
-
/** A cluster of near-identical code living in more than one file. */
|
|
65
|
-
export interface DuplicateFact {
|
|
66
|
-
files: string[];
|
|
67
|
-
/** Lines in the repeated block, after normalization. */
|
|
68
|
-
lines: number;
|
|
69
|
-
/** A header comment says the copy is deliberate ("copied from", "never imported"). */
|
|
70
|
-
deliberate: boolean;
|
|
71
|
-
/** First normalized line of the block, so a reader can find it. */
|
|
72
|
-
opens_with: string;
|
|
73
|
-
}
|
|
74
|
-
/** A runtime file no entrypoint reaches. */
|
|
75
|
-
export interface DeadFact {
|
|
76
|
-
file: string;
|
|
77
|
-
loc: number;
|
|
78
|
-
/** Why the detector believes nothing reaches it. */
|
|
79
|
-
note: string;
|
|
80
|
-
}
|
|
81
|
-
/** A call whose cost multiplies: per request, per row, or on a clock. */
|
|
82
|
-
export interface CostFact {
|
|
83
|
-
file: string;
|
|
84
|
-
shape: "per-request" | "per-row" | "per-schedule";
|
|
85
|
-
/** What is being called - a vendor host or an SDK call name. */
|
|
86
|
-
target: string;
|
|
87
|
-
/** The line that shows the shape (the loop, the handler, the interval). */
|
|
88
|
-
line: string;
|
|
89
|
-
}
|
|
90
|
-
/** Vocabulary the repository uses about its own domain, ranked by weight.
|
|
91
|
-
* The CLI never names the industry; it hands the agent the evidence. */
|
|
92
|
-
export interface Fingerprint {
|
|
93
|
-
terms: Array<{
|
|
94
|
-
term: string;
|
|
95
|
-
count: number;
|
|
96
|
-
sources: string[];
|
|
97
|
-
}>;
|
|
98
|
-
/** Where the terms came from: tables, routes, copy, manifest. */
|
|
99
|
-
note: string;
|
|
100
|
-
}
|
|
101
|
-
export interface NorthStar {
|
|
102
|
-
sentence: string | null;
|
|
103
|
-
source: "NORTH-STAR.md" | "planning" | "PRODUCT.md" | "heuristic" | "none";
|
|
104
|
-
confidence: "high" | "low" | "unknown";
|
|
105
|
-
note: string;
|
|
106
|
-
}
|
package/dist/detect/types 2.js
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The fact shapes. Family rule: a field is either read from the repository or
|
|
3
|
-
* absent. Nothing in these types is an opinion; opinions belong to the agent
|
|
4
|
-
* protocol, and every one it forms must cite a field from here.
|
|
5
|
-
*
|
|
6
|
-
* The one register rule encoded structurally: findings carry the honest verb.
|
|
7
|
-
* A dependency is not "unused", it has "no reference found" - the difference
|
|
8
|
-
* is a CLI-only tool that ships to production anyway versus a lie in a report
|
|
9
|
-
* a founder pays attention to.
|
|
10
|
-
*/
|
|
11
|
-
export {};
|
package/dist/walk 2.d.ts
DELETED
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
export interface Repo {
|
|
2
|
-
root: string;
|
|
3
|
-
name: string;
|
|
4
|
-
/** Every file considered, repository-relative with forward slashes. */
|
|
5
|
-
files: string[];
|
|
6
|
-
/** Text of a file, or null when it is not text, too large, an env file, or unreadable. */
|
|
7
|
-
read(rel: string): Promise<string | null>;
|
|
8
|
-
/** Files whose path matches. */
|
|
9
|
-
matching(re: RegExp): string[];
|
|
10
|
-
/** Directories skipped because they are a repository of their own or a copy of this one. */
|
|
11
|
-
skipped: string[];
|
|
12
|
-
/**
|
|
13
|
-
* The manifest of an INSTALLED package, looked up the way Node resolves it: from
|
|
14
|
-
* the directory of the manifest that declares it, up to the root. Null when it is
|
|
15
|
-
* not installed. node_modules is never walked; this reads one file on request.
|
|
16
|
-
*/
|
|
17
|
-
installed(fromManifest: string, pkg: string): Promise<InstalledManifest | null>;
|
|
18
|
-
}
|
|
19
|
-
export interface InstalledManifest {
|
|
20
|
-
bin?: string | Record<string, string>;
|
|
21
|
-
peerDependencies?: Record<string, string>;
|
|
22
|
-
peerDependenciesMeta?: Record<string, {
|
|
23
|
-
optional?: boolean;
|
|
24
|
-
}>;
|
|
25
|
-
}
|
|
26
|
-
export declare function openRepo(root: string): Promise<Repo>;
|
|
27
|
-
/** Files whose text matches; each hit carries the count of matches. Reads at most `limit` files. */
|
|
28
|
-
export declare function grep(repo: Repo, files: string[], re: RegExp, limit?: number): Promise<Array<{
|
|
29
|
-
file: string;
|
|
30
|
-
count: number;
|
|
31
|
-
}>>;
|
|
32
|
-
/** Code files only: what the detectors read for behaviour. */
|
|
33
|
-
export declare const CODE: RegExp;
|
|
34
|
-
/** Test files, which describe behaviour but do not run in production. */
|
|
35
|
-
export declare const TEST_FILE: RegExp;
|
|
36
|
-
/**
|
|
37
|
-
* Not the running software: documentation trees, fixtures, golden files, and this
|
|
38
|
-
* auditor's own package when it is audited from inside the repository that holds it.
|
|
39
|
-
* The first run counted the auditor's detector source as an MCP server, a Bedrock
|
|
40
|
-
* integration and a human gate in the host repository.
|
|
41
|
-
*/
|
|
42
|
-
export declare const NOT_RUNTIME: RegExp;
|
|
43
|
-
/** Files the detectors read for behaviour: code, in the runtime tree, not tests. */
|
|
44
|
-
export declare function runtimeCode(files: string[]): string[];
|
package/dist/walk 2.js
DELETED
|
@@ -1,123 +0,0 @@
|
|
|
1
|
-
// Copied from ai-audit/src/walk.ts, never imported, so the package stays independently
|
|
2
|
-
// deployable (family convention). The only place that touches the filesystem. Walks a repository, skips what is not the
|
|
3
|
-
// project's own code, reads text files under a size cap, and never opens an env file.
|
|
4
|
-
import { readdir, readFile, stat } from "node:fs/promises";
|
|
5
|
-
import { join, relative, sep } from "node:path";
|
|
6
|
-
const SKIP_DIRS = new Set(["node_modules", ".git", "dist", "build", "out", ".next", ".nuxt", ".svelte-kit", ".vercel", ".turbo", "coverage", "vendor", "dist.bak", ".cache", "__pycache__", ".venv", "venv", "target", "test-results", "playwright-report", ".chrome-debug", ".claude-browser"]);
|
|
7
|
-
// Stylesheets are read too: `@import "tw-animate-css"` and Tailwind 4's `@plugin`
|
|
8
|
-
// are how a whole class of packages is used, and a walker that listed .css files
|
|
9
|
-
// but never read them flagged every one of those packages as unreferenced.
|
|
10
|
-
const TEXT = /\.(ts|tsx|js|jsx|mjs|cjs|py|go|rs|rb|php|java|kt|swift|sql|json|jsonc|ya?ml|toml|md|mdx|txt|prisma|graphql|gql|env\.example|sh|css|scss|sass|less|pcss)$/i;
|
|
11
|
-
const ENV_FILE = /(^|\/)\.env(\.[a-z0-9_-]+)?$/i;
|
|
12
|
-
const MAX_BYTES = 512 * 1024;
|
|
13
|
-
const MAX_FILES = 25_000;
|
|
14
|
-
export async function openRepo(root) {
|
|
15
|
-
const files = [];
|
|
16
|
-
const skipped = [];
|
|
17
|
-
const rootName = root.replace(/[\\/]+$/, "").split(/[\\/]/).pop() ?? "";
|
|
18
|
-
const walk = async (dir, depth) => {
|
|
19
|
-
let entries;
|
|
20
|
-
try {
|
|
21
|
-
entries = await readdir(dir, { withFileTypes: true });
|
|
22
|
-
}
|
|
23
|
-
catch {
|
|
24
|
-
return;
|
|
25
|
-
}
|
|
26
|
-
const names = new Set(entries.map((e) => e.name));
|
|
27
|
-
// A directory below the root that is a repository of its own (its own .git), or a
|
|
28
|
-
// copy of this one (named like the root, with its own manifest), is not this
|
|
29
|
-
// software: Brokrr's audit counted a nested Brokrr/ twice and cited both.
|
|
30
|
-
if (depth > 0 && (names.has(".git") || (dir.split(/[\\/]/).pop() === rootName && names.has("package.json")))) {
|
|
31
|
-
skipped.push(relative(root, dir).split(sep).join("/"));
|
|
32
|
-
return;
|
|
33
|
-
}
|
|
34
|
-
for (const e of entries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
35
|
-
if (files.length >= MAX_FILES)
|
|
36
|
-
return;
|
|
37
|
-
const abs = join(dir, e.name);
|
|
38
|
-
if (e.isSymbolicLink())
|
|
39
|
-
continue;
|
|
40
|
-
if (e.isDirectory()) {
|
|
41
|
-
// dist-demo, build_old, out-web: build output under any suffix.
|
|
42
|
-
if (SKIP_DIRS.has(e.name) || /^(dist|build|out)([-_.][a-z0-9-]*)?$/i.test(e.name))
|
|
43
|
-
continue;
|
|
44
|
-
await walk(abs, depth + 1);
|
|
45
|
-
}
|
|
46
|
-
else if (e.isFile()) {
|
|
47
|
-
files.push(relative(root, abs).split(sep).join("/"));
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
};
|
|
51
|
-
await walk(root, 0);
|
|
52
|
-
const cache = new Map();
|
|
53
|
-
const name = root.replace(/[\\/]+$/, "").split(/[\\/]/).pop() ?? root;
|
|
54
|
-
return {
|
|
55
|
-
root,
|
|
56
|
-
name,
|
|
57
|
-
files,
|
|
58
|
-
skipped,
|
|
59
|
-
async read(rel) {
|
|
60
|
-
if (cache.has(rel))
|
|
61
|
-
return cache.get(rel);
|
|
62
|
-
let text = null;
|
|
63
|
-
if (TEXT.test(rel) && !ENV_FILE.test(rel)) {
|
|
64
|
-
try {
|
|
65
|
-
const s = await stat(join(root, rel));
|
|
66
|
-
if (s.size <= MAX_BYTES)
|
|
67
|
-
text = await readFile(join(root, rel), "utf8");
|
|
68
|
-
}
|
|
69
|
-
catch {
|
|
70
|
-
text = null;
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
cache.set(rel, text);
|
|
74
|
-
return text;
|
|
75
|
-
},
|
|
76
|
-
matching(re) {
|
|
77
|
-
return files.filter((f) => re.test(f));
|
|
78
|
-
},
|
|
79
|
-
async installed(fromManifest, pkg) {
|
|
80
|
-
const parts = fromManifest.split("/").slice(0, -1);
|
|
81
|
-
for (let i = parts.length; i >= 0; i--) {
|
|
82
|
-
const abs = join(root, ...parts.slice(0, i), "node_modules", ...pkg.split("/"), "package.json");
|
|
83
|
-
try {
|
|
84
|
-
return JSON.parse(await readFile(abs, "utf8"));
|
|
85
|
-
}
|
|
86
|
-
catch {
|
|
87
|
-
// not installed at this level
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
return null;
|
|
91
|
-
},
|
|
92
|
-
};
|
|
93
|
-
}
|
|
94
|
-
/** Files whose text matches; each hit carries the count of matches. Reads at most `limit` files. */
|
|
95
|
-
export async function grep(repo, files, re, limit = 4000) {
|
|
96
|
-
const out = [];
|
|
97
|
-
const flags = re.flags.includes("g") ? re.flags : re.flags + "g";
|
|
98
|
-
const global = new RegExp(re.source, flags);
|
|
99
|
-
for (const f of files.slice(0, limit)) {
|
|
100
|
-
const text = await repo.read(f);
|
|
101
|
-
if (!text)
|
|
102
|
-
continue;
|
|
103
|
-
const count = (text.match(global) ?? []).length;
|
|
104
|
-
if (count > 0)
|
|
105
|
-
out.push({ file: f, count });
|
|
106
|
-
}
|
|
107
|
-
return out;
|
|
108
|
-
}
|
|
109
|
-
/** Code files only: what the detectors read for behaviour. */
|
|
110
|
-
export const CODE = /\.(ts|tsx|js|jsx|mjs|cjs|py|go|rs|rb|php|java|kt|swift)$/i;
|
|
111
|
-
/** Test files, which describe behaviour but do not run in production. */
|
|
112
|
-
export const TEST_FILE = /(^|\.|_|\/)(test|spec|e2e)s?(\.|\/)|(^|\/)__tests__\//i;
|
|
113
|
-
/**
|
|
114
|
-
* Not the running software: documentation trees, fixtures, golden files, and this
|
|
115
|
-
* auditor's own package when it is audited from inside the repository that holds it.
|
|
116
|
-
* The first run counted the auditor's detector source as an MCP server, a Bedrock
|
|
117
|
-
* integration and a human gate in the host repository.
|
|
118
|
-
*/
|
|
119
|
-
export const NOT_RUNTIME = /(^|\/)(docs?|fixtures?|__fixtures__|__mocks__|golden|examples?|samples?|packages\/the-prospect)\/|(^|\/)\.(?!well-known\/)[^/]+\//i;
|
|
120
|
-
/** Files the detectors read for behaviour: code, in the runtime tree, not tests. */
|
|
121
|
-
export function runtimeCode(files) {
|
|
122
|
-
return files.filter((f) => CODE.test(f) && !TEST_FILE.test(f) && !NOT_RUNTIME.test(f));
|
|
123
|
-
}
|