great-cto 1.0.166 → 1.0.167
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/dist/archetypes.js +317 -39
- package/dist/bootstrap.js +1 -1
- package/dist/detect.js +239 -3
- package/package.json +1 -1
package/dist/archetypes.js
CHANGED
|
@@ -80,12 +80,25 @@ const RULES = [
|
|
|
80
80
|
archetype: "commerce",
|
|
81
81
|
score: (d) => {
|
|
82
82
|
let s = 0;
|
|
83
|
+
// Don't score commerce if fintech signals are stronger (Plaid etc.)
|
|
84
|
+
if (d.stack.includes("plaid") || d.stack.includes("dwolla") || d.stack.includes("teller"))
|
|
85
|
+
return 0;
|
|
83
86
|
if (d.stack.includes("stripe"))
|
|
84
|
-
s +=
|
|
87
|
+
s += 7;
|
|
85
88
|
if (d.stack.includes("shopify"))
|
|
86
|
-
s +=
|
|
89
|
+
s += 7;
|
|
87
90
|
if (d.stack.includes("braintree"))
|
|
91
|
+
s += 6;
|
|
92
|
+
if (d.stack.includes("adyen"))
|
|
93
|
+
s += 6;
|
|
94
|
+
if (d.stack.includes("paddle"))
|
|
88
95
|
s += 5;
|
|
96
|
+
if (d.stack.includes("lemonsqueezy"))
|
|
97
|
+
s += 5;
|
|
98
|
+
// Add small bonus when paired with web framework (real e-commerce, not just SDK)
|
|
99
|
+
if (s > 0 && (d.stack.includes("next.js") || d.stack.includes("nuxt") ||
|
|
100
|
+
d.stack.includes("remix") || d.stack.includes("sveltekit")))
|
|
101
|
+
s += 1;
|
|
89
102
|
return s;
|
|
90
103
|
},
|
|
91
104
|
reason: (d) => {
|
|
@@ -96,6 +109,10 @@ const RULES = [
|
|
|
96
109
|
payments.push("Shopify");
|
|
97
110
|
if (d.stack.includes("braintree"))
|
|
98
111
|
payments.push("Braintree");
|
|
112
|
+
if (d.stack.includes("adyen"))
|
|
113
|
+
payments.push("Adyen");
|
|
114
|
+
if (d.stack.includes("paddle"))
|
|
115
|
+
payments.push("Paddle");
|
|
99
116
|
return `payments SDK detected: ${payments.join(", ")} — PCI-DSS gate mandatory`;
|
|
100
117
|
},
|
|
101
118
|
},
|
|
@@ -118,7 +135,49 @@ const RULES = [
|
|
|
118
135
|
score: (d) => (d.stack.includes("embedded") ? 6 : 0),
|
|
119
136
|
reason: (_d) => "platformio.ini / sdkconfig detected — embedded firmware archetype",
|
|
120
137
|
},
|
|
138
|
+
// ── agent-product (LLM + vector DB OR multi-agent framework) ────
|
|
139
|
+
// Higher score than ai-system: this is an autonomous-agent product, not a wrapper
|
|
140
|
+
{
|
|
141
|
+
archetype: "agent-product",
|
|
142
|
+
score: (d) => {
|
|
143
|
+
const llms = ["anthropic-sdk", "openai-sdk", "google-ai", "aws-bedrock", "cohere"];
|
|
144
|
+
const vdbs = ["pinecone", "weaviate", "chroma", "qdrant"];
|
|
145
|
+
const agents = ["langgraph", "crewai", "autogen", "mastra", "mcp"];
|
|
146
|
+
const hasLlm = llms.some((s) => d.stack.includes(s));
|
|
147
|
+
const hasVdb = vdbs.some((s) => d.stack.includes(s));
|
|
148
|
+
const hasAgentFw = agents.some((s) => d.stack.includes(s));
|
|
149
|
+
let s = 0;
|
|
150
|
+
if (hasLlm && hasVdb)
|
|
151
|
+
s += 7; // RAG-style agent
|
|
152
|
+
if (hasAgentFw)
|
|
153
|
+
s += 6; // explicit agent framework
|
|
154
|
+
if (hasLlm && hasAgentFw)
|
|
155
|
+
s += 2; // bonus
|
|
156
|
+
// README mining hint
|
|
157
|
+
if (d.readmeKeywords.includes("agent") || d.readmeKeywords.includes("ai"))
|
|
158
|
+
s += 1;
|
|
159
|
+
return s;
|
|
160
|
+
},
|
|
161
|
+
reason: (d) => {
|
|
162
|
+
const bits = [];
|
|
163
|
+
if (d.stack.includes("langgraph"))
|
|
164
|
+
bits.push("LangGraph");
|
|
165
|
+
if (d.stack.includes("crewai"))
|
|
166
|
+
bits.push("CrewAI");
|
|
167
|
+
if (d.stack.includes("autogen"))
|
|
168
|
+
bits.push("AutoGen");
|
|
169
|
+
if (d.stack.includes("mastra"))
|
|
170
|
+
bits.push("Mastra");
|
|
171
|
+
if (d.stack.includes("mcp"))
|
|
172
|
+
bits.push("MCP SDK");
|
|
173
|
+
const vdb = ["pinecone", "weaviate", "chroma", "qdrant"].filter((s) => d.stack.includes(s));
|
|
174
|
+
if (vdb.length)
|
|
175
|
+
bits.push(`vector DB (${vdb.join(",")})`);
|
|
176
|
+
return `agent-product detected — ${bits.join(", ")} — agent-eval + isolation + prompt-injection gates required`;
|
|
177
|
+
},
|
|
178
|
+
},
|
|
121
179
|
// ── ai-system ────────────────────────────────────
|
|
180
|
+
// Lower score than agent-product: an LLM-using app without vector DB / agent FW
|
|
122
181
|
{
|
|
123
182
|
archetype: "ai-system",
|
|
124
183
|
score: (d) => {
|
|
@@ -127,33 +186,126 @@ const RULES = [
|
|
|
127
186
|
s += 4;
|
|
128
187
|
if (d.stack.includes("openai-sdk"))
|
|
129
188
|
s += 3;
|
|
189
|
+
if (d.stack.includes("google-ai"))
|
|
190
|
+
s += 3;
|
|
191
|
+
if (d.stack.includes("aws-bedrock"))
|
|
192
|
+
s += 3;
|
|
193
|
+
if (d.stack.includes("cohere"))
|
|
194
|
+
s += 2;
|
|
195
|
+
if (d.stack.includes("replicate"))
|
|
196
|
+
s += 2;
|
|
130
197
|
if (d.stack.includes("langchain"))
|
|
131
|
-
s +=
|
|
198
|
+
s += 3;
|
|
132
199
|
if (d.stack.includes("llamaindex"))
|
|
133
|
-
s += 4;
|
|
134
|
-
if (d.stack.includes("mcp"))
|
|
135
|
-
s += 5;
|
|
136
|
-
if (d.stack.includes("ml"))
|
|
137
200
|
s += 3;
|
|
201
|
+
if (d.stack.includes("vercel-ai-sdk"))
|
|
202
|
+
s += 3;
|
|
203
|
+
if (d.stack.includes("ml"))
|
|
204
|
+
s += 2;
|
|
205
|
+
// Don't double-score if already an agent-product
|
|
206
|
+
const agents = ["langgraph", "crewai", "autogen", "mastra", "mcp"];
|
|
207
|
+
if (agents.some((a) => d.stack.includes(a)))
|
|
208
|
+
s = Math.max(0, s - 2);
|
|
209
|
+
const vdbs = ["pinecone", "weaviate", "chroma", "qdrant"];
|
|
210
|
+
if (vdbs.some((v) => d.stack.includes(v)))
|
|
211
|
+
s = Math.max(0, s - 2);
|
|
138
212
|
return s;
|
|
139
213
|
},
|
|
140
214
|
reason: (d) => {
|
|
141
215
|
const bits = [];
|
|
142
|
-
if (d.stack.includes("mcp"))
|
|
143
|
-
bits.push("MCP SDK");
|
|
144
216
|
if (d.stack.includes("anthropic-sdk"))
|
|
145
217
|
bits.push("Anthropic SDK");
|
|
146
218
|
if (d.stack.includes("openai-sdk"))
|
|
147
219
|
bits.push("OpenAI SDK");
|
|
220
|
+
if (d.stack.includes("google-ai"))
|
|
221
|
+
bits.push("Gemini");
|
|
222
|
+
if (d.stack.includes("aws-bedrock"))
|
|
223
|
+
bits.push("AWS Bedrock");
|
|
148
224
|
if (d.stack.includes("langchain"))
|
|
149
225
|
bits.push("LangChain");
|
|
150
226
|
if (d.stack.includes("llamaindex"))
|
|
151
227
|
bits.push("LlamaIndex");
|
|
228
|
+
if (d.stack.includes("vercel-ai-sdk"))
|
|
229
|
+
bits.push("Vercel AI SDK");
|
|
152
230
|
if (d.stack.includes("ml"))
|
|
153
231
|
bits.push("ML stack");
|
|
154
|
-
return `AI/LLM tooling detected (${bits.join(", ")}) —
|
|
232
|
+
return `AI/LLM tooling detected (${bits.join(", ")}) — prompt injection + output sanitization gates`;
|
|
155
233
|
},
|
|
156
234
|
},
|
|
235
|
+
// ── fintech (Plaid, banking integrations) ────────
|
|
236
|
+
{
|
|
237
|
+
archetype: "fintech",
|
|
238
|
+
score: (d) => {
|
|
239
|
+
let s = 0;
|
|
240
|
+
if (d.stack.includes("plaid"))
|
|
241
|
+
s += 10;
|
|
242
|
+
if (d.stack.includes("wise"))
|
|
243
|
+
s += 9;
|
|
244
|
+
if (d.stack.includes("dwolla"))
|
|
245
|
+
s += 9;
|
|
246
|
+
if (d.stack.includes("teller"))
|
|
247
|
+
s += 9;
|
|
248
|
+
if (d.stack.includes("fintech"))
|
|
249
|
+
s += 7;
|
|
250
|
+
if (d.readmeKeywords.includes("fintech"))
|
|
251
|
+
s += 2;
|
|
252
|
+
return s;
|
|
253
|
+
},
|
|
254
|
+
reason: (d) => {
|
|
255
|
+
const bits = [];
|
|
256
|
+
if (d.stack.includes("plaid"))
|
|
257
|
+
bits.push("Plaid");
|
|
258
|
+
if (d.stack.includes("wise"))
|
|
259
|
+
bits.push("Wise");
|
|
260
|
+
if (d.stack.includes("dwolla"))
|
|
261
|
+
bits.push("Dwolla");
|
|
262
|
+
if (d.stack.includes("teller"))
|
|
263
|
+
bits.push("Teller");
|
|
264
|
+
return `fintech integration: ${bits.join(", ")} — SOX, PCI, KYC/AML compliance gates`;
|
|
265
|
+
},
|
|
266
|
+
},
|
|
267
|
+
// ── healthcare (FHIR/HL7/PHI) ───────────────────
|
|
268
|
+
{
|
|
269
|
+
archetype: "healthcare",
|
|
270
|
+
score: (d) => {
|
|
271
|
+
let s = 0;
|
|
272
|
+
if (d.stack.includes("fhir"))
|
|
273
|
+
s += 7;
|
|
274
|
+
if (d.stack.includes("hl7"))
|
|
275
|
+
s += 6;
|
|
276
|
+
if (d.readmeKeywords.includes("healthcare"))
|
|
277
|
+
s += 3;
|
|
278
|
+
return s;
|
|
279
|
+
},
|
|
280
|
+
reason: (d) => {
|
|
281
|
+
const bits = [];
|
|
282
|
+
if (d.stack.includes("fhir"))
|
|
283
|
+
bits.push("FHIR");
|
|
284
|
+
if (d.stack.includes("hl7"))
|
|
285
|
+
bits.push("HL7");
|
|
286
|
+
return `healthcare data tooling: ${bits.join(", ")} — HIPAA/PHI handling gates required`;
|
|
287
|
+
},
|
|
288
|
+
},
|
|
289
|
+
// ── cli-tool (explicit CLI: bin field + cli entry) ─
|
|
290
|
+
{
|
|
291
|
+
archetype: "cli-tool",
|
|
292
|
+
score: (d) => {
|
|
293
|
+
let s = 0;
|
|
294
|
+
// Explicit cli marker from detect.ts (bin field present)
|
|
295
|
+
if (d.stack.includes("cli"))
|
|
296
|
+
s += 6;
|
|
297
|
+
if (d.codeStructure.hasCliEntry)
|
|
298
|
+
s += 2;
|
|
299
|
+
if (d.readmeKeywords.includes("cli"))
|
|
300
|
+
s += 1;
|
|
301
|
+
// Penalize if web framework present (CLI + web is rare)
|
|
302
|
+
const webFw = ["next.js", "express", "fastify", "nestjs", "django", "fastapi", "flask", "hono", "koa"];
|
|
303
|
+
if (webFw.some((w) => d.stack.includes(w)))
|
|
304
|
+
s = Math.max(0, s - 4);
|
|
305
|
+
return s;
|
|
306
|
+
},
|
|
307
|
+
reason: (_d) => "package.json bin field + CLI entry detected — argument-parsing, exit-code, --help gates",
|
|
308
|
+
},
|
|
157
309
|
// ── mobile-app ───────────────────────────────────
|
|
158
310
|
{
|
|
159
311
|
archetype: "mobile-app",
|
|
@@ -183,8 +335,34 @@ const RULES = [
|
|
|
183
335
|
// ── data-platform ────────────────────────────────
|
|
184
336
|
{
|
|
185
337
|
archetype: "data-platform",
|
|
186
|
-
score: (d) =>
|
|
187
|
-
|
|
338
|
+
score: (d) => {
|
|
339
|
+
let s = 0;
|
|
340
|
+
if (d.stack.includes("data-pipeline"))
|
|
341
|
+
s += 6;
|
|
342
|
+
if (d.stack.includes("dbt"))
|
|
343
|
+
s += 3;
|
|
344
|
+
if (d.stack.includes("dagster"))
|
|
345
|
+
s += 3;
|
|
346
|
+
if (d.stack.includes("polars"))
|
|
347
|
+
s += 2;
|
|
348
|
+
if (d.stack.includes("iceberg"))
|
|
349
|
+
s += 3;
|
|
350
|
+
if (d.stack.includes("duckdb"))
|
|
351
|
+
s += 2;
|
|
352
|
+
if (d.readmeKeywords.includes("data"))
|
|
353
|
+
s += 1;
|
|
354
|
+
return s;
|
|
355
|
+
},
|
|
356
|
+
reason: (d) => {
|
|
357
|
+
const bits = [];
|
|
358
|
+
if (d.stack.includes("data-pipeline"))
|
|
359
|
+
bits.push("pandas/airflow/prefect");
|
|
360
|
+
if (d.stack.includes("dbt"))
|
|
361
|
+
bits.push("dbt");
|
|
362
|
+
if (d.stack.includes("dagster"))
|
|
363
|
+
bits.push("dagster");
|
|
364
|
+
return `data pipeline tooling: ${bits.join(", ") || "detected"}`;
|
|
365
|
+
},
|
|
188
366
|
},
|
|
189
367
|
// ── infra ────────────────────────────────────────
|
|
190
368
|
{
|
|
@@ -224,34 +402,70 @@ const RULES = [
|
|
|
224
402
|
archetype: "web-service",
|
|
225
403
|
score: (d) => {
|
|
226
404
|
let s = 0;
|
|
227
|
-
const
|
|
228
|
-
"
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
405
|
+
const serverFrameworks = ["express", "fastify", "nestjs", "hono", "koa",
|
|
406
|
+
"django", "fastapi", "flask", "gin", "echo", "chi"];
|
|
407
|
+
const fullstackFw = ["next.js", "nuxt", "remix", "sveltekit", "astro"];
|
|
408
|
+
const uiOnly = ["react", "vue", "angular", "svelte"];
|
|
409
|
+
for (const fw of serverFrameworks)
|
|
410
|
+
if (d.stack.includes(fw))
|
|
411
|
+
s += 3;
|
|
412
|
+
for (const fw of fullstackFw)
|
|
413
|
+
if (d.stack.includes(fw))
|
|
414
|
+
s += 4;
|
|
415
|
+
// UI-only: weaker signal (could be a library)
|
|
416
|
+
for (const fw of uiOnly)
|
|
233
417
|
if (d.stack.includes(fw))
|
|
234
418
|
s += 1;
|
|
235
419
|
if (s > 0)
|
|
236
|
-
s +=
|
|
420
|
+
s += 1; // baseline web bonus
|
|
421
|
+
// Code-structure boost
|
|
422
|
+
if (d.codeStructure.hasRoutesDir)
|
|
423
|
+
s += 2;
|
|
424
|
+
if (d.codeStructure.hasServerEntry)
|
|
425
|
+
s += 2;
|
|
426
|
+
if (d.scripts.hasStart || d.scripts.hasDev)
|
|
427
|
+
s += 1;
|
|
428
|
+
// Penalize if explicitly a library (publishConfig/cli)
|
|
429
|
+
if (d.stack.includes("library") && !d.codeStructure.hasRoutesDir && !d.codeStructure.hasServerEntry) {
|
|
430
|
+
s = Math.max(0, s - 3);
|
|
431
|
+
}
|
|
237
432
|
return s;
|
|
238
433
|
},
|
|
239
434
|
reason: (d) => {
|
|
240
|
-
const fw = d.stack.find((t) => ["next.js", "
|
|
241
|
-
|
|
435
|
+
const fw = d.stack.find((t) => ["next.js", "express", "fastify", "nestjs", "hono", "koa",
|
|
436
|
+
"django", "fastapi", "flask", "gin", "echo"].includes(t));
|
|
437
|
+
const extras = [];
|
|
438
|
+
if (d.codeStructure.hasRoutesDir)
|
|
439
|
+
extras.push("routes/");
|
|
440
|
+
if (d.codeStructure.hasServerEntry)
|
|
441
|
+
extras.push("server entry");
|
|
442
|
+
return `web framework detected: ${fw ?? "unknown"}${extras.length ? " + " + extras.join(", ") : ""}`;
|
|
242
443
|
},
|
|
243
444
|
},
|
|
244
445
|
// ── library (no app framework, just code) ────────
|
|
245
|
-
//
|
|
246
|
-
// Fallback: plain runtime + no web/mobile/infra framework (low confidence)
|
|
446
|
+
// Strong signal: detect.ts marked "library" + no web/server structure
|
|
247
447
|
{
|
|
248
448
|
archetype: "library",
|
|
249
449
|
score: (d) => {
|
|
250
|
-
|
|
251
|
-
|
|
450
|
+
const isExplicitLib = d.stack.includes("library");
|
|
451
|
+
const isExplicitCli = d.stack.includes("cli");
|
|
452
|
+
// Don't claim library if web-service shape is obvious
|
|
453
|
+
const looksLikeWebService = d.codeStructure.hasRoutesDir || d.codeStructure.hasServerEntry;
|
|
454
|
+
if (looksLikeWebService)
|
|
455
|
+
return 0;
|
|
456
|
+
// Don't claim library if data-pipeline / ml signals dominate
|
|
457
|
+
if (d.stack.includes("data-pipeline") || d.stack.includes("ml"))
|
|
458
|
+
return 0;
|
|
459
|
+
// Don't claim library if a domain-specific archetype is clearly present
|
|
460
|
+
const domainSignals = ["plaid", "wise", "dwolla", "fhir", "hl7", "stripe", "shopify", "solidity",
|
|
461
|
+
"embedded", "unity", "unreal", "godot", "react-native", "expo"];
|
|
462
|
+
if (domainSignals.some((s) => d.stack.includes(s)))
|
|
463
|
+
return 0;
|
|
464
|
+
if (isExplicitLib && !isExplicitCli)
|
|
252
465
|
return 7;
|
|
253
|
-
|
|
254
|
-
|
|
466
|
+
if (isExplicitLib && isExplicitCli)
|
|
467
|
+
return 4; // cli-tool rule will outscore
|
|
468
|
+
// Weaker signal: plain runtime + no app framework
|
|
255
469
|
const hasApp = d.stack.some((t) => ["next.js", "django", "fastapi", "flask", "express", "fastify", "nestjs", "hono",
|
|
256
470
|
"react-native", "expo", "tauri", "capacitor", "flutter",
|
|
257
471
|
"terraform", "pulumi", "aws-cdk", "helm",
|
|
@@ -265,18 +479,35 @@ const RULES = [
|
|
|
265
479
|
return 0;
|
|
266
480
|
},
|
|
267
481
|
reason: (d) => {
|
|
268
|
-
if (d.stack.includes("library")
|
|
269
|
-
return "package.json/pyproject/Cargo.toml indicates a publishable library
|
|
482
|
+
if (d.stack.includes("library")) {
|
|
483
|
+
return "package.json/pyproject/Cargo.toml indicates a publishable library";
|
|
270
484
|
}
|
|
271
485
|
return "no web/mobile/infra framework detected — looks like a library/SDK";
|
|
272
486
|
},
|
|
273
487
|
},
|
|
274
488
|
];
|
|
489
|
+
// Tie-break priority — when two rules score equally, prefer the one
|
|
490
|
+
// higher in this list (more specific / domain-bound first).
|
|
491
|
+
const TIE_BREAK_PRIORITY = [
|
|
492
|
+
"browser-extension", "iot-embedded", "web3", "game",
|
|
493
|
+
"agent-product", "fintech", "healthcare",
|
|
494
|
+
"commerce", "ai-system", "devtools",
|
|
495
|
+
"data-platform", "infra", "mobile-app",
|
|
496
|
+
"cli-tool", "web-service", "library", "regulated", "greenfield",
|
|
497
|
+
];
|
|
498
|
+
function priorityIndex(a) {
|
|
499
|
+
const i = TIE_BREAK_PRIORITY.indexOf(a);
|
|
500
|
+
return i < 0 ? TIE_BREAK_PRIORITY.length : i;
|
|
501
|
+
}
|
|
275
502
|
export function pickArchetype(d) {
|
|
276
503
|
const scored = RULES
|
|
277
504
|
.map((r) => ({ archetype: r.archetype, score: r.score(d), reason: r.reason(d) }))
|
|
278
505
|
.filter((r) => r.score > 0)
|
|
279
|
-
.sort((a, b) =>
|
|
506
|
+
.sort((a, b) => {
|
|
507
|
+
if (b.score !== a.score)
|
|
508
|
+
return b.score - a.score;
|
|
509
|
+
return priorityIndex(a.archetype) - priorityIndex(b.archetype);
|
|
510
|
+
});
|
|
280
511
|
if (scored.length === 0) {
|
|
281
512
|
return {
|
|
282
513
|
primary: "greenfield",
|
|
@@ -288,30 +519,58 @@ export function pickArchetype(d) {
|
|
|
288
519
|
const top = scored[0];
|
|
289
520
|
const nextBest = scored[1]?.score ?? 0;
|
|
290
521
|
const gap = top.score - nextBest;
|
|
291
|
-
|
|
292
|
-
|
|
522
|
+
// Confidence: high when score≥6 and gap≥3; medium ≥4; else low
|
|
523
|
+
const confidence = top.score >= 6 && gap >= 3 ? "high" :
|
|
524
|
+
top.score >= 4 && gap >= 1 ? "medium" : "low";
|
|
525
|
+
// Dedupe alternatives, keep top 3
|
|
526
|
+
const seen = new Set([top.archetype]);
|
|
527
|
+
const alternatives = [];
|
|
528
|
+
for (const r of scored.slice(1)) {
|
|
529
|
+
if (!seen.has(r.archetype)) {
|
|
530
|
+
seen.add(r.archetype);
|
|
531
|
+
alternatives.push(r.archetype);
|
|
532
|
+
if (alternatives.length >= 3)
|
|
533
|
+
break;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
293
536
|
return {
|
|
294
537
|
primary: top.archetype,
|
|
295
538
|
confidence,
|
|
296
539
|
rationale: top.reason,
|
|
297
|
-
alternatives
|
|
540
|
+
alternatives,
|
|
298
541
|
};
|
|
299
542
|
}
|
|
300
|
-
// Compliance hints — auto-suggested based on stack.
|
|
543
|
+
// Compliance hints — auto-suggested based on stack and README.
|
|
301
544
|
export function suggestCompliance(d, archetype) {
|
|
302
545
|
const c = new Set();
|
|
546
|
+
// ── archetype defaults ───────────────────────────
|
|
303
547
|
if (archetype === "commerce") {
|
|
304
548
|
c.add("pci-dss");
|
|
305
549
|
c.add("gdpr");
|
|
306
550
|
}
|
|
307
|
-
if (archetype === "
|
|
551
|
+
if (archetype === "fintech") {
|
|
552
|
+
c.add("pci-dss");
|
|
553
|
+
c.add("sox");
|
|
554
|
+
c.add("kyc-aml");
|
|
555
|
+
c.add("gdpr");
|
|
556
|
+
}
|
|
557
|
+
if (archetype === "healthcare") {
|
|
558
|
+
c.add("hipaa");
|
|
559
|
+
c.add("gdpr");
|
|
560
|
+
c.add("hitech");
|
|
561
|
+
}
|
|
562
|
+
if (archetype === "ai-system" || archetype === "agent-product") {
|
|
308
563
|
c.add("eu-ai-act");
|
|
564
|
+
// Add OWASP LLM only if it's truly an agent product
|
|
565
|
+
if (archetype === "agent-product")
|
|
566
|
+
c.add("owasp-llm-top-10");
|
|
309
567
|
}
|
|
310
568
|
if (archetype === "web3") {
|
|
311
569
|
c.add("soc2");
|
|
312
570
|
}
|
|
313
571
|
if (archetype === "iot-embedded") {
|
|
314
572
|
c.add("iso27001");
|
|
573
|
+
c.add("etsi-en-303-645");
|
|
315
574
|
}
|
|
316
575
|
if (archetype === "browser-extension") {
|
|
317
576
|
c.add("csp");
|
|
@@ -329,10 +588,29 @@ export function suggestCompliance(d, archetype) {
|
|
|
329
588
|
c.add("soc2-type-2");
|
|
330
589
|
c.add("gdpr");
|
|
331
590
|
}
|
|
332
|
-
if (d.stack.includes("stripe"))
|
|
333
|
-
c.add("pci-dss");
|
|
334
|
-
// Reasonable default for any web service storing user data
|
|
335
591
|
if (archetype === "web-service")
|
|
336
|
-
c.add("gdpr");
|
|
592
|
+
c.add("gdpr"); // baseline for user data
|
|
593
|
+
if (archetype === "cli-tool") { /* CLI tools usually don't have compliance load */ }
|
|
594
|
+
// ── stack-derived (cross-archetype) ──────────────
|
|
595
|
+
if (d.stack.includes("stripe") || d.stack.includes("braintree") ||
|
|
596
|
+
d.stack.includes("adyen") || d.stack.includes("paddle")) {
|
|
597
|
+
c.add("pci-dss");
|
|
598
|
+
}
|
|
599
|
+
if (d.stack.includes("plaid") || d.stack.includes("dwolla") ||
|
|
600
|
+
d.stack.includes("teller") || d.stack.includes("wise")) {
|
|
601
|
+
c.add("kyc-aml");
|
|
602
|
+
c.add("sox");
|
|
603
|
+
}
|
|
604
|
+
if (d.stack.includes("fhir") || d.stack.includes("hl7")) {
|
|
605
|
+
c.add("hipaa");
|
|
606
|
+
c.add("hitech");
|
|
607
|
+
}
|
|
608
|
+
// ── README-derived ───────────────────────────────
|
|
609
|
+
if (d.readmeKeywords.includes("regulated"))
|
|
610
|
+
c.add("compliance-required");
|
|
611
|
+
if (d.readmeKeywords.includes("healthcare"))
|
|
612
|
+
c.add("hipaa");
|
|
613
|
+
if (d.readmeKeywords.includes("fintech"))
|
|
614
|
+
c.add("sox");
|
|
337
615
|
return Array.from(c).sort();
|
|
338
616
|
}
|
package/dist/bootstrap.js
CHANGED
|
@@ -25,7 +25,7 @@ export function bootstrap(dir, detection, archetype, compliance, detectionMeta)
|
|
|
25
25
|
|
|
26
26
|
primary: ${archetype}
|
|
27
27
|
archetype: ${archetype}
|
|
28
|
-
project_size:
|
|
28
|
+
project_size: ${detection.projectSize}
|
|
29
29
|
stack: ${stackLine}
|
|
30
30
|
languages: ${detection.languages.join(", ") || "to be defined"}
|
|
31
31
|
package-manager: ${detection.packageManager ?? "none"}
|
package/dist/detect.js
CHANGED
|
@@ -73,7 +73,7 @@ export function detect(dir) {
|
|
|
73
73
|
stack.add("openai-sdk");
|
|
74
74
|
sig("ai", "openai");
|
|
75
75
|
}
|
|
76
|
-
if (has("@anthropic-ai/sdk")) {
|
|
76
|
+
if (has("@anthropic-ai/sdk") || has("@anthropic-ai/claude-code")) {
|
|
77
77
|
stack.add("anthropic-sdk");
|
|
78
78
|
sig("ai", "anthropic-sdk");
|
|
79
79
|
}
|
|
@@ -109,6 +109,39 @@ export function detect(dir) {
|
|
|
109
109
|
stack.add("vercel-ai-sdk");
|
|
110
110
|
sig("ai", "vercel-ai-sdk");
|
|
111
111
|
}
|
|
112
|
+
// Vector databases (when paired with LLM SDK → agent-product)
|
|
113
|
+
if (has("@pinecone-database/pinecone")) {
|
|
114
|
+
stack.add("pinecone");
|
|
115
|
+
sig("vectordb", "pinecone");
|
|
116
|
+
}
|
|
117
|
+
if (has("weaviate-ts-client") || has("weaviate-client")) {
|
|
118
|
+
stack.add("weaviate");
|
|
119
|
+
sig("vectordb", "weaviate");
|
|
120
|
+
}
|
|
121
|
+
if (has("chromadb")) {
|
|
122
|
+
stack.add("chroma");
|
|
123
|
+
sig("vectordb", "chroma");
|
|
124
|
+
}
|
|
125
|
+
if (has("@qdrant/js-client-rest")) {
|
|
126
|
+
stack.add("qdrant");
|
|
127
|
+
sig("vectordb", "qdrant");
|
|
128
|
+
}
|
|
129
|
+
if (has("@google/generative-ai") || has("@google-ai/generativelanguage")) {
|
|
130
|
+
stack.add("google-ai");
|
|
131
|
+
sig("ai", "gemini");
|
|
132
|
+
}
|
|
133
|
+
if (has("@aws-sdk/client-bedrock-runtime")) {
|
|
134
|
+
stack.add("aws-bedrock");
|
|
135
|
+
sig("ai", "bedrock");
|
|
136
|
+
}
|
|
137
|
+
if (has("cohere-ai")) {
|
|
138
|
+
stack.add("cohere");
|
|
139
|
+
sig("ai", "cohere");
|
|
140
|
+
}
|
|
141
|
+
if (has("replicate")) {
|
|
142
|
+
stack.add("replicate");
|
|
143
|
+
sig("ai", "replicate");
|
|
144
|
+
}
|
|
112
145
|
// Payments / commerce
|
|
113
146
|
if (has("stripe") || has("@stripe/stripe-js")) {
|
|
114
147
|
stack.add("stripe");
|
|
@@ -138,6 +171,41 @@ export function detect(dir) {
|
|
|
138
171
|
stack.add("polar");
|
|
139
172
|
sig("commerce", "polar");
|
|
140
173
|
}
|
|
174
|
+
// Fintech (banking, ACH, regulated payments)
|
|
175
|
+
if (has("plaid")) {
|
|
176
|
+
stack.add("plaid");
|
|
177
|
+
sig("fintech", "plaid");
|
|
178
|
+
}
|
|
179
|
+
if (has("@wise-api/api-client") || has("wise")) {
|
|
180
|
+
stack.add("wise");
|
|
181
|
+
sig("fintech", "wise");
|
|
182
|
+
}
|
|
183
|
+
if (has("dwolla-v2")) {
|
|
184
|
+
stack.add("dwolla");
|
|
185
|
+
sig("fintech", "dwolla");
|
|
186
|
+
}
|
|
187
|
+
if (has("@teller/teller")) {
|
|
188
|
+
stack.add("teller");
|
|
189
|
+
sig("fintech", "teller");
|
|
190
|
+
}
|
|
191
|
+
// Stripe Connect/Issuing → fintech (not just commerce)
|
|
192
|
+
if (has("stripe") && (pkg.name?.includes("bank") || pkg.name?.includes("card"))) {
|
|
193
|
+
sig("fintech", "stripe-connect");
|
|
194
|
+
stack.add("fintech");
|
|
195
|
+
}
|
|
196
|
+
// Healthcare / FHIR
|
|
197
|
+
if (has("fhir") || has("fhir.js") || has("@types/fhir")) {
|
|
198
|
+
stack.add("fhir");
|
|
199
|
+
sig("healthcare", "fhir");
|
|
200
|
+
}
|
|
201
|
+
if (has("@smile-cdr/fhirts")) {
|
|
202
|
+
stack.add("fhir");
|
|
203
|
+
sig("healthcare", "smile-cdr");
|
|
204
|
+
}
|
|
205
|
+
if (has("hl7")) {
|
|
206
|
+
stack.add("hl7");
|
|
207
|
+
sig("healthcare", "hl7");
|
|
208
|
+
}
|
|
141
209
|
// Auth
|
|
142
210
|
if (has("next-auth") || has("@auth/core"))
|
|
143
211
|
stack.add("auth");
|
|
@@ -190,17 +258,31 @@ export function detect(dir) {
|
|
|
190
258
|
}
|
|
191
259
|
// Library detection (npm package intended for distribution)
|
|
192
260
|
// Signals: has "main" or "exports", NOT "private:true", NOT a typical app structure
|
|
261
|
+
// Negative signal: any backend framework dep → likely a server, not a library
|
|
262
|
+
const SERVER_FRAMEWORKS = ["express", "fastify", "nestjs", "@nestjs/core", "hono", "koa", "@hapi/hapi", "restify", "polka"];
|
|
263
|
+
const hasServerFramework = SERVER_FRAMEWORKS.some((f) => has(f));
|
|
264
|
+
const FULLSTACK_FRAMEWORKS = ["next", "nuxt", "@remix-run/node", "@sveltejs/kit", "astro", "gatsby"];
|
|
265
|
+
const hasFullstack = FULLSTACK_FRAMEWORKS.some((f) => has(f));
|
|
193
266
|
const isPublishable = !pkg.private && (pkg.main || pkg.exports || pkg.module || pkg.type === "module");
|
|
194
267
|
const hasAppStructure = existsSync(join(dir, "pages")) ||
|
|
195
268
|
existsSync(join(dir, "app")) ||
|
|
196
269
|
existsSync(join(dir, "src/pages")) ||
|
|
197
270
|
existsSync(join(dir, "src/app")) ||
|
|
198
|
-
existsSync(join(dir, "public/index.html"))
|
|
271
|
+
existsSync(join(dir, "public/index.html")) ||
|
|
272
|
+
existsSync(join(dir, "routes")) ||
|
|
273
|
+
existsSync(join(dir, "src/routes")) ||
|
|
274
|
+
existsSync(join(dir, "api"));
|
|
199
275
|
const hasBin = !!pkg.bin;
|
|
200
|
-
|
|
276
|
+
// Strong library signal: explicit publishConfig or files field listing dist
|
|
277
|
+
const hasExplicitLibConfig = !!pkg.publishConfig || (Array.isArray(pkg.files) && !hasServerFramework);
|
|
278
|
+
if (isPublishable && !hasAppStructure && !hasServerFramework && !hasFullstack) {
|
|
201
279
|
stack.add("library");
|
|
202
280
|
sig("library", "package.json");
|
|
203
281
|
}
|
|
282
|
+
else if (hasExplicitLibConfig && !hasServerFramework && !hasFullstack) {
|
|
283
|
+
stack.add("library");
|
|
284
|
+
sig("library", "publishConfig");
|
|
285
|
+
}
|
|
204
286
|
if (hasBin) {
|
|
205
287
|
stack.add("cli");
|
|
206
288
|
sig("library", "bin");
|
|
@@ -633,6 +715,63 @@ export function detect(dir) {
|
|
|
633
715
|
existsSync(join(dir, "azure-pipelines.yml"));
|
|
634
716
|
const hasExistingGreatCto = existsSync(join(dir, ".great_cto", "PROJECT.md")) ||
|
|
635
717
|
existsSync(join(dir, ".great_cto", "SKILL.md"));
|
|
718
|
+
// ── Code-structure signals (Wave 1) ──────────────────────
|
|
719
|
+
const hasRoutesDir = existsSync(join(dir, "routes")) ||
|
|
720
|
+
existsSync(join(dir, "app")) ||
|
|
721
|
+
existsSync(join(dir, "pages")) ||
|
|
722
|
+
existsSync(join(dir, "src/api")) ||
|
|
723
|
+
existsSync(join(dir, "src/routes")) ||
|
|
724
|
+
existsSync(join(dir, "src/pages")) ||
|
|
725
|
+
existsSync(join(dir, "src/app")) ||
|
|
726
|
+
existsSync(join(dir, "api"));
|
|
727
|
+
const hasCliEntry = existsSync(join(dir, "bin")) ||
|
|
728
|
+
existsSync(join(dir, "src/cli.ts")) ||
|
|
729
|
+
existsSync(join(dir, "src/cli.js")) ||
|
|
730
|
+
existsSync(join(dir, "src/main.ts")) && pkg.bin != null ||
|
|
731
|
+
existsSync(join(dir, "cmd"));
|
|
732
|
+
const hasPublicHtml = existsSync(join(dir, "public/index.html")) ||
|
|
733
|
+
existsSync(join(dir, "index.html")) ||
|
|
734
|
+
existsSync(join(dir, "src/index.html"));
|
|
735
|
+
const hasServerEntry = existsSync(join(dir, "server.ts")) || existsSync(join(dir, "server.js")) ||
|
|
736
|
+
existsSync(join(dir, "server.mjs")) ||
|
|
737
|
+
existsSync(join(dir, "src/server.ts")) || existsSync(join(dir, "src/server.js")) ||
|
|
738
|
+
existsSync(join(dir, "app.ts")) || existsSync(join(dir, "app.js")) ||
|
|
739
|
+
existsSync(join(dir, "src/app.ts")) || existsSync(join(dir, "src/app.js"));
|
|
740
|
+
const hasMonorepo = existsSync(join(dir, "pnpm-workspace.yaml")) ||
|
|
741
|
+
existsSync(join(dir, "lerna.json")) ||
|
|
742
|
+
existsSync(join(dir, "nx.json")) ||
|
|
743
|
+
existsSync(join(dir, "turbo.json")) ||
|
|
744
|
+
existsSync(join(dir, "rush.json"));
|
|
745
|
+
if (hasMonorepo)
|
|
746
|
+
sig("monorepo", "workspace-config");
|
|
747
|
+
if (hasRoutesDir)
|
|
748
|
+
sig("structure", "routes-dir");
|
|
749
|
+
if (hasCliEntry)
|
|
750
|
+
sig("structure", "cli-entry");
|
|
751
|
+
if (hasServerEntry)
|
|
752
|
+
sig("structure", "server-entry");
|
|
753
|
+
// ── npm-scripts heuristics (Wave 1) ──────────────────────
|
|
754
|
+
const scripts = pkg.scripts || {};
|
|
755
|
+
const scriptHints = {
|
|
756
|
+
hasStart: !!scripts.start,
|
|
757
|
+
hasDev: !!scripts.dev,
|
|
758
|
+
hasBuild: !!scripts.build,
|
|
759
|
+
hasPublish: !!scripts.prepublishOnly || !!scripts.publish,
|
|
760
|
+
};
|
|
761
|
+
if (scriptHints.hasPublish && pkg.publishConfig?.access === "public") {
|
|
762
|
+
sig("library", "publishConfig");
|
|
763
|
+
stack.add("library");
|
|
764
|
+
}
|
|
765
|
+
// server-ish scripts AND server entry → web-service indicator
|
|
766
|
+
if ((scriptHints.hasStart || scriptHints.hasDev) && hasServerEntry) {
|
|
767
|
+
sig("structure", "web-service-shape");
|
|
768
|
+
}
|
|
769
|
+
// ── Project size estimate (Wave 3 lite) ──────────────────
|
|
770
|
+
const projectSize = estimateProjectSize(dir);
|
|
771
|
+
// ── README mining (Wave 2) ───────────────────────────────
|
|
772
|
+
const readmeKeywords = mineReadmeKeywords(dir);
|
|
773
|
+
for (const kw of readmeKeywords)
|
|
774
|
+
sig(`readme:${kw}`, "README");
|
|
636
775
|
return {
|
|
637
776
|
stack: Array.from(stack).sort(),
|
|
638
777
|
languages: Array.from(languages).sort(),
|
|
@@ -641,9 +780,106 @@ export function detect(dir) {
|
|
|
641
780
|
hasTests,
|
|
642
781
|
hasCI,
|
|
643
782
|
hasExistingGreatCto,
|
|
783
|
+
codeStructure: {
|
|
784
|
+
hasRoutesDir,
|
|
785
|
+
hasCliEntry,
|
|
786
|
+
hasPublicHtml,
|
|
787
|
+
hasServerEntry,
|
|
788
|
+
hasMonorepo,
|
|
789
|
+
},
|
|
790
|
+
scripts: scriptHints,
|
|
791
|
+
projectSize,
|
|
792
|
+
readmeKeywords,
|
|
644
793
|
};
|
|
645
794
|
}
|
|
646
795
|
// ── helpers ──────────────────────────────────────────────────
|
|
796
|
+
/**
|
|
797
|
+
* Estimate project size from file count (no LOC, to stay fast).
|
|
798
|
+
* Skips node_modules, .git, dist, build, .next, target, vendor.
|
|
799
|
+
*/
|
|
800
|
+
function estimateProjectSize(dir) {
|
|
801
|
+
const SKIP = new Set(["node_modules", ".git", "dist", "build", ".next", ".nuxt",
|
|
802
|
+
"target", "vendor", ".venv", "venv", "__pycache__", ".cache",
|
|
803
|
+
"coverage", ".turbo", ".vercel", ".wrangler"]);
|
|
804
|
+
const SOURCE_EXTS = /\.(ts|tsx|js|jsx|mjs|cjs|py|rs|go|java|kt|swift|rb|php|c|cpp|h|hpp|cs|sol|sql|css|scss|html|md)$/;
|
|
805
|
+
let count = 0;
|
|
806
|
+
const MAX = 5000; // hard cap to keep scan O(seconds)
|
|
807
|
+
function walk(d, depth) {
|
|
808
|
+
if (count > MAX || depth > 6)
|
|
809
|
+
return;
|
|
810
|
+
try {
|
|
811
|
+
const entries = readdirSync(d);
|
|
812
|
+
for (const e of entries) {
|
|
813
|
+
if (count > MAX)
|
|
814
|
+
return;
|
|
815
|
+
if (e.startsWith(".") && depth === 0 && e !== ".github")
|
|
816
|
+
continue;
|
|
817
|
+
if (SKIP.has(e))
|
|
818
|
+
continue;
|
|
819
|
+
const p = join(d, e);
|
|
820
|
+
try {
|
|
821
|
+
const st = statSync(p);
|
|
822
|
+
if (st.isDirectory())
|
|
823
|
+
walk(p, depth + 1);
|
|
824
|
+
else if (st.isFile() && SOURCE_EXTS.test(e))
|
|
825
|
+
count++;
|
|
826
|
+
}
|
|
827
|
+
catch { /* unreadable */ }
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
catch { /* unreadable */ }
|
|
831
|
+
}
|
|
832
|
+
walk(dir, 0);
|
|
833
|
+
if (count < 10)
|
|
834
|
+
return "nano";
|
|
835
|
+
if (count < 50)
|
|
836
|
+
return "small";
|
|
837
|
+
if (count < 250)
|
|
838
|
+
return "medium";
|
|
839
|
+
if (count < 1000)
|
|
840
|
+
return "large";
|
|
841
|
+
return "enterprise";
|
|
842
|
+
}
|
|
843
|
+
/**
|
|
844
|
+
* Extract a small set of categorical keywords from README.md.
|
|
845
|
+
* Used as a *hint* — not a primary signal.
|
|
846
|
+
*/
|
|
847
|
+
function mineReadmeKeywords(dir) {
|
|
848
|
+
const kws = new Set();
|
|
849
|
+
const path = ["README.md", "readme.md", "README.rst", "README"]
|
|
850
|
+
.map((f) => join(dir, f))
|
|
851
|
+
.find((p) => existsSync(p));
|
|
852
|
+
if (!path)
|
|
853
|
+
return [];
|
|
854
|
+
let text = "";
|
|
855
|
+
try {
|
|
856
|
+
text = readFileSync(path, "utf-8").slice(0, 4000).toLowerCase();
|
|
857
|
+
}
|
|
858
|
+
catch {
|
|
859
|
+
return [];
|
|
860
|
+
}
|
|
861
|
+
// Categorical hints — broad strokes, intentionally conservative
|
|
862
|
+
const buckets = {
|
|
863
|
+
"ai": ["llm", "gpt", "claude", "anthropic", "openai", "embedding", "rag", "agent ", "agentic"],
|
|
864
|
+
"agent": ["multi-agent", " agent ", "autonomous", "orchestrat"],
|
|
865
|
+
"commerce": ["payment", "checkout", "stripe", "subscription", "billing"],
|
|
866
|
+
"fintech": ["bank", "ach", "fintech", "account number", "ledger", "kyc", "aml"],
|
|
867
|
+
"healthcare": ["health", "patient", "medical", "fhir", "hipaa", "phi", "ehr"],
|
|
868
|
+
"web3": ["smart contract", "ethereum", "solidity", "defi", "nft", "wallet"],
|
|
869
|
+
"iot": ["embedded", "firmware", "iot ", "esp32", "esp8266", "arduino"],
|
|
870
|
+
"browser-extension": ["chrome extension", "browser extension", "manifest_version"],
|
|
871
|
+
"library": ["installation", "## install", "## usage", "publish", "npm install"],
|
|
872
|
+
"cli": ["command-line", "command line", "cli ", "$ npx ", "subcommand"],
|
|
873
|
+
"data": ["pipeline", "etl ", "warehouse", "dbt", "airflow", "dataset"],
|
|
874
|
+
"game": ["game ", "gameplay", "player", "level design"],
|
|
875
|
+
"regulated": ["pci", "hipaa", "soc2", "iso 27001", "gdpr", "compliance"],
|
|
876
|
+
};
|
|
877
|
+
for (const [bucket, terms] of Object.entries(buckets)) {
|
|
878
|
+
if (terms.some((t) => text.includes(t)))
|
|
879
|
+
kws.add(bucket);
|
|
880
|
+
}
|
|
881
|
+
return Array.from(kws).sort();
|
|
882
|
+
}
|
|
647
883
|
function safeGlob(dir, pattern, kind = "file") {
|
|
648
884
|
try {
|
|
649
885
|
const entries = readdirSync(dir);
|
package/package.json
CHANGED