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.
@@ -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 += 5;
87
+ s += 7;
85
88
  if (d.stack.includes("shopify"))
86
- s += 5;
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 += 4;
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(", ")}) — security gate mandatory for prompt injection + output sanitization`;
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) => (d.stack.includes("data-pipeline") ? 4 : 0),
187
- reason: (_d) => "data pipeline tooling detected (pandas/airflow/prefect)",
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 webFrameworks = [
228
- "next.js", "react", "vue", "angular", "svelte", "astro",
229
- "express", "fastify", "nestjs", "hono",
230
- "django", "fastapi", "flask",
231
- ];
232
- for (const fw of webFrameworks)
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 += 2; // baseline bonus for any web framework
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", "react", "vue", "angular", "svelte", "astro", "express", "fastify", "nestjs", "hono", "django", "fastapi", "flask"].includes(t));
241
- return `web framework detected: ${fw ?? "unknown"}`;
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
- // Detection priority: explicit "library" or "cli" stack signal from detect.ts (high confidence)
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
- // Strong signal: detect.ts found library indicators
251
- if (d.stack.includes("library") || d.stack.includes("cli")) {
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
- // Weaker signal: no app framework detected
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") || d.stack.includes("cli")) {
269
- return "package.json/pyproject/Cargo.toml indicates a publishable library or CLI";
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) => b.score - a.score);
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
- const confidence = top.score >= 5 && gap >= 2 ? "high" :
292
- top.score >= 3 ? "medium" : "low";
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: scored.slice(1, 4).map((r) => r.archetype),
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 === "ai-system") {
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: medium
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
- if (isPublishable && !hasAppStructure) {
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "great-cto",
3
- "version": "1.0.166",
3
+ "version": "1.0.167",
4
4
  "description": "One command install for the great_cto Claude Code plugin. Auto-detects your stack, picks the right archetype, bootstraps PROJECT.md.",
5
5
  "keywords": [
6
6
  "claude-code",