@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.
@@ -0,0 +1,164 @@
1
+ import { NOT_RUNTIME } from "../walk.js";
2
+ const HOSTING = [
3
+ { platform: "Vercel", files: /(^|\/)vercel\.json$|(^|\/)\.vercel\/project\.json$/ },
4
+ { platform: "Netlify", files: /(^|\/)netlify\.toml$/ },
5
+ { platform: "Cloudflare (Pages/Workers)", files: /(^|\/)wrangler\.(toml|jsonc?)$/ },
6
+ { platform: "Fly.io", files: /(^|\/)fly\.toml$/ },
7
+ { platform: "Render", files: /(^|\/)render\.ya?ml$/ },
8
+ { platform: "Railway", files: /(^|\/)railway\.(json|toml)$/ },
9
+ { platform: "Heroku", files: /(^|\/)Procfile$/ },
10
+ { platform: "Firebase Hosting", files: /(^|\/)firebase\.json$/ },
11
+ { platform: "AWS Amplify", files: /(^|\/)amplify\.ya?ml$/ },
12
+ ];
13
+ const CI = [
14
+ { platform: "GitHub Actions", files: /^\.github\/workflows\/[^/]+\.ya?ml$/ },
15
+ { platform: "GitLab CI", files: /(^|\/)\.gitlab-ci\.ya?ml$/ },
16
+ { platform: "CircleCI", files: /^\.circleci\/config\.ya?ml$/ },
17
+ { platform: "Jenkins", files: /(^|\/)Jenkinsfile$/ },
18
+ ];
19
+ const PUBLISH_MARK = /npm publish|registry-url|id-token|NODE_AUTH_TOKEN/i;
20
+ const DEPLOY_MARK = /wrangler|vercel|netlify|fly deploy|aws s3|aws lambda|gcloud|az webapp|firebase deploy|supabase functions deploy/i;
21
+ const SCRIPT_RUN = /^\s*(npm|npx|yarn|pnpm|node|bun)\b/;
22
+ function parseWorkflow(file, text) {
23
+ const triggers = [];
24
+ const onBlock = /(^|\n)(?:on|"on"):\s*([\s\S]*?)(?=\n[a-zA-Z"']|$)/.exec(text)?.[2] ?? "";
25
+ for (const t of ["push", "pull_request", "schedule", "workflow_dispatch", "release", "tags"]) {
26
+ if (new RegExp(`(^|\\W)${t}\\s*:`).test(onBlock) || new RegExp(`(^|\\W)${t}\\b`).test(onBlock.split("\n")[0] ?? ""))
27
+ triggers.push(t);
28
+ }
29
+ const runs = [...text.matchAll(/(^|\n)\s*(?:-\s+)?run:\s*(\|?\s*\n?\s*)?([^\n]+)/g)].map((m) => m[3].trim()).filter(Boolean);
30
+ const publishes = PUBLISH_MARK.test(text);
31
+ const deploys = DEPLOY_MARK.test(text);
32
+ const script_only = runs.length > 0 && !publishes && !deploys && runs.every((r) => SCRIPT_RUN.test(r));
33
+ return { file, triggers, runs: runs.slice(0, 10), publishes, deploys, script_only };
34
+ }
35
+ export async function detectStack(repo) {
36
+ const platforms = [];
37
+ const seen = (rules, kind) => {
38
+ for (const r of rules) {
39
+ const hits = repo.files.filter((f) => r.files.test(f) && !/fixtures?|templates?|examples?|node_modules/.test(f));
40
+ if (hits.length)
41
+ platforms.push({ platform: r.platform, kind, evidence: hits.slice(0, 5) });
42
+ }
43
+ };
44
+ seen(HOSTING, "hosting");
45
+ seen(CI, "ci");
46
+ if (repo.files.some((f) => /(^|\/)Dockerfile$|(^|\/)docker-compose\.ya?ml$/.test(f) && !/fixtures?|examples?/.test(f))) {
47
+ platforms.push({ platform: "Docker", kind: "container", evidence: repo.files.filter((f) => /(^|\/)Dockerfile$|(^|\/)docker-compose\.ya?ml$/.test(f)).slice(0, 3) });
48
+ }
49
+ const workflows = [];
50
+ for (const f of repo.files.filter((x) => /^\.github\/workflows\/[^/]+\.ya?ml$/.test(x))) {
51
+ const text = await repo.read(f);
52
+ if (text)
53
+ workflows.push(parseWorkflow(f, text));
54
+ }
55
+ // What the repo already has, for the consolidation map's "keep" side.
56
+ const has = (p) => platforms.find((x) => x.platform.startsWith(p));
57
+ // IMPORT-ANCHORED probes, a lesson paid for on the first run: a sibling
58
+ // scanner's detector source MENTIONS "@upstash/" inside its own regexes,
59
+ // and a .claude settings file mentions supabase - neither is this product
60
+ // using the service. Only an import statement (or a live fetch to the
61
+ // vendor's API host) counts as presence.
62
+ const imp = (pkg) => new RegExp(`(from\\s+|require\\(\\s*|import\\(\\s*)["']${pkg.replace(/[.*+?^$()|[\]\\]/g, "\\$&")}`);
63
+ const cloudflareSdk = (await grepAny(repo, imp("@cloudflare/"), 2000)) ?? (await grepAny(repo, /fetch\(\s*[`"']https:\/\/api\.cloudflare\.com/, 2000));
64
+ const supabase = (await grepAny(repo, imp("@supabase/"), 2000)) ?? (await grepAny(repo, /createClient\([^)]{0,120}\.supabase\.co/, 2000));
65
+ const clerk = await grepAny(repo, imp("@clerk/"), 2000);
66
+ const auth0 = await grepAny(repo, imp("@auth0/"), 2000);
67
+ const s3 = await grepAny(repo, imp("@aws-sdk/client-s3"), 2000);
68
+ const upstash = await grepAny(repo, imp("@upstash/"), 2000);
69
+ const searchSaaS = (await grepAny(repo, imp("algoliasearch"), 2000)) ?? (await grepAny(repo, imp("meilisearch"), 2000)) ?? (await grepAny(repo, imp("typesense"), 2000));
70
+ const consolidations = [];
71
+ const cf = has("Cloudflare") ?? (cloudflareSdk ? { platform: "Cloudflare", evidence: [cloudflareSdk] } : null);
72
+ for (const rival of ["Vercel", "Netlify"]) {
73
+ const r = has(rival);
74
+ if (cf && r) {
75
+ consolidations.push({
76
+ keep: "Cloudflare",
77
+ candidate: rival,
78
+ covers: "static hosting and serverless functions (Pages and Workers)",
79
+ evidence_keep: cf.evidence[0],
80
+ evidence_candidate: r.evidence[0],
81
+ note: `Two deploy platforms, two bills, two places a deploy can fail. Is ${rival} carrying anything Cloudflare is not already paid to carry?`,
82
+ });
83
+ }
84
+ }
85
+ if (supabase && (clerk || auth0)) {
86
+ const candidate = clerk ? "Clerk" : "Auth0";
87
+ consolidations.push({
88
+ keep: "Supabase",
89
+ candidate,
90
+ covers: "auth and sessions (included with the database you already pay for)",
91
+ evidence_keep: supabase,
92
+ evidence_candidate: (clerk ?? auth0),
93
+ note: `Supabase Auth ships with the plan the database is on. What is ${candidate} doing that it does not?`,
94
+ });
95
+ }
96
+ if (supabase && s3) {
97
+ consolidations.push({
98
+ keep: "Supabase",
99
+ candidate: "AWS S3",
100
+ covers: "file storage (included with the database you already pay for)",
101
+ evidence_keep: supabase,
102
+ evidence_candidate: s3,
103
+ note: "If S3 is holding plain uploads, Supabase Storage already holds them under the same bill and the same auth rules.",
104
+ });
105
+ }
106
+ if (supabase && upstash) {
107
+ consolidations.push({
108
+ keep: "Supabase (Postgres)",
109
+ candidate: "Upstash",
110
+ covers: "queues, schedules and counters (pg_cron, pgmq, or a table)",
111
+ evidence_keep: supabase,
112
+ evidence_candidate: upstash,
113
+ note: "Postgres you already run does queues and schedules at this scale. What is the second datastore buying?",
114
+ });
115
+ }
116
+ if (supabase && searchSaaS) {
117
+ consolidations.push({
118
+ keep: "Supabase (Postgres)",
119
+ candidate: "hosted search",
120
+ covers: "search (Postgres full-text, tsvector) at small-corpus scale",
121
+ evidence_keep: supabase,
122
+ evidence_candidate: searchSaaS,
123
+ note: "Below roughly a hundred thousand records, the database you already run searches fine. Above it, the vendor earns its bill. Which side is this on?",
124
+ });
125
+ }
126
+ // The npx-instead finding, owner's example one.
127
+ const scriptOnly = workflows.filter((w) => w.script_only);
128
+ if (scriptOnly.length) {
129
+ consolidations.push({
130
+ keep: "the machine that pushes",
131
+ candidate: "GitHub Actions minutes",
132
+ covers: `npm scripts that run anywhere (${scriptOnly.map((w) => w.file.split("/").pop()).join(", ")})`,
133
+ evidence_keep: "package.json scripts",
134
+ evidence_candidate: scriptOnly[0].file,
135
+ note: `${scriptOnly.length} workflow${scriptOnly.length > 1 ? "s" : ""} only run npm scripts. The same line runs free and faster as a pre-push check on the machine that already built the code. Worth keeping CI for the team-wide guarantee, or worth the minutes back?`,
136
+ });
137
+ }
138
+ // Two hosting platforms, or two CI systems: the same paid-twice shape the
139
+ // vendor detector reports, fed into the same machinery.
140
+ const overlaps = [];
141
+ for (const kind of ["hosting", "ci"]) {
142
+ const list = platforms.filter((p) => p.kind === kind);
143
+ if (list.length >= 2) {
144
+ overlaps.push({
145
+ category: kind,
146
+ services: list.map((p) => p.platform),
147
+ call_sites: list.reduce((t, p) => t + p.evidence.length, 0),
148
+ });
149
+ }
150
+ }
151
+ return { platforms, workflows, consolidations, overlaps };
152
+ }
153
+ /** First RUNTIME file whose text matches; a cheap presence probe. Dot
154
+ * directories, docs, fixtures and this package's own tree are excluded by
155
+ * the same rule the walker's detectors use. */
156
+ async function grepAny(repo, re, limit) {
157
+ const files = repo.files.filter((f) => /\.(ts|tsx|js|jsx|mjs|cjs|toml|jsonc?)$/i.test(f) && !NOT_RUNTIME.test(f) && !/node_modules|fixtures?|templates?|(^|\/)package(-lock)?\.json$/.test(f)).slice(0, limit);
158
+ for (const f of files) {
159
+ const text = await repo.read(f);
160
+ if (text && re.test(text))
161
+ return f;
162
+ }
163
+ return null;
164
+ }
@@ -0,0 +1,106 @@
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" | "hosting" | "ci" | "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
+ }
@@ -0,0 +1,11 @@
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 {};
@@ -0,0 +1,26 @@
1
+ /**
2
+ * The vendor roster, and the overlaps.
3
+ *
4
+ * The exemplar repo called three AI providers. Not because anyone chose to
5
+ * pay three vendors for one category of work - because integration number
6
+ * two and integration number three each arrived inside a feature, and no
7
+ * ledger existed where the three would ever appear on the same line. This
8
+ * detector is that ledger: every external service the code talks to, grouped
9
+ * by what kind of work it does, so paying twice for one job becomes a line
10
+ * a founder can read.
11
+ *
12
+ * Evidence only: SDK imports, outbound URL hosts, environment variable NAMES.
13
+ * Values are never read - the walker refuses env files outright.
14
+ */
15
+ import type { Repo } from "../walk.js";
16
+ import type { OverlapFact, VendorFact } from "./types.js";
17
+ export interface VendorReading {
18
+ vendors: VendorFact[];
19
+ overlaps: OverlapFact[];
20
+ /** Hosts called that the map does not recognise - listed, never guessed at. */
21
+ unrecognised: Array<{
22
+ host: string;
23
+ files: string[];
24
+ }>;
25
+ }
26
+ export declare function detectVendors(repo: Repo): Promise<VendorReading>;
@@ -0,0 +1,125 @@
1
+ import { runtimeCode } from "../walk.js";
2
+ /** The map earns a row when a vendor is common enough that a founder would
3
+ * recognise the name. Everything else lands in "unrecognised outbound". */
4
+ const KNOWN = [
5
+ { service: "OpenAI", category: "ai", sdks: /^openai$|^@openai\//, hosts: /(^|\.)api\.openai\.com$/, env: /^OPENAI_/ },
6
+ { service: "Anthropic", category: "ai", sdks: /^@anthropic-ai\//, hosts: /(^|\.)api\.anthropic\.com$/, env: /^ANTHROPIC_/ },
7
+ { service: "Google AI (Gemini)", category: "ai", sdks: /^@google\/(generative-ai|genai)$/, hosts: /generativelanguage\.googleapis\.com$/, env: /^(GOOGLE_AI|GEMINI)_/ },
8
+ { service: "Groq", category: "ai", sdks: /^groq-sdk$/, hosts: /api\.groq\.com$/, env: /^GROQ_/ },
9
+ { service: "Mistral", category: "ai", sdks: /^@mistralai\//, hosts: /api\.mistral\.ai$/, env: /^MISTRAL_/ },
10
+ { service: "Replicate", category: "ai", sdks: /^replicate$/, hosts: /api\.replicate\.com$/, env: /^REPLICATE_/ },
11
+ { service: "ElevenLabs", category: "ai", sdks: /^elevenlabs$|^@elevenlabs\//, hosts: /api\.elevenlabs\.io$/, env: /^ELEVENLABS_/ },
12
+ { service: "OpenRouter", category: "ai", hosts: /openrouter\.ai$/, env: /^OPENROUTER_/ },
13
+ { service: "AWS Bedrock", category: "ai", sdks: /^@aws-sdk\/client-bedrock/ },
14
+ { service: "Resend", category: "email", sdks: /^resend$/, hosts: /api\.resend\.com$/, env: /^RESEND_/ },
15
+ { service: "SendGrid", category: "email", sdks: /^@sendgrid\//, hosts: /api\.sendgrid\.com$/, env: /^SENDGRID_/ },
16
+ { service: "Postmark", category: "email", sdks: /^postmark$/, hosts: /api\.postmarkapp\.com$/, env: /^POSTMARK_/ },
17
+ { service: "Mailgun", category: "email", sdks: /^mailgun/, hosts: /api\.mailgun\.net$/, env: /^MAILGUN_/ },
18
+ { service: "AWS SES", category: "email", sdks: /^@aws-sdk\/client-ses/ },
19
+ { service: "Nodemailer (SMTP)", category: "email", sdks: /^nodemailer$/, env: /^SMTP_/ },
20
+ { service: "Twilio", category: "sms", sdks: /^twilio$/, hosts: /api\.twilio\.com$/, env: /^TWILIO_/ },
21
+ { service: "Vonage", category: "sms", sdks: /^@vonage\//, env: /^VONAGE_/ },
22
+ { service: "Stripe", category: "payments", sdks: /^stripe$|^@stripe\//, hosts: /api\.stripe\.com$/, env: /^STRIPE_/ },
23
+ { service: "PayPal", category: "payments", sdks: /^@paypal\//, hosts: /api(-m)?\.paypal\.com$/, env: /^PAYPAL_/ },
24
+ { service: "Square", category: "payments", sdks: /^square$/, hosts: /connect\.squareup\.com$/, env: /^SQUARE_/ },
25
+ { service: "Lemon Squeezy", category: "payments", sdks: /^@lemonsqueezy\//, env: /^LEMONSQUEEZY_/ },
26
+ { service: "Supabase", category: "database", sdks: /^@supabase\//, hosts: /\.supabase\.co$/, env: /^(SUPABASE|VITE_SUPABASE|NEXT_PUBLIC_SUPABASE)_/ },
27
+ { service: "Firebase", category: "database", sdks: /^firebase(-admin)?$/, env: /^FIREBASE_/ },
28
+ { service: "PlanetScale", category: "database", sdks: /^@planetscale\//, env: /^DATABASE_URL$/ },
29
+ { service: "Neon", category: "database", sdks: /^@neondatabase\//, env: /^NEON_/ },
30
+ { service: "MongoDB Atlas", category: "database", sdks: /^mongodb$|^mongoose$/, env: /^MONGO(DB)?_/ },
31
+ { service: "Upstash", category: "queue", sdks: /^@upstash\//, hosts: /upstash\.io$/, env: /^UPSTASH_/ },
32
+ { service: "Clerk", category: "auth", sdks: /^@clerk\//, env: /^CLERK_/ },
33
+ { service: "Auth0", category: "auth", sdks: /^auth0$|^@auth0\//, env: /^AUTH0_/ },
34
+ { service: "Cloudflare", category: "render", sdks: /^@cloudflare\//, hosts: /api\.cloudflare\.com$/, env: /^(CF|CLOUDFLARE)_/ },
35
+ { service: "AWS S3", category: "storage", sdks: /^@aws-sdk\/client-s3/, env: /^(AWS_S3|S3)_/ },
36
+ { service: "Uploadthing", category: "storage", sdks: /^uploadthing$|^@uploadthing\//, env: /^UPLOADTHING_/ },
37
+ { service: "Cloudinary", category: "storage", sdks: /^cloudinary$/, hosts: /api\.cloudinary\.com$/, env: /^CLOUDINARY_/ },
38
+ { service: "GoHighLevel", category: "crm", hosts: /(services\.)?leadconnectorhq\.com$|rest\.gohighlevel\.com$/, env: /^(GHL|HIGHLEVEL)_/ },
39
+ { service: "HubSpot", category: "crm", sdks: /^@hubspot\//, hosts: /api\.hubapi\.com$/, env: /^HUBSPOT_/ },
40
+ { service: "Salesforce", category: "crm", sdks: /^jsforce$/, env: /^(SF|SALESFORCE)_/ },
41
+ { service: "Airtable", category: "crm", sdks: /^airtable$/, hosts: /api\.airtable\.com$/, env: /^AIRTABLE_/ },
42
+ { service: "PostHog", category: "analytics", sdks: /^posthog/, hosts: /app\.posthog\.com$/, env: /^POSTHOG_/ },
43
+ { service: "Mixpanel", category: "analytics", sdks: /^mixpanel/, env: /^MIXPANEL_/ },
44
+ { service: "Segment", category: "analytics", sdks: /^@segment\//, env: /^SEGMENT_/ },
45
+ { service: "Google Analytics", category: "analytics", hosts: /www\.google-analytics\.com$/ },
46
+ { service: "Sentry", category: "monitoring", sdks: /^@sentry\//, env: /^SENTRY_/ },
47
+ { service: "Langfuse", category: "monitoring", sdks: /^langfuse/, env: /^LANGFUSE_/ },
48
+ { service: "Algolia", category: "search", sdks: /^algoliasearch$/, env: /^ALGOLIA_/ },
49
+ { service: "Meilisearch", category: "search", sdks: /^meilisearch$/, env: /^MEILI/ },
50
+ { service: "Typesense", category: "search", sdks: /^typesense$/, env: /^TYPESENSE_/ },
51
+ { service: "Google Maps", category: "maps", sdks: /^@googlemaps\//, hosts: /maps\.googleapis\.com$/, env: /^GOOGLE_MAPS_/ },
52
+ { service: "Mapbox", category: "maps", sdks: /^mapbox-gl$/, env: /^MAPBOX_/ },
53
+ { service: "Cal.com", category: "calendar", hosts: /api\.cal\.com$/, env: /^CAL_/ },
54
+ { service: "Calendly", category: "calendar", hosts: /api\.calendly\.com$/, env: /^CALENDLY_/ },
55
+ ];
56
+ /** Hosts that are content, not services: fonts, CDNs of the app's own assets, social links. */
57
+ const NOISE_HOST = /(^|\.)(esm\.sh|cdn\.jsdelivr\.net|unpkg\.com|fonts\.(googleapis|gstatic)\.com|githubusercontent\.com|github\.com|linkedin\.com|x\.com|twitter\.com|instagram\.com|facebook\.com|youtube\.com|tiktok\.com|schema\.org|w3\.org|localhost)$/i;
58
+ export async function detectVendors(repo) {
59
+ const files = runtimeCode(repo.files);
60
+ const byService = new Map();
61
+ const touch = (k, kind, what, file) => {
62
+ const v = byService.get(k.service) ?? { service: k.service, category: k.category, evidence: [], call_sites: 0 };
63
+ v.call_sites++;
64
+ if (v.evidence.length < 8 && !v.evidence.some((e) => e.what === what && e.file === file)) {
65
+ v.evidence.push({ kind, what, file });
66
+ }
67
+ byService.set(k.service, v);
68
+ };
69
+ const unknownHosts = new Map();
70
+ for (const f of files) {
71
+ const text = await repo.read(f);
72
+ if (!text)
73
+ continue;
74
+ for (const m of text.matchAll(/\bfrom\s+["']([^"'\n]+)["']|\brequire\(\s*["']([^"'\n]+)["']\s*\)/g)) {
75
+ const spec = (m[1] ?? m[2]);
76
+ if (spec.startsWith("."))
77
+ continue;
78
+ const pkg = spec.replace(/^npm:/, "").split("/").slice(0, spec.startsWith("@") || spec.startsWith("npm:@") ? 2 : 1).join("/");
79
+ for (const k of KNOWN)
80
+ if (k.sdks?.test(pkg))
81
+ touch(k, "sdk", pkg, f);
82
+ }
83
+ for (const m of text.matchAll(/https?:\/\/([a-z0-9][a-z0-9.-]+\.[a-z]{2,})/gi)) {
84
+ const host = m[1].toLowerCase();
85
+ if (NOISE_HOST.test(host))
86
+ continue;
87
+ const known = KNOWN.find((k) => k.hosts?.test(host));
88
+ if (known)
89
+ touch(known, "url", host, f);
90
+ else {
91
+ const set = unknownHosts.get(host) ?? new Set();
92
+ set.add(f);
93
+ unknownHosts.set(host, set);
94
+ }
95
+ }
96
+ for (const m of text.matchAll(/process\.env\.([A-Z][A-Z0-9_]{2,})|Deno\.env\.get\(\s*["']([A-Z][A-Z0-9_]{2,})["']\s*\)/g)) {
97
+ const name = (m[1] ?? m[2]);
98
+ for (const k of KNOWN)
99
+ if (k.env?.test(name))
100
+ touch(k, "env", name, f);
101
+ }
102
+ }
103
+ const vendors = [...byService.values()].sort((a, b) => b.call_sites - a.call_sites);
104
+ const byCategory = new Map();
105
+ for (const v of vendors) {
106
+ const list = byCategory.get(v.category) ?? [];
107
+ list.push(v);
108
+ byCategory.set(v.category, list);
109
+ }
110
+ const overlaps = [];
111
+ for (const [category, list] of byCategory) {
112
+ if (category === "other" || list.length < 2)
113
+ continue;
114
+ overlaps.push({
115
+ category,
116
+ services: list.map((v) => v.service),
117
+ call_sites: list.reduce((t, v) => t + v.call_sites, 0),
118
+ });
119
+ }
120
+ const unrecognised = [...unknownHosts.entries()]
121
+ .map(([host, set]) => ({ host, files: [...set].slice(0, 5) }))
122
+ .sort((a, b) => b.files.length - a.files.length)
123
+ .slice(0, 20);
124
+ return { vendors, overlaps: overlaps.sort((a, b) => b.services.length - a.services.length), unrecognised };
125
+ }
@@ -0,0 +1,36 @@
1
+ import { type StackReading } from "./detect/stack.js";
2
+ import { type ProspectScore } from "./score.js";
3
+ import type { CostFact, DeadFact, DepFact, DuplicateFact, Fingerprint, HandrolledFact, NorthStar, OverlapFact, VendorFact } from "./detect/types.js";
4
+ export { toMarkdown, secretShaped } from "./report.js";
5
+ export { checkReport } from "./check.js";
6
+ export type { ProspectScore } from "./score.js";
7
+ export interface Prospect {
8
+ format: "bigsteele-prospect/1";
9
+ brand: "The Prospect";
10
+ version: string;
11
+ app: string;
12
+ generated: string;
13
+ north_star: NorthStar;
14
+ deps: DepFact[];
15
+ vendors: VendorFact[];
16
+ overlaps: OverlapFact[];
17
+ unrecognised_hosts: Array<{
18
+ host: string;
19
+ files: string[];
20
+ }>;
21
+ stack: StackReading;
22
+ handrolled: HandrolledFact[];
23
+ duplicates: DuplicateFact[];
24
+ dead: DeadFact[];
25
+ cost_surfaces: CostFact[];
26
+ fingerprint: Fingerprint;
27
+ score: ProspectScore;
28
+ totals: {
29
+ files: number;
30
+ runtime_files: number;
31
+ runtime_deps: number;
32
+ entrypoints: number;
33
+ };
34
+ }
35
+ export declare const VERSION = "0.1.1";
36
+ export declare function runProspect(root: string): Promise<Prospect>;
package/dist/index.js ADDED
@@ -0,0 +1,93 @@
1
+ /**
2
+ * The Prospect - a prospector's read of your codebase and your market.
3
+ *
4
+ * Two lanes, one law. Lane 1 subtracts: dependencies that do no work, code
5
+ * no entrypoint reaches, the same block living in several files, two
6
+ * vendors doing one job, subsystems hand-rolled where a rail now exists.
7
+ * Lane 2 adds: what the industry ships that the code shows this product is
8
+ * missing - and Lane 2 is the agent protocol's job, because it takes
9
+ * research with sources and dates, which a deterministic offline scanner
10
+ * must not fake.
11
+ *
12
+ * The order of detection is the order of trust. Dependencies and vendors
13
+ * are read first because they are ledgers - closest to money and hardest
14
+ * to argue with. Reachability and duplication next: mechanical, checkable.
15
+ * The fingerprint last, because it is evidence for a question the agent
16
+ * asks the operator, never an answer.
17
+ *
18
+ * Standalone is law (owner, 2026-09-12): one npx on a cold repository
19
+ * produces the full Step 0 and the full report. No sibling scan installed,
20
+ * run, or required.
21
+ */
22
+ import { openRepo } from "./walk.js";
23
+ import { runtimeCode } from "./walk.js";
24
+ import { detectDeps } from "./detect/deps.js";
25
+ import { detectVendors } from "./detect/vendors.js";
26
+ import { detectHandrolled } from "./detect/handrolled.js";
27
+ import { detectDuplication } from "./detect/duplication.js";
28
+ import { detectDeadweight } from "./detect/deadweight.js";
29
+ import { detectCosts } from "./detect/costs.js";
30
+ import { detectFingerprint } from "./detect/fingerprint.js";
31
+ import { detectStack } from "./detect/stack.js";
32
+ import { readNorthStar } from "./northstar.js";
33
+ import { scoreProspect } from "./score.js";
34
+ export { toMarkdown, secretShaped } from "./report.js";
35
+ export { checkReport } from "./check.js";
36
+ export const VERSION = "0.1.1";
37
+ export async function runProspect(root) {
38
+ const repo = await openRepo(root);
39
+ const runtime = runtimeCode(repo.files);
40
+ const [deps, vendorsReading, handrolled, duplicates, deadReading, costs, fingerprint, northStar, stack] = await Promise.all([
41
+ detectDeps(repo),
42
+ detectVendors(repo),
43
+ detectHandrolled(repo),
44
+ detectDuplication(repo),
45
+ detectDeadweight(repo),
46
+ detectCosts(repo),
47
+ detectFingerprint(repo),
48
+ readNorthStar(repo),
49
+ detectStack(repo),
50
+ ]);
51
+ // A vendor SDK that is DECLARED but never referenced is a bill with no
52
+ // work behind it - the score's hardest floor.
53
+ const vendorSdkNames = new Set(vendorsReading.vendors.flatMap((v) => v.evidence.filter((e) => e.kind === "sdk").map((e) => e.what)));
54
+ const unusedPaid = deps.find((d) => !d.dev && d.no_reference_found && /^(openai|stripe|twilio|resend|@sendgrid\/|@anthropic-ai\/|@clerk\/|algoliasearch|cloudinary)/.test(d.name))?.name ?? null;
55
+ void vendorSdkNames;
56
+ const allOverlaps = [...vendorsReading.overlaps, ...stack.overlaps];
57
+ const score = scoreProspect({
58
+ deps,
59
+ overlaps: allOverlaps,
60
+ consolidations: stack.consolidations,
61
+ handrolled,
62
+ duplicates,
63
+ dead: deadReading.dead,
64
+ costs,
65
+ runtime_files: runtime.length,
66
+ unused_paid_service: unusedPaid,
67
+ });
68
+ return {
69
+ format: "bigsteele-prospect/1",
70
+ brand: "The Prospect",
71
+ version: VERSION,
72
+ app: repo.name,
73
+ generated: new Date().toISOString().slice(0, 10),
74
+ north_star: northStar,
75
+ deps,
76
+ vendors: vendorsReading.vendors,
77
+ overlaps: allOverlaps,
78
+ stack,
79
+ unrecognised_hosts: vendorsReading.unrecognised,
80
+ handrolled,
81
+ duplicates,
82
+ dead: deadReading.dead,
83
+ cost_surfaces: costs,
84
+ fingerprint,
85
+ score,
86
+ totals: {
87
+ files: repo.files.length,
88
+ runtime_files: runtime.length,
89
+ runtime_deps: deps.filter((d) => !d.dev).length,
90
+ entrypoints: deadReading.entrypoints,
91
+ },
92
+ };
93
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * The North Star, read rather than written. Adapted from wiremap's reader
3
+ * (copied, never imported - family convention). The Prospect is judgment-
4
+ * heavy in its second half, which makes the deterministic half's honesty
5
+ * matter MORE, not less: a made-up mission here would tilt every R&D
6
+ * suggestion the agent researches later. It reads one and names its source,
7
+ * or it says UNKNOWN and the protocol derives one with the operator.
8
+ */
9
+ import type { Repo } from "./walk.js";
10
+ import type { NorthStar } from "./detect/types.js";
11
+ export declare function readNorthStar(repo: Repo): Promise<NorthStar>;