@bigsteele/the-prospect 0.1.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 +104 -0
- package/dist/check.d.ts +26 -0
- package/dist/check.js +77 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +132 -0
- package/dist/detect/costs.d.ts +14 -0
- package/dist/detect/costs.js +46 -0
- package/dist/detect/deadweight.d.ts +22 -0
- package/dist/detect/deadweight.js +137 -0
- package/dist/detect/deps.d.ts +25 -0
- package/dist/detect/deps.js +198 -0
- package/dist/detect/duplication.d.ts +17 -0
- package/dist/detect/duplication.js +79 -0
- package/dist/detect/fingerprint.d.ts +16 -0
- package/dist/detect/fingerprint.js +95 -0
- package/dist/detect/handrolled.d.ts +23 -0
- package/dist/detect/handrolled.js +112 -0
- package/dist/detect/stack.d.ts +66 -0
- package/dist/detect/stack.js +164 -0
- package/dist/detect/types.d.ts +106 -0
- package/dist/detect/types.js +11 -0
- package/dist/detect/vendors.d.ts +26 -0
- package/dist/detect/vendors.js +125 -0
- package/dist/index.d.ts +36 -0
- package/dist/index.js +93 -0
- package/dist/northstar.d.ts +11 -0
- package/dist/northstar.js +95 -0
- package/dist/report.d.ts +16 -0
- package/dist/report.js +166 -0
- package/dist/score.d.ts +47 -0
- package/dist/score.js +93 -0
- package/dist/walk.d.ts +44 -0
- package/dist/walk.js +123 -0
- package/package.json +49 -0
- package/prompt/THE-PROSPECT.md +125 -0
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// Root-anchored on purpose: a NORTH-STAR.md inside a fixture, a template, or
|
|
2
|
+
// a vendored package is someone else's mission. The first host-repo run read
|
|
3
|
+
// this package's own lean fixture as the host's North Star, which is the
|
|
4
|
+
// exact self-contamination bug the family review caught in wiremap.
|
|
5
|
+
const UNKNOWN_NOTE = "No North Star found. The protocol derives one with the operator before any suggestion is written; without it, neither lane can tell overbuilt from essential.";
|
|
6
|
+
function section(md, heading) {
|
|
7
|
+
const lines = md.split("\n");
|
|
8
|
+
const at = lines.findIndex((l) => /^#{1,4}\s/.test(l) && heading.test(l));
|
|
9
|
+
if (at < 0)
|
|
10
|
+
return null;
|
|
11
|
+
const body = [];
|
|
12
|
+
for (const l of lines.slice(at + 1)) {
|
|
13
|
+
if (/^#{1,4}\s/.test(l))
|
|
14
|
+
break;
|
|
15
|
+
if (l.trim())
|
|
16
|
+
body.push(l.trim());
|
|
17
|
+
}
|
|
18
|
+
return body.length ? body.join(" ") : null;
|
|
19
|
+
}
|
|
20
|
+
const clean = (s) => s.replace(/\*\*/g, "").replace(/^[-*]\s*/, "").trim();
|
|
21
|
+
/** A README whose opening is the generator's, not the product's. */
|
|
22
|
+
const SCAFFOLD_README = /welcome to your \w+ project|## project info|\*\*URL\*\*: https:\/\/(lovable|bolt|v0)\.|this project (was )?(bootstrapped|generated) with|## getting started[\s\S]{0,200}(npm run dev|yarn dev|pnpm dev)/i;
|
|
23
|
+
const SCAFFOLD = /\b(lovable|bolt\.new|v0\.dev|start prompting|create[- ]react[- ]app|vite|getting started|npm (run|install)|yarn (dev|install)|pnpm (dev|install)|this (template|starter|boilerplate|scaffold)|bootstrapped with|generated (by|with)|quick ?start|installation|prerequisites)\b/i;
|
|
24
|
+
export async function readNorthStar(repo) {
|
|
25
|
+
const nsFile = repo.files.find((f) => /^NORTH-STAR\.md$/i.test(f));
|
|
26
|
+
if (nsFile) {
|
|
27
|
+
const md = await repo.read(nsFile);
|
|
28
|
+
if (md) {
|
|
29
|
+
const sentence = section(md, /one sentence/i) ?? section(md, /north star/i);
|
|
30
|
+
return {
|
|
31
|
+
sentence: sentence ? clean(sentence).slice(0, 400) : null,
|
|
32
|
+
source: "NORTH-STAR.md",
|
|
33
|
+
confidence: sentence ? "high" : "low",
|
|
34
|
+
note: sentence ? "" : "NORTH-STAR.md exists but states no one-sentence purpose.",
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
const planFile = repo.files.find((f) => /^\.planning\/[^/]*north-star[^/]*\.md$/i.test(f));
|
|
39
|
+
if (planFile) {
|
|
40
|
+
const md = await repo.read(planFile);
|
|
41
|
+
const sentence = md ? section(md, /one sentence/i) ?? section(md, /north star/i) : null;
|
|
42
|
+
if (sentence) {
|
|
43
|
+
return { sentence: clean(sentence).slice(0, 400), source: "planning", confidence: "high", note: "" };
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
const productFile = repo.files.find((f) => /^PRODUCT\.md$/i.test(f));
|
|
47
|
+
if (productFile) {
|
|
48
|
+
const md = await repo.read(productFile);
|
|
49
|
+
const purpose = md ? section(md, /product purpose/i) ?? section(md, /purpose/i) : null;
|
|
50
|
+
if (purpose) {
|
|
51
|
+
return { sentence: clean(purpose).slice(0, 400), source: "PRODUCT.md", confidence: "high", note: "" };
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
const readme = repo.files.find((f) => /^README\.md$/i.test(f));
|
|
55
|
+
if (readme) {
|
|
56
|
+
const md = await repo.read(readme);
|
|
57
|
+
if (md && SCAFFOLD_README.test(md.slice(0, 600))) {
|
|
58
|
+
return {
|
|
59
|
+
sentence: null,
|
|
60
|
+
source: "none",
|
|
61
|
+
confidence: "unknown",
|
|
62
|
+
note: `The README is still the scaffold's own, so it states no mission to read. ${UNKNOWN_NOTE}`,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
if (md) {
|
|
66
|
+
const para = md.split("\n").find((l) => {
|
|
67
|
+
const x = l.trim();
|
|
68
|
+
if (x.length <= 60)
|
|
69
|
+
return false;
|
|
70
|
+
if (/^[#>|\-![]/.test(x) || /^<!--/.test(x) || /^<[a-z]/i.test(x) || /^(```|:::)/.test(x))
|
|
71
|
+
return false;
|
|
72
|
+
const prose = x
|
|
73
|
+
.replace(/\[([^\]]*)\]\([^)]*\)/g, "$1")
|
|
74
|
+
.replace(/\bhttps?:\/\/\S+/gi, " ")
|
|
75
|
+
.replace(/`[^`]*`/g, " ")
|
|
76
|
+
.replace(/\*\*/g, "")
|
|
77
|
+
.trim();
|
|
78
|
+
if (prose.replace(/^\s*[A-Za-z ]{1,20}:\s*/, "").trim().length < 40)
|
|
79
|
+
return false;
|
|
80
|
+
if (prose.split(/\s+/).length < 8)
|
|
81
|
+
return false;
|
|
82
|
+
return !SCAFFOLD.test(prose);
|
|
83
|
+
});
|
|
84
|
+
if (para) {
|
|
85
|
+
return {
|
|
86
|
+
sentence: clean(para).slice(0, 300),
|
|
87
|
+
source: "heuristic",
|
|
88
|
+
confidence: "low",
|
|
89
|
+
note: "Read from the README's first paragraph, not from a stated North Star.",
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return { sentence: null, source: "none", confidence: "unknown", note: UNKNOWN_NOTE };
|
|
95
|
+
}
|
package/dist/report.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Step 0 report: the deterministic half, written for a founder.
|
|
3
|
+
*
|
|
4
|
+
* Every line here is READ - a fact from the repository. The RESEARCHED
|
|
5
|
+
* lines (the industry half, Lane 2) do not exist yet; the protocol adds
|
|
6
|
+
* them, and the report says so plainly instead of leaving a gap a reader
|
|
7
|
+
* would mistake for "nothing found".
|
|
8
|
+
*
|
|
9
|
+
* The refusal branch is the most important part of the file, family
|
|
10
|
+
* lesson: a scan that found nothing must say the scan could not see, never
|
|
11
|
+
* "you pass". An empty vendor roster on a repo with 90 dependencies means
|
|
12
|
+
* the detector missed, not that the founder pays no one.
|
|
13
|
+
*/
|
|
14
|
+
import type { Prospect } from "./index.js";
|
|
15
|
+
export declare function secretShaped(p: Prospect): string | null;
|
|
16
|
+
export declare function toMarkdown(p: Prospect): string;
|
package/dist/report.js
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
const CATEGORY_LABEL = {
|
|
2
|
+
ai: "AI", email: "email", sms: "SMS", payments: "payments", database: "database",
|
|
3
|
+
auth: "auth", storage: "storage", crm: "CRM", analytics: "analytics",
|
|
4
|
+
monitoring: "monitoring", render: "rendering", search: "search", queue: "queues",
|
|
5
|
+
maps: "maps", calendar: "calendar", other: "other",
|
|
6
|
+
};
|
|
7
|
+
const label = (c) => CATEGORY_LABEL[c] ?? c;
|
|
8
|
+
const prose = (items) => items.length <= 1 ? items.join("") : `${items.slice(0, -1).join(", ")} and ${items[items.length - 1]}`;
|
|
9
|
+
const SECRET = /\b(sk-[A-Za-z0-9_-]{20,}|sk_(live|test)_[A-Za-z0-9]{16,}|AKIA[0-9A-Z]{16}|gh[pousr]_[A-Za-z0-9]{30,}|github_pat_[A-Za-z0-9_]{20,}|xox[baprs]-[A-Za-z0-9-]{10,}|eyJ[A-Za-z0-9_-]{30,}\.[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{10,}|AIza[0-9A-Za-z_-]{30,}|pit-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}|-----BEGIN [A-Z ]*PRIVATE KEY-----)/;
|
|
10
|
+
export function secretShaped(p) {
|
|
11
|
+
const m = SECRET.exec(JSON.stringify(p));
|
|
12
|
+
return m ? `${m[1].slice(0, 6)}…` : null;
|
|
13
|
+
}
|
|
14
|
+
export function toMarkdown(p) {
|
|
15
|
+
const L = [];
|
|
16
|
+
const noRef = p.deps.filter((d) => !d.dev && d.no_reference_found);
|
|
17
|
+
const accidental = p.duplicates.filter((d) => !d.deliberate);
|
|
18
|
+
const highHand = p.handrolled.filter((h) => h.confidence === "high");
|
|
19
|
+
const cuts = p.stack.consolidations;
|
|
20
|
+
L.push(`# The Prospect: ${p.app}`, "");
|
|
21
|
+
// The headline is an argument, not a metric.
|
|
22
|
+
const claims = [];
|
|
23
|
+
if (noRef.length)
|
|
24
|
+
claims.push(`**${noRef.length} of ${p.totals.runtime_deps}** dependencies show no reference anywhere`);
|
|
25
|
+
if (p.overlaps.length)
|
|
26
|
+
claims.push(`**${p.overlaps.length}** job${p.overlaps.length > 1 ? "s are" : " is"} paid for twice`);
|
|
27
|
+
if (highHand.length)
|
|
28
|
+
claims.push(`**${highHand.length}** subsystem${highHand.length > 1 ? "s" : ""} built by hand where the market sells a rail`);
|
|
29
|
+
if (cuts.length)
|
|
30
|
+
claims.push(`**${cuts.length}** platform${cuts.length > 1 ? "s" : ""} another platform you already run can cover`);
|
|
31
|
+
if (p.dead.length)
|
|
32
|
+
claims.push(`**${p.dead.length}** files no entrypoint reaches`);
|
|
33
|
+
const sawLittle = claims.length === 0 && accidental.length === 0 && p.cost_surfaces.length === 0;
|
|
34
|
+
if (sawLittle) {
|
|
35
|
+
// The refusal branch. Silence is not a pass.
|
|
36
|
+
L.push(`The scan read ${p.totals.files} files and could not build a case either way. That is a`, `finding about the scan, not a clean bill: the repository may be too small, written in a`, `language the detectors do not read well, or genuinely tight. The market half below is`, `still worth running - a lean codebase can still be missing what its industry now ships.`, "");
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
L.push(claims.slice(0, 2).join(", and ") + ".", "");
|
|
40
|
+
}
|
|
41
|
+
L.push(`## Today / After`, "");
|
|
42
|
+
L.push(`| Today | After |`);
|
|
43
|
+
L.push(`| --- | --- |`);
|
|
44
|
+
if (noRef.length)
|
|
45
|
+
L.push(`| ${noRef.length} packages installed, updated and audited that nothing imports | They are gone, and every install, update and security audit is smaller |`);
|
|
46
|
+
if (p.overlaps.length) {
|
|
47
|
+
const o = p.overlaps[0];
|
|
48
|
+
L.push(`| ${prose(o.services)} ${o.services.length > 2 ? "all" : "both"} do ${label(o.category)} work | One vendor does it, one invoice, one integration to maintain |`);
|
|
49
|
+
}
|
|
50
|
+
if (highHand.length) {
|
|
51
|
+
const h = highHand[0];
|
|
52
|
+
L.push(`| ${h.loc} lines of hand-rolled ${h.rail} code you maintain alone | A decision on record: keep it on purpose, or a rail carries it |`);
|
|
53
|
+
}
|
|
54
|
+
if (cuts.length) {
|
|
55
|
+
const c = cuts[0];
|
|
56
|
+
L.push(`| ${c.candidate} runs beside ${c.keep}, which already covers ${c.covers.split(" (")[0]} | One platform, one bill, one place a deploy can fail |`);
|
|
57
|
+
}
|
|
58
|
+
if (p.dead.length)
|
|
59
|
+
L.push(`| ${p.dead.length} files everyone reads, searches and ships but nothing runs | Deleted, with this report as the receipt |`);
|
|
60
|
+
L.push(`| Suggestions arrive as opinions | Every suggestion stands on a fact in your code, a sourced fact from your market, and your North Star |`);
|
|
61
|
+
L.push("");
|
|
62
|
+
L.push(`## Score`, "");
|
|
63
|
+
L.push(`**${p.score.total}/100 (${p.score.grade})** - Level ${p.score.level.n}: **${p.score.level.name}**. ${p.score.level.meaning}`, "");
|
|
64
|
+
for (const f of p.score.floors)
|
|
65
|
+
L.push(`- ${f}`);
|
|
66
|
+
if (p.score.floors.length)
|
|
67
|
+
L.push("");
|
|
68
|
+
if (p.score.deductions.length) {
|
|
69
|
+
L.push(`| What cost points | Points | Evidence |`);
|
|
70
|
+
L.push(`| --- | --- | --- |`);
|
|
71
|
+
for (const d of p.score.deductions)
|
|
72
|
+
L.push(`| ${d.what} | ${d.points} | ${d.evidence} |`);
|
|
73
|
+
L.push("");
|
|
74
|
+
}
|
|
75
|
+
L.push(`## North Star`, "");
|
|
76
|
+
if (p.north_star.sentence) {
|
|
77
|
+
L.push(`> ${p.north_star.sentence}`, "");
|
|
78
|
+
L.push(`Read from ${p.north_star.source} (confidence: ${p.north_star.confidence}). ${p.north_star.note}`.trim(), "");
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
L.push(p.north_star.note, "");
|
|
82
|
+
}
|
|
83
|
+
L.push(`## Lane 1 - what to subtract [READ]`, "");
|
|
84
|
+
L.push(`Every line in this lane was read from the repository. Nothing here is a guess.`, "");
|
|
85
|
+
if (noRef.length) {
|
|
86
|
+
L.push(`### Dependencies with no reference found`, "");
|
|
87
|
+
L.push(`| Package | Declared in | Looked for |`);
|
|
88
|
+
L.push(`| --- | --- | --- |`);
|
|
89
|
+
for (const d of noRef.slice(0, 25))
|
|
90
|
+
L.push(`| ${d.name} | ${d.manifest} | imports in every code file, names in every config: none found |`);
|
|
91
|
+
if (noRef.length > 25)
|
|
92
|
+
L.push(`| …and ${noRef.length - 25} more in the JSON | | |`);
|
|
93
|
+
L.push("");
|
|
94
|
+
L.push(`A package can earn its keep without an import (a CLI run from a script, a plugin loaded by name at runtime). This table is the list to answer for, not a kill list.`, "");
|
|
95
|
+
}
|
|
96
|
+
if (p.overlaps.length) {
|
|
97
|
+
L.push(`### Jobs paid for twice`, "");
|
|
98
|
+
for (const o of p.overlaps) {
|
|
99
|
+
L.push(`- **${label(o.category)}**: ${o.services.join(" + ")} (${o.call_sites} call sites). One category of work, ${o.services.length} vendors, ${o.services.length} invoices, ${o.services.length} places a key can leak.`);
|
|
100
|
+
}
|
|
101
|
+
L.push("");
|
|
102
|
+
}
|
|
103
|
+
if (p.handrolled.length) {
|
|
104
|
+
L.push(`### Built by hand where the market sells a rail`, "");
|
|
105
|
+
L.push(`| Subsystem | Size | Confidence | The line that shows it |`);
|
|
106
|
+
L.push(`| --- | --- | --- | --- |`);
|
|
107
|
+
for (const h of p.handrolled)
|
|
108
|
+
L.push(`| ${h.rail} (${h.files.length} file${h.files.length > 1 ? "s" : ""}) | ${h.loc} lines | ${h.confidence} | \`${h.signal.file}:${h.signal.line.replace(/\|/g, "\\|")}\` |`);
|
|
109
|
+
L.push("");
|
|
110
|
+
L.push(`Hand-rolled is not wrong. Half of these are the right call - control, cost, no vendor risk. The protocol researches the actual rails and puts the trade in front of you as a question, never a verdict.`, "");
|
|
111
|
+
}
|
|
112
|
+
if (cuts.length) {
|
|
113
|
+
L.push(`### The stack cut: platforms another platform already covers`, "");
|
|
114
|
+
L.push(`Both sides of every pair below were read from your config files. Whether the kept platform's current plan truly carries the load is what the research half verifies, with a source.`, "");
|
|
115
|
+
for (const c of cuts) {
|
|
116
|
+
L.push(`- **Keep ${c.keep}, question ${c.candidate}.** ${c.keep} covers ${c.covers}. Evidence: \`${c.evidence_keep}\` and \`${c.evidence_candidate}\`. ${c.note}`);
|
|
117
|
+
}
|
|
118
|
+
L.push("");
|
|
119
|
+
const so = p.stack.workflows.filter((w) => w.script_only);
|
|
120
|
+
if (so.length) {
|
|
121
|
+
L.push(`| Workflow | Triggers | Every step is |`);
|
|
122
|
+
L.push(`| --- | --- | --- |`);
|
|
123
|
+
for (const w of so.slice(0, 8))
|
|
124
|
+
L.push(`| \`${w.file}\` | ${w.triggers.join(", ") || "?"} | ${w.runs.slice(0, 3).join(" ; ").replace(/\|/g, "\\|")} |`);
|
|
125
|
+
L.push("");
|
|
126
|
+
L.push(`A workflow that publishes with a registry identity is doing a job a laptop cannot. These are not those: every step is an npm script, and the same line runs free as a pre-push check.`, "");
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
if (accidental.length) {
|
|
130
|
+
L.push(`### The same code in more than one place`, "");
|
|
131
|
+
for (const d of accidental.slice(0, 10)) {
|
|
132
|
+
L.push(`- ${d.lines} matching lines across ${d.files.join(", ")} - opens with \`${d.opens_with}\``);
|
|
133
|
+
}
|
|
134
|
+
const deliberate = p.duplicates.length - accidental.length;
|
|
135
|
+
if (deliberate > 0)
|
|
136
|
+
L.push(`- (${deliberate} further cop${deliberate > 1 ? "ies" : "y"} announce themselves as deliberate in a header comment; a copy that says so is a decision, not a debt.)`);
|
|
137
|
+
L.push("");
|
|
138
|
+
}
|
|
139
|
+
if (p.dead.length) {
|
|
140
|
+
L.push(`### Files no entrypoint reaches`, "");
|
|
141
|
+
L.push(`From ${p.totals.entrypoints} entrypoints, the import walk never arrived at these. A file loaded by a path built at runtime can still be alive - this is the list to answer for.`, "");
|
|
142
|
+
for (const d of p.dead.slice(0, 20))
|
|
143
|
+
L.push(`- \`${d.file}\` (${d.loc} lines)`);
|
|
144
|
+
if (p.dead.length > 20)
|
|
145
|
+
L.push(`- …and ${p.dead.length - 20} more in the JSON`);
|
|
146
|
+
L.push("");
|
|
147
|
+
}
|
|
148
|
+
if (p.cost_surfaces.length) {
|
|
149
|
+
L.push(`### Paid calls that multiply`, "");
|
|
150
|
+
L.push(`| Shape | Target | Where |`);
|
|
151
|
+
L.push(`| --- | --- | --- |`);
|
|
152
|
+
for (const c of p.cost_surfaces.slice(0, 15))
|
|
153
|
+
L.push(`| ${c.shape} | ${c.target} | \`${c.file}:${c.line.replace(/\|/g, "\\|")}\` |`);
|
|
154
|
+
L.push("");
|
|
155
|
+
L.push(`Per-row is the loud one: a paid call inside a loop bills once per record, forever. Worth deciding once, on purpose.`, "");
|
|
156
|
+
}
|
|
157
|
+
L.push(`## Lane 2 - what the territory holds [RESEARCHED - not yet run]`, "");
|
|
158
|
+
L.push(`This lane needs research your machine must not do on its own: the vendor and API landscape`, `of your industry, with sources and dates. Run \`npx @bigsteele/the-prospect --run\` and an`, `agent researches it against the facts above. Every suggestion it may make must stand on`, `three legs - a fact read from your code, a fact researched from your market, and your North`, `Star - and ends with what your customer gets. Anything short of that is cut, not softened.`, "");
|
|
159
|
+
L.push(`The evidence it starts from - your repository's own vocabulary:`, "");
|
|
160
|
+
L.push(p.fingerprint.terms.slice(0, 15).map((t) => `\`${t.term}\``).join(" ") || "(fingerprint too thin - the agent will ask what the business is)", "");
|
|
161
|
+
L.push(`## Send this in`, "");
|
|
162
|
+
L.push(`**bigsteele.com/scan** Upload this file. You get a written read of the three moves worth`, `making first and what each one buys. No call required to get it, and no pitch inside it.`, "");
|
|
163
|
+
L.push(`---`, "");
|
|
164
|
+
L.push(`Step 0 is read-only and offline. No network, no database, no shell, and it never opens an`, `\`.env\` file. It never copies your source into the report beyond the single evidence lines`, `cited above. Not a security review, not a linter, not a design review; it runs nothing.`, "", `Big Steele · bigsteele.com`);
|
|
165
|
+
return L.join("\n");
|
|
166
|
+
}
|
package/dist/score.d.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The score: distance from "every part earns its place".
|
|
3
|
+
*
|
|
4
|
+
* Only Lane 1 scores. Opportunity is not gradable - a repo cannot lose
|
|
5
|
+
* points for the R&D nobody has researched yet - so the number measures
|
|
6
|
+
* what the deterministic half saw: references that go nowhere, code no
|
|
7
|
+
* entrypoint reaches, the same block living in several files, two vendors
|
|
8
|
+
* doing one job, and subsystems hand-rolled where a rail exists.
|
|
9
|
+
*
|
|
10
|
+
* Levels carry floors a score cannot fake, family rule. A tidy repository
|
|
11
|
+
* with one vendor SDK it never imports is still paying for a thing it does
|
|
12
|
+
* not use, and no arithmetic talks its way past that.
|
|
13
|
+
*/
|
|
14
|
+
import type { DepFact, DuplicateFact, DeadFact, HandrolledFact, OverlapFact, CostFact } from "./detect/types.js";
|
|
15
|
+
import type { ConsolidationFact } from "./detect/stack.js";
|
|
16
|
+
export interface ProspectScore {
|
|
17
|
+
total: number;
|
|
18
|
+
grade: string;
|
|
19
|
+
level: {
|
|
20
|
+
n: 0 | 1 | 2 | 3 | 4;
|
|
21
|
+
name: string;
|
|
22
|
+
meaning: string;
|
|
23
|
+
};
|
|
24
|
+
/** Every deduction, so the number can be argued with rather than trusted. */
|
|
25
|
+
deductions: Array<{
|
|
26
|
+
what: string;
|
|
27
|
+
points: number;
|
|
28
|
+
evidence: string;
|
|
29
|
+
}>;
|
|
30
|
+
floors: string[];
|
|
31
|
+
}
|
|
32
|
+
export declare function grade(total: number): string;
|
|
33
|
+
export interface ScoreInput {
|
|
34
|
+
deps: DepFact[];
|
|
35
|
+
overlaps: OverlapFact[];
|
|
36
|
+
handrolled: HandrolledFact[];
|
|
37
|
+
duplicates: DuplicateFact[];
|
|
38
|
+
dead: DeadFact[];
|
|
39
|
+
costs: CostFact[];
|
|
40
|
+
/** Platforms whose job another platform already present can carry. */
|
|
41
|
+
consolidations?: ConsolidationFact[];
|
|
42
|
+
/** Total runtime files, for shares. */
|
|
43
|
+
runtime_files: number;
|
|
44
|
+
/** True when a vendor SDK dependency itself has no reference found - paying for a thing never called. */
|
|
45
|
+
unused_paid_service: string | null;
|
|
46
|
+
}
|
|
47
|
+
export declare function scoreProspect(input: ScoreInput): ProspectScore;
|
package/dist/score.js
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
export function grade(total) {
|
|
2
|
+
if (total >= 97)
|
|
3
|
+
return "A+";
|
|
4
|
+
if (total >= 93)
|
|
5
|
+
return "A";
|
|
6
|
+
if (total >= 90)
|
|
7
|
+
return "A-";
|
|
8
|
+
if (total >= 87)
|
|
9
|
+
return "B+";
|
|
10
|
+
if (total >= 83)
|
|
11
|
+
return "B";
|
|
12
|
+
if (total >= 80)
|
|
13
|
+
return "B-";
|
|
14
|
+
if (total >= 77)
|
|
15
|
+
return "C+";
|
|
16
|
+
if (total >= 73)
|
|
17
|
+
return "C";
|
|
18
|
+
if (total >= 70)
|
|
19
|
+
return "C-";
|
|
20
|
+
if (total >= 60)
|
|
21
|
+
return "D";
|
|
22
|
+
return "F";
|
|
23
|
+
}
|
|
24
|
+
const LEVELS = [
|
|
25
|
+
{ n: 0, name: "Overgrown", meaning: "Dead weight, duplicates and doubled vendors in most categories. The codebase carries more than it uses, and everyone who touches it pays the freight." },
|
|
26
|
+
{ n: 1, name: "Heavy", meaning: "The system works, but several categories carry weight nothing uses: unreferenced packages, unreached files, or a vendor paid twice." },
|
|
27
|
+
{ n: 2, name: "Trimmed", meaning: "Most parts earn their place. What remains is a short, specific list rather than a habit." },
|
|
28
|
+
{ n: 3, name: "Lean", meaning: "Little is carried that is not used. Hand-rolled subsystems remain, but each is small or clearly a choice." },
|
|
29
|
+
{ n: 4, name: "Sharp", meaning: "Nothing unaccounted for: every dependency referenced, every file reached, one vendor per job, and every hand-rolled subsystem is a recorded decision." },
|
|
30
|
+
];
|
|
31
|
+
export function scoreProspect(input) {
|
|
32
|
+
const deductions = [];
|
|
33
|
+
const ding = (what, points, evidence) => {
|
|
34
|
+
if (points > 0)
|
|
35
|
+
deductions.push({ what, points: Math.round(points * 10) / 10, evidence });
|
|
36
|
+
};
|
|
37
|
+
const runtimeDeps = input.deps.filter((d) => !d.dev);
|
|
38
|
+
const noRef = runtimeDeps.filter((d) => d.no_reference_found);
|
|
39
|
+
// Unreferenced runtime dependencies: up to 20.
|
|
40
|
+
ding("dependencies with no reference found", Math.min(20, noRef.length * 2), noRef.length ? `${noRef.length} of ${runtimeDeps.length} runtime dependencies` : "");
|
|
41
|
+
// Files no entrypoint reaches: up to 20, by share of runtime files.
|
|
42
|
+
const deadShare = input.runtime_files > 0 ? input.dead.length / input.runtime_files : 0;
|
|
43
|
+
ding("files no entrypoint reaches", Math.min(20, Math.round(deadShare * 100)), input.dead.length ? `${input.dead.length} files (${Math.round(deadShare * 100)} in 100)` : "");
|
|
44
|
+
// Accidental duplicate clusters: up to 20. Deliberate copies cost nothing.
|
|
45
|
+
const accidental = input.duplicates.filter((d) => !d.deliberate);
|
|
46
|
+
const dupLines = accidental.reduce((t, d) => t + d.lines * (d.files.length - 1), 0);
|
|
47
|
+
ding("duplicated blocks not marked deliberate", Math.min(20, Math.round(dupLines / 40)), accidental.length ? `${accidental.length} clusters, ${dupLines} repeated lines` : "");
|
|
48
|
+
// Vendor overlap: up to 20. Two vendors in one category is a doubled bill.
|
|
49
|
+
ding("two or more vendors doing one job", Math.min(20, input.overlaps.length * 7), input.overlaps.length ? input.overlaps.map((o) => `${o.category}: ${o.services.join(" + ")}`).join("; ") : "");
|
|
50
|
+
// Hand-rolled where a rail exists: up to 12, high-confidence only.
|
|
51
|
+
const high = input.handrolled.filter((h) => h.confidence === "high");
|
|
52
|
+
ding("hand-rolled subsystems with rails available", Math.min(12, high.length * 4), high.length ? high.map((h) => h.rail).join(", ") : "");
|
|
53
|
+
// The stack cut: up to 12. A platform another platform already covers is
|
|
54
|
+
// a bill and a failure surface, and nobody decided to have both.
|
|
55
|
+
const consolidations = input.consolidations ?? [];
|
|
56
|
+
ding("platforms another platform already covers", Math.min(12, consolidations.length * 4), consolidations.length ? consolidations.map((c) => `${c.candidate} (covered by ${c.keep})`).join("; ") : "");
|
|
57
|
+
// Multiplying cost shapes: up to 8; per-row is the loud one.
|
|
58
|
+
const perRow = input.costs.filter((c) => c.shape === "per-row").length;
|
|
59
|
+
ding("paid calls that multiply per row", Math.min(8, perRow * 4), perRow ? `${perRow} loops around a paid call` : "");
|
|
60
|
+
const total = Math.max(0, 100 - deductions.reduce((t, d) => t + d.points, 0));
|
|
61
|
+
// Floors, then the level the remaining score allows.
|
|
62
|
+
const floors = [];
|
|
63
|
+
let cap = 4;
|
|
64
|
+
if (input.unused_paid_service) {
|
|
65
|
+
cap = Math.min(cap, 2);
|
|
66
|
+
floors.push(`A paid service (${input.unused_paid_service}) that nothing references caps the level at 2: a bill with no work behind it is the first one to stop.`);
|
|
67
|
+
}
|
|
68
|
+
const bigDup = accidental.find((d) => d.lines > 200);
|
|
69
|
+
if (bigDup) {
|
|
70
|
+
cap = Math.min(cap, 3);
|
|
71
|
+
floors.push(`A duplicate block over 200 lines (${bigDup.files.join(", ")}) caps the level at 3.`);
|
|
72
|
+
}
|
|
73
|
+
let n;
|
|
74
|
+
if (total >= 93)
|
|
75
|
+
n = 4;
|
|
76
|
+
else if (total >= 82)
|
|
77
|
+
n = 3;
|
|
78
|
+
else if (total >= 68)
|
|
79
|
+
n = 2;
|
|
80
|
+
else if (total >= 50)
|
|
81
|
+
n = 1;
|
|
82
|
+
else
|
|
83
|
+
n = 0;
|
|
84
|
+
if (n > cap)
|
|
85
|
+
n = cap;
|
|
86
|
+
return {
|
|
87
|
+
total: Math.round(total),
|
|
88
|
+
grade: grade(Math.round(total)),
|
|
89
|
+
level: LEVELS[n],
|
|
90
|
+
deductions: deductions.sort((a, b) => b.points - a.points),
|
|
91
|
+
floors,
|
|
92
|
+
};
|
|
93
|
+
}
|
package/dist/walk.d.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
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.js
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
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
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@bigsteele/the-prospect",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "A prospector's read of your codebase and your market. Digs the ground you own: every dependency that does no work, every vendor you pay twice, every subsystem you built by hand where a rail now exists. Then surveys the territory: what your industry ships by API that your code still does the hard way. Every suggestion stands on three legs - a fact read from your code, a fact researched from your market with a source and a date, and the thing your product exists to do. Standalone: one npx, no other scan required.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "UNLICENSED",
|
|
7
|
+
"author": "Big Steele (Together Inc.)",
|
|
8
|
+
"homepage": "https://bigsteele.com",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/bigsteele/blank-canvas-project.git",
|
|
12
|
+
"directory": "packages/the-prospect"
|
|
13
|
+
},
|
|
14
|
+
"bin": {
|
|
15
|
+
"the-prospect": "dist/cli.js",
|
|
16
|
+
"prospect": "dist/cli.js"
|
|
17
|
+
},
|
|
18
|
+
"main": "dist/index.js",
|
|
19
|
+
"types": "dist/index.d.ts",
|
|
20
|
+
"exports": {
|
|
21
|
+
".": {
|
|
22
|
+
"types": "./dist/index.d.ts",
|
|
23
|
+
"import": "./dist/index.js"
|
|
24
|
+
},
|
|
25
|
+
"./package.json": "./package.json"
|
|
26
|
+
},
|
|
27
|
+
"files": [
|
|
28
|
+
"dist",
|
|
29
|
+
"prompt",
|
|
30
|
+
"README.md"
|
|
31
|
+
],
|
|
32
|
+
"engines": {
|
|
33
|
+
"node": ">=20"
|
|
34
|
+
},
|
|
35
|
+
"scripts": {
|
|
36
|
+
"build": "rm -rf dist && tsc -p tsconfig.json",
|
|
37
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
38
|
+
"test": "vitest run",
|
|
39
|
+
"prepublishOnly": "npm run build && npm test"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@types/node": "^22.10.0",
|
|
43
|
+
"typescript": "^5.7.0",
|
|
44
|
+
"vitest": "^3.0.0"
|
|
45
|
+
},
|
|
46
|
+
"publishConfig": {
|
|
47
|
+
"access": "public"
|
|
48
|
+
}
|
|
49
|
+
}
|