great-cto 2.76.0 → 2.77.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/board/.claude-plugin/plugin.json +256 -0
- package/board/packages/board/lib/alerts.mjs +413 -0
- package/board/packages/board/lib/beads.mjs +233 -0
- package/board/packages/board/lib/config.mjs +45 -0
- package/board/packages/board/lib/data-readers.mjs +340 -0
- package/board/packages/board/lib/fleet.mjs +363 -0
- package/board/packages/board/lib/metrics.mjs +282 -0
- package/board/packages/board/lib/notifications.mjs +50 -0
- package/board/packages/board/lib/projects.mjs +256 -0
- package/board/packages/board/lib/routes.mjs +1036 -0
- package/board/packages/board/lib/share.mjs +143 -0
- package/board/packages/board/lib/sse.mjs +20 -0
- package/board/packages/board/lib/state.mjs +31 -0
- package/board/packages/board/lib/util.mjs +36 -0
- package/board/packages/board/lib/verdicts.mjs +168 -0
- package/board/packages/board/lib/watchers.mjs +87 -0
- package/board/packages/board/mcp-server.mjs +310 -0
- package/board/packages/board/public/assets/apple-touch-icon.png +0 -0
- package/board/packages/board/public/assets/favicon-16.png +0 -0
- package/board/packages/board/public/assets/favicon-32.png +0 -0
- package/board/packages/board/public/assets/favicon.ico +0 -0
- package/board/packages/board/public/assets/favicon.svg +8 -0
- package/board/packages/board/public/index.html +5452 -0
- package/board/packages/board/public/share.html +1190 -0
- package/board/packages/board/public/sw.js +79 -0
- package/board/packages/board/push-adapter.mjs +222 -0
- package/board/packages/board/server.mjs +133 -0
- package/board/packages/cli/dist/archetypes.js +1461 -0
- package/board/scripts/lib/change-tier.mjs +119 -0
- package/board/scripts/lib/gate-plan.mjs +119 -0
- package/board/scripts/lib/judge-model.mjs +42 -0
- package/dist/board-path.js +47 -0
- package/dist/main.js +3 -31
- package/package.json +3 -1
|
@@ -0,0 +1,1461 @@
|
|
|
1
|
+
// Archetype decision: detected stack → archetype recommendation.
|
|
2
|
+
// Mirrors great_cto's 25 archetypes (+ greenfield fallback) in skills/great_cto/ARCHETYPES.md.
|
|
3
|
+
// Rules are evaluated; highest score wins.
|
|
4
|
+
const RULES = [
|
|
5
|
+
// ── browser-extension ─────────────────────────────
|
|
6
|
+
// High priority: explicit MV3 manifest signal beats web-service / library
|
|
7
|
+
{
|
|
8
|
+
archetype: "browser-extension",
|
|
9
|
+
score: (d) => (d.stack.includes("browser-extension") ? 8 : 0),
|
|
10
|
+
reason: (_d) => "manifest.json with manifest_version detected — Chrome/Firefox/Edge extension",
|
|
11
|
+
},
|
|
12
|
+
// ── game ─────────────────────────────────────────
|
|
13
|
+
// High priority: Unity / Unreal / Godot signals beat library
|
|
14
|
+
{
|
|
15
|
+
archetype: "game",
|
|
16
|
+
score: (d) => {
|
|
17
|
+
let s = 0;
|
|
18
|
+
if (d.stack.includes("unity"))
|
|
19
|
+
s += 8;
|
|
20
|
+
if (d.stack.includes("unreal"))
|
|
21
|
+
s += 8;
|
|
22
|
+
if (d.stack.includes("godot"))
|
|
23
|
+
s += 8;
|
|
24
|
+
if (d.stack.includes("phaser"))
|
|
25
|
+
s += 6;
|
|
26
|
+
if (d.stack.includes("cocos"))
|
|
27
|
+
s += 6;
|
|
28
|
+
return s;
|
|
29
|
+
},
|
|
30
|
+
reason: (d) => {
|
|
31
|
+
const bits = [];
|
|
32
|
+
if (d.stack.includes("unity"))
|
|
33
|
+
bits.push("Unity");
|
|
34
|
+
if (d.stack.includes("unreal"))
|
|
35
|
+
bits.push("Unreal");
|
|
36
|
+
if (d.stack.includes("godot"))
|
|
37
|
+
bits.push("Godot");
|
|
38
|
+
if (d.stack.includes("phaser"))
|
|
39
|
+
bits.push("Phaser");
|
|
40
|
+
if (d.stack.includes("cocos"))
|
|
41
|
+
bits.push("Cocos");
|
|
42
|
+
return `game engine detected: ${bits.join(", ")} — netcode/anti-cheat/age-rating gates`;
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
// ── devtools ─────────────────────────────────────
|
|
46
|
+
// High priority: OpenAPI + multi-language SDK presence beats library
|
|
47
|
+
{
|
|
48
|
+
archetype: "devtools",
|
|
49
|
+
score: (d) => {
|
|
50
|
+
let s = 0;
|
|
51
|
+
if (d.stack.includes("devtools-api"))
|
|
52
|
+
s += 7;
|
|
53
|
+
if (d.stack.includes("openapi-spec"))
|
|
54
|
+
s += 8;
|
|
55
|
+
if (d.stack.includes("graphql-schema"))
|
|
56
|
+
s += 6;
|
|
57
|
+
if (d.stack.includes("multi-sdk"))
|
|
58
|
+
s += 4;
|
|
59
|
+
if (d.stack.includes("stainless"))
|
|
60
|
+
s += 4;
|
|
61
|
+
if (d.stack.includes("docs-platform"))
|
|
62
|
+
s += 2;
|
|
63
|
+
return s;
|
|
64
|
+
},
|
|
65
|
+
reason: (d) => {
|
|
66
|
+
const bits = [];
|
|
67
|
+
if (d.stack.includes("openapi-spec"))
|
|
68
|
+
bits.push("OpenAPI spec");
|
|
69
|
+
if (d.stack.includes("graphql-schema"))
|
|
70
|
+
bits.push("GraphQL schema");
|
|
71
|
+
if (d.stack.includes("multi-sdk"))
|
|
72
|
+
bits.push("multi-language SDKs");
|
|
73
|
+
if (d.stack.includes("stainless"))
|
|
74
|
+
bits.push("Stainless");
|
|
75
|
+
return `developer-tools platform detected (${bits.join(", ")}) — API stability + SDK quality gates`;
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
// ── commerce ─────────────────────────────────────
|
|
79
|
+
{
|
|
80
|
+
archetype: "commerce",
|
|
81
|
+
score: (d) => {
|
|
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;
|
|
86
|
+
if (d.stack.includes("stripe"))
|
|
87
|
+
s += 7;
|
|
88
|
+
if (d.stack.includes("shopify"))
|
|
89
|
+
s += 7;
|
|
90
|
+
if (d.stack.includes("braintree"))
|
|
91
|
+
s += 6;
|
|
92
|
+
if (d.stack.includes("adyen"))
|
|
93
|
+
s += 6;
|
|
94
|
+
if (d.stack.includes("paddle"))
|
|
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;
|
|
102
|
+
return s;
|
|
103
|
+
},
|
|
104
|
+
reason: (d) => {
|
|
105
|
+
const payments = [];
|
|
106
|
+
if (d.stack.includes("stripe"))
|
|
107
|
+
payments.push("Stripe");
|
|
108
|
+
if (d.stack.includes("shopify"))
|
|
109
|
+
payments.push("Shopify");
|
|
110
|
+
if (d.stack.includes("braintree"))
|
|
111
|
+
payments.push("Braintree");
|
|
112
|
+
if (d.stack.includes("adyen"))
|
|
113
|
+
payments.push("Adyen");
|
|
114
|
+
if (d.stack.includes("paddle"))
|
|
115
|
+
payments.push("Paddle");
|
|
116
|
+
return `payments SDK detected: ${payments.join(", ")} — PCI-DSS gate mandatory`;
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
// ── web3 ─────────────────────────────────────────
|
|
120
|
+
{
|
|
121
|
+
archetype: "web3",
|
|
122
|
+
score: (d) => {
|
|
123
|
+
let s = 0;
|
|
124
|
+
if (d.stack.includes("solidity"))
|
|
125
|
+
s += 6;
|
|
126
|
+
if (d.stack.includes("web3"))
|
|
127
|
+
s += 4;
|
|
128
|
+
return s;
|
|
129
|
+
},
|
|
130
|
+
reason: (_d) => "Solidity / smart-contract tooling detected — formal verification gate",
|
|
131
|
+
},
|
|
132
|
+
// ── iot-embedded ─────────────────────────────────
|
|
133
|
+
{
|
|
134
|
+
archetype: "iot-embedded",
|
|
135
|
+
score: (d) => (d.stack.includes("embedded") ? 6 : 0),
|
|
136
|
+
reason: (_d) => "platformio.ini / sdkconfig detected — embedded firmware archetype",
|
|
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 llmFrameworks = ["langchain", "llamaindex"]; // RAG-capable LLM frameworks
|
|
145
|
+
const vdbs = ["pinecone", "weaviate", "chroma", "qdrant"];
|
|
146
|
+
const agents = ["langgraph", "crewai", "autogen", "mastra", "mcp"];
|
|
147
|
+
const hasLlm = llms.some((s) => d.stack.includes(s));
|
|
148
|
+
const hasLlmFw = llmFrameworks.some((s) => d.stack.includes(s));
|
|
149
|
+
const hasVdb = vdbs.some((s) => d.stack.includes(s));
|
|
150
|
+
const hasAgentFw = agents.some((s) => d.stack.includes(s));
|
|
151
|
+
let s = 0;
|
|
152
|
+
// RAG-style agent: any LLM (raw SDK or framework) + vector DB
|
|
153
|
+
if ((hasLlm || hasLlmFw) && hasVdb)
|
|
154
|
+
s += 7;
|
|
155
|
+
if (hasAgentFw)
|
|
156
|
+
s += 6; // explicit agent framework
|
|
157
|
+
if ((hasLlm || hasLlmFw) && hasAgentFw)
|
|
158
|
+
s += 2; // bonus
|
|
159
|
+
// Project-shape signals (agent-runtime archetypes that don't depend
|
|
160
|
+
// on a specific framework — e.g. a deterministic local agent runtime
|
|
161
|
+
// built from scratch). Each signal is weak; multiple together strong.
|
|
162
|
+
const kws = d.readmeKeywords.map((k) => k.toLowerCase());
|
|
163
|
+
const agentRuntimeKws = [
|
|
164
|
+
"agent", "agent-runtime", "agent_runtime", "agentic",
|
|
165
|
+
"tool-calling", "tool-use", "tool_use", "tool-registry",
|
|
166
|
+
"planner", "agent-loop", "agent-runtime", "deterministic-agent",
|
|
167
|
+
"plan-step", "planstep", "tool-allowlist", "agent-budget",
|
|
168
|
+
"mcp-server", "mcp-client", "mcp",
|
|
169
|
+
];
|
|
170
|
+
const matchedAgentKws = agentRuntimeKws.filter((k) => kws.includes(k));
|
|
171
|
+
if (matchedAgentKws.length >= 3)
|
|
172
|
+
s += 5; // strong shape signal
|
|
173
|
+
else if (matchedAgentKws.length === 2)
|
|
174
|
+
s += 3;
|
|
175
|
+
else if (matchedAgentKws.length === 1)
|
|
176
|
+
s += 1;
|
|
177
|
+
// Stack-name patterns: agent-runtime / agent-product (the user
|
|
178
|
+
// explicitly named their library/package after agent or runtime).
|
|
179
|
+
// Detected via signals — d.stack includes the package name as a token.
|
|
180
|
+
const stackJoined = d.stack.join(" ").toLowerCase();
|
|
181
|
+
if (/(?:^| )agent[-_](?:runtime|product|loop|kit|sdk)/.test(stackJoined))
|
|
182
|
+
s += 2;
|
|
183
|
+
// Voice-AI agent: telephony provider + STT + TTS + LLM = autonomous voice agent
|
|
184
|
+
const voiceProviders = ["twilio", "vonage", "livekit"];
|
|
185
|
+
const sttProviders = ["deepgram"];
|
|
186
|
+
const ttsProviders = ["elevenlabs", "hume"];
|
|
187
|
+
const hasVoice = voiceProviders.some((s) => d.stack.includes(s));
|
|
188
|
+
const hasStt = sttProviders.some((s) => d.stack.includes(s));
|
|
189
|
+
const hasTts = ttsProviders.some((s) => d.stack.includes(s));
|
|
190
|
+
if (hasVoice && hasLlm && (hasStt || hasTts))
|
|
191
|
+
s += 7; // strong: full voice-agent stack
|
|
192
|
+
else if (hasVoice && hasLlm)
|
|
193
|
+
s += 4; // medium: voice + LLM
|
|
194
|
+
return s;
|
|
195
|
+
},
|
|
196
|
+
reason: (d) => {
|
|
197
|
+
const bits = [];
|
|
198
|
+
if (d.stack.includes("langgraph"))
|
|
199
|
+
bits.push("LangGraph");
|
|
200
|
+
if (d.stack.includes("crewai"))
|
|
201
|
+
bits.push("CrewAI");
|
|
202
|
+
if (d.stack.includes("autogen"))
|
|
203
|
+
bits.push("AutoGen");
|
|
204
|
+
if (d.stack.includes("mastra"))
|
|
205
|
+
bits.push("Mastra");
|
|
206
|
+
if (d.stack.includes("mcp"))
|
|
207
|
+
bits.push("MCP SDK");
|
|
208
|
+
const vdb = ["pinecone", "weaviate", "chroma", "qdrant"].filter((s) => d.stack.includes(s));
|
|
209
|
+
if (vdb.length)
|
|
210
|
+
bits.push(`vector DB (${vdb.join(",")})`);
|
|
211
|
+
const voice = ["twilio", "vonage", "livekit", "deepgram", "elevenlabs", "hume"].filter((s) => d.stack.includes(s));
|
|
212
|
+
if (voice.length)
|
|
213
|
+
bits.push(`voice stack (${voice.join(",")})`);
|
|
214
|
+
return `agent-product detected — ${bits.join(", ") || "agent signals"} — agent-eval + isolation + prompt-injection gates required`;
|
|
215
|
+
},
|
|
216
|
+
},
|
|
217
|
+
// ── ai-system ────────────────────────────────────
|
|
218
|
+
// Lower score than agent-product: an LLM-using app without vector DB / agent FW
|
|
219
|
+
{
|
|
220
|
+
archetype: "ai-system",
|
|
221
|
+
score: (d) => {
|
|
222
|
+
let s = 0;
|
|
223
|
+
if (d.stack.includes("anthropic-sdk"))
|
|
224
|
+
s += 6;
|
|
225
|
+
if (d.stack.includes("openai-sdk"))
|
|
226
|
+
s += 5;
|
|
227
|
+
if (d.stack.includes("google-ai"))
|
|
228
|
+
s += 5;
|
|
229
|
+
if (d.stack.includes("aws-bedrock"))
|
|
230
|
+
s += 5;
|
|
231
|
+
if (d.stack.includes("cohere"))
|
|
232
|
+
s += 4;
|
|
233
|
+
if (d.stack.includes("replicate"))
|
|
234
|
+
s += 4;
|
|
235
|
+
if (d.stack.includes("langchain"))
|
|
236
|
+
s += 5;
|
|
237
|
+
if (d.stack.includes("llamaindex"))
|
|
238
|
+
s += 5;
|
|
239
|
+
if (d.stack.includes("vercel-ai-sdk"))
|
|
240
|
+
s += 5;
|
|
241
|
+
if (d.stack.includes("ml"))
|
|
242
|
+
s += 2;
|
|
243
|
+
// Combo bonus: using multiple AI providers/frameworks together
|
|
244
|
+
const aiCount = ["anthropic-sdk", "openai-sdk", "google-ai", "aws-bedrock", "cohere", "replicate", "langchain", "llamaindex", "vercel-ai-sdk"]
|
|
245
|
+
.filter(x => d.stack.includes(x)).length;
|
|
246
|
+
if (aiCount >= 2)
|
|
247
|
+
s += 3;
|
|
248
|
+
// Don't double-score if already an agent-product
|
|
249
|
+
const agents = ["langgraph", "crewai", "autogen", "mastra", "mcp"];
|
|
250
|
+
if (agents.some((a) => d.stack.includes(a)))
|
|
251
|
+
s = Math.max(0, s - 2);
|
|
252
|
+
const vdbs = ["pinecone", "weaviate", "chroma", "qdrant"];
|
|
253
|
+
const hasVdb = vdbs.some((v) => d.stack.includes(v));
|
|
254
|
+
if (hasVdb)
|
|
255
|
+
s = Math.max(0, s - 2);
|
|
256
|
+
// Strong RAG signal — LangChain/LlamaIndex + VDB is almost certainly
|
|
257
|
+
// an agent-product (RAG-style), not a generic ai-system. The +5 score
|
|
258
|
+
// these frameworks contribute (above) is appropriate when there's no
|
|
259
|
+
// VDB; with a VDB it overcounts. Apply an additional deduction so
|
|
260
|
+
// agent-product wins the tie. See test "LangChain + Pinecone → agent-product".
|
|
261
|
+
const hasLlmFw = ["langchain", "llamaindex"].some((s) => d.stack.includes(s));
|
|
262
|
+
if (hasVdb && hasLlmFw)
|
|
263
|
+
s = Math.max(0, s - 5);
|
|
264
|
+
return s;
|
|
265
|
+
},
|
|
266
|
+
reason: (d) => {
|
|
267
|
+
const bits = [];
|
|
268
|
+
if (d.stack.includes("anthropic-sdk"))
|
|
269
|
+
bits.push("Anthropic SDK");
|
|
270
|
+
if (d.stack.includes("openai-sdk"))
|
|
271
|
+
bits.push("OpenAI SDK");
|
|
272
|
+
if (d.stack.includes("google-ai"))
|
|
273
|
+
bits.push("Gemini");
|
|
274
|
+
if (d.stack.includes("aws-bedrock"))
|
|
275
|
+
bits.push("AWS Bedrock");
|
|
276
|
+
if (d.stack.includes("langchain"))
|
|
277
|
+
bits.push("LangChain");
|
|
278
|
+
if (d.stack.includes("llamaindex"))
|
|
279
|
+
bits.push("LlamaIndex");
|
|
280
|
+
if (d.stack.includes("vercel-ai-sdk"))
|
|
281
|
+
bits.push("Vercel AI SDK");
|
|
282
|
+
if (d.stack.includes("ml"))
|
|
283
|
+
bits.push("ML stack");
|
|
284
|
+
return `AI/LLM tooling detected (${bits.join(", ")}) — prompt injection + output sanitization gates`;
|
|
285
|
+
},
|
|
286
|
+
},
|
|
287
|
+
// ── fintech (Plaid, banking integrations) ────────
|
|
288
|
+
{
|
|
289
|
+
archetype: "fintech",
|
|
290
|
+
score: (d) => {
|
|
291
|
+
let s = 0;
|
|
292
|
+
if (d.stack.includes("plaid"))
|
|
293
|
+
s += 10;
|
|
294
|
+
if (d.stack.includes("wise"))
|
|
295
|
+
s += 9;
|
|
296
|
+
if (d.stack.includes("dwolla"))
|
|
297
|
+
s += 9;
|
|
298
|
+
if (d.stack.includes("teller"))
|
|
299
|
+
s += 9;
|
|
300
|
+
if (d.stack.includes("fintech"))
|
|
301
|
+
s += 7;
|
|
302
|
+
// Emerging-markets payment providers
|
|
303
|
+
if (d.stack.includes("razorpay"))
|
|
304
|
+
s += 9;
|
|
305
|
+
if (d.stack.includes("paystack"))
|
|
306
|
+
s += 9;
|
|
307
|
+
if (d.stack.includes("flutterwave"))
|
|
308
|
+
s += 9;
|
|
309
|
+
if (d.stack.includes("mercadopago"))
|
|
310
|
+
s += 9;
|
|
311
|
+
if (d.readmeKeywords.includes("fintech"))
|
|
312
|
+
s += 2;
|
|
313
|
+
return s;
|
|
314
|
+
},
|
|
315
|
+
reason: (d) => {
|
|
316
|
+
const bits = [];
|
|
317
|
+
if (d.stack.includes("plaid"))
|
|
318
|
+
bits.push("Plaid");
|
|
319
|
+
if (d.stack.includes("wise"))
|
|
320
|
+
bits.push("Wise");
|
|
321
|
+
if (d.stack.includes("dwolla"))
|
|
322
|
+
bits.push("Dwolla");
|
|
323
|
+
if (d.stack.includes("teller"))
|
|
324
|
+
bits.push("Teller");
|
|
325
|
+
if (d.stack.includes("razorpay"))
|
|
326
|
+
bits.push("Razorpay (India)");
|
|
327
|
+
if (d.stack.includes("paystack"))
|
|
328
|
+
bits.push("Paystack (Nigeria)");
|
|
329
|
+
if (d.stack.includes("flutterwave"))
|
|
330
|
+
bits.push("Flutterwave (Africa)");
|
|
331
|
+
if (d.stack.includes("mercadopago"))
|
|
332
|
+
bits.push("MercadoPago (LATAM)");
|
|
333
|
+
return `fintech integration: ${bits.join(", ")} — SOX, PCI, KYC/AML compliance gates`;
|
|
334
|
+
},
|
|
335
|
+
},
|
|
336
|
+
// ── marketplace (two-sided platform — Stripe Connect / KYC) ────
|
|
337
|
+
// Stronger than commerce when payouts to sellers + KYC providers detected
|
|
338
|
+
{
|
|
339
|
+
archetype: "marketplace",
|
|
340
|
+
score: (d) => {
|
|
341
|
+
let s = 0;
|
|
342
|
+
if (d.stack.includes("stripe-connect"))
|
|
343
|
+
s += 9;
|
|
344
|
+
if (d.stack.includes("adyen-marketpay"))
|
|
345
|
+
s += 9;
|
|
346
|
+
if (d.stack.includes("persona"))
|
|
347
|
+
s += 5;
|
|
348
|
+
if (d.stack.includes("onfido"))
|
|
349
|
+
s += 5;
|
|
350
|
+
if (d.stack.includes("sumsub"))
|
|
351
|
+
s += 5;
|
|
352
|
+
if (d.readmeKeywords.includes("marketplace") || d.readmeKeywords.includes("two-sided"))
|
|
353
|
+
s += 4;
|
|
354
|
+
if (d.readmeKeywords.includes("seller") && d.readmeKeywords.includes("buyer"))
|
|
355
|
+
s += 3;
|
|
356
|
+
// Don't score if pure single-merchant commerce
|
|
357
|
+
if (s > 0 && d.stack.includes("stripe") && !d.stack.includes("stripe-connect"))
|
|
358
|
+
s += 1;
|
|
359
|
+
return s;
|
|
360
|
+
},
|
|
361
|
+
reason: (d) => {
|
|
362
|
+
const bits = [];
|
|
363
|
+
if (d.stack.includes("stripe-connect"))
|
|
364
|
+
bits.push("Stripe Connect");
|
|
365
|
+
if (d.stack.includes("adyen-marketpay"))
|
|
366
|
+
bits.push("Adyen MarketPay");
|
|
367
|
+
if (d.stack.includes("persona") || d.stack.includes("onfido") || d.stack.includes("sumsub"))
|
|
368
|
+
bits.push("KYC vendor");
|
|
369
|
+
return `marketplace / two-sided platform (${bits.join(", ")}) — multi-party payouts + seller KYC + 1099-K + DSA`;
|
|
370
|
+
},
|
|
371
|
+
},
|
|
372
|
+
// ── enterprise-saas (B2B multi-tenant, SSO/SAML/SCIM, audit) ──
|
|
373
|
+
{
|
|
374
|
+
archetype: "enterprise-saas",
|
|
375
|
+
score: (d) => {
|
|
376
|
+
let s = 0;
|
|
377
|
+
if (d.stack.includes("workos"))
|
|
378
|
+
s += 8; // SSO/SCIM-as-a-service
|
|
379
|
+
if (d.stack.includes("auth0"))
|
|
380
|
+
s += 6;
|
|
381
|
+
if (d.stack.includes("okta"))
|
|
382
|
+
s += 6;
|
|
383
|
+
if (d.stack.includes("clerk"))
|
|
384
|
+
s += 4;
|
|
385
|
+
if (d.stack.includes("samlify"))
|
|
386
|
+
s += 6;
|
|
387
|
+
if (d.stack.includes("passport-saml"))
|
|
388
|
+
s += 6;
|
|
389
|
+
if (d.stack.includes("scim"))
|
|
390
|
+
s += 5;
|
|
391
|
+
if (d.readmeKeywords.includes("multi-tenant") || d.readmeKeywords.includes("multitenant"))
|
|
392
|
+
s += 4;
|
|
393
|
+
if (d.readmeKeywords.includes("enterprise") || d.readmeKeywords.includes("b2b"))
|
|
394
|
+
s += 3;
|
|
395
|
+
if (d.readmeKeywords.includes("sso") || d.readmeKeywords.includes("saml"))
|
|
396
|
+
s += 3;
|
|
397
|
+
// Stripe billing + multi-tenant signals = SaaS
|
|
398
|
+
if (s > 0 && d.stack.includes("stripe"))
|
|
399
|
+
s += 1;
|
|
400
|
+
return s;
|
|
401
|
+
},
|
|
402
|
+
reason: (d) => {
|
|
403
|
+
const bits = [];
|
|
404
|
+
if (d.stack.includes("workos"))
|
|
405
|
+
bits.push("WorkOS");
|
|
406
|
+
if (d.stack.includes("auth0"))
|
|
407
|
+
bits.push("Auth0");
|
|
408
|
+
if (d.stack.includes("okta"))
|
|
409
|
+
bits.push("Okta");
|
|
410
|
+
if (d.stack.includes("samlify") || d.stack.includes("passport-saml"))
|
|
411
|
+
bits.push("SAML lib");
|
|
412
|
+
if (d.stack.includes("scim"))
|
|
413
|
+
bits.push("SCIM");
|
|
414
|
+
return `enterprise B2B SaaS (${bits.join(", ")}) — multi-tenant isolation + SSO + audit log + SOC2 mandatory`;
|
|
415
|
+
},
|
|
416
|
+
},
|
|
417
|
+
// ── mlops (model training & lifecycle, distinct from inference) ─
|
|
418
|
+
{
|
|
419
|
+
archetype: "mlops",
|
|
420
|
+
score: (d) => {
|
|
421
|
+
let s = 0;
|
|
422
|
+
if (d.stack.includes("mlflow"))
|
|
423
|
+
s += 7;
|
|
424
|
+
if (d.stack.includes("wandb"))
|
|
425
|
+
s += 6;
|
|
426
|
+
if (d.stack.includes("dvc"))
|
|
427
|
+
s += 6;
|
|
428
|
+
if (d.stack.includes("kubeflow"))
|
|
429
|
+
s += 7;
|
|
430
|
+
if (d.stack.includes("bentoml"))
|
|
431
|
+
s += 5;
|
|
432
|
+
if (d.stack.includes("seldon"))
|
|
433
|
+
s += 5;
|
|
434
|
+
if (d.stack.includes("kserve"))
|
|
435
|
+
s += 5;
|
|
436
|
+
if (d.stack.includes("sagemaker"))
|
|
437
|
+
s += 5;
|
|
438
|
+
if (d.stack.includes("vertex-ai"))
|
|
439
|
+
s += 5;
|
|
440
|
+
if (d.stack.includes("ray"))
|
|
441
|
+
s += 4;
|
|
442
|
+
if (d.stack.includes("torch") || d.stack.includes("tensorflow"))
|
|
443
|
+
s += 2;
|
|
444
|
+
if (d.readmeKeywords.includes("training") && d.readmeKeywords.includes("model"))
|
|
445
|
+
s += 3;
|
|
446
|
+
// De-prioritize if pure inference (LLM API only)
|
|
447
|
+
if (s > 0 && d.stack.includes("anthropic-sdk") && !d.stack.includes("torch") && !d.stack.includes("mlflow"))
|
|
448
|
+
s -= 2;
|
|
449
|
+
return s;
|
|
450
|
+
},
|
|
451
|
+
reason: (d) => {
|
|
452
|
+
const bits = [];
|
|
453
|
+
if (d.stack.includes("mlflow"))
|
|
454
|
+
bits.push("MLflow");
|
|
455
|
+
if (d.stack.includes("wandb"))
|
|
456
|
+
bits.push("W&B");
|
|
457
|
+
if (d.stack.includes("dvc"))
|
|
458
|
+
bits.push("DVC");
|
|
459
|
+
if (d.stack.includes("kubeflow"))
|
|
460
|
+
bits.push("Kubeflow");
|
|
461
|
+
if (d.stack.includes("bentoml"))
|
|
462
|
+
bits.push("BentoML");
|
|
463
|
+
if (d.stack.includes("torch"))
|
|
464
|
+
bits.push("PyTorch");
|
|
465
|
+
if (d.stack.includes("tensorflow"))
|
|
466
|
+
bits.push("TensorFlow");
|
|
467
|
+
return `MLOps stack detected (${bits.join(", ")}) — dataset versioning + drift detection + model registry + EU AI Act high-risk gates`;
|
|
468
|
+
},
|
|
469
|
+
},
|
|
470
|
+
// ── streaming (event-driven / Kafka / Flink — distinct from batch data-platform) ─
|
|
471
|
+
{
|
|
472
|
+
archetype: "streaming",
|
|
473
|
+
score: (d) => {
|
|
474
|
+
let s = 0;
|
|
475
|
+
if (d.stack.includes("kafkajs"))
|
|
476
|
+
s += 7;
|
|
477
|
+
if (d.stack.includes("kafka-node"))
|
|
478
|
+
s += 6;
|
|
479
|
+
if (d.stack.includes("rdkafka"))
|
|
480
|
+
s += 7;
|
|
481
|
+
if (d.stack.includes("kinesis"))
|
|
482
|
+
s += 6;
|
|
483
|
+
if (d.stack.includes("pulsar"))
|
|
484
|
+
s += 6;
|
|
485
|
+
if (d.stack.includes("flink"))
|
|
486
|
+
s += 7;
|
|
487
|
+
if (d.stack.includes("beam"))
|
|
488
|
+
s += 6;
|
|
489
|
+
if (d.stack.includes("debezium"))
|
|
490
|
+
s += 7;
|
|
491
|
+
if (d.stack.includes("nats"))
|
|
492
|
+
s += 5;
|
|
493
|
+
if (d.stack.includes("rabbitmq"))
|
|
494
|
+
s += 4;
|
|
495
|
+
if (d.readmeKeywords.includes("streaming") || d.readmeKeywords.includes("event-driven"))
|
|
496
|
+
s += 2;
|
|
497
|
+
if (d.readmeKeywords.includes("cdc") || d.readmeKeywords.includes("real-time"))
|
|
498
|
+
s += 2;
|
|
499
|
+
return s;
|
|
500
|
+
},
|
|
501
|
+
reason: (d) => {
|
|
502
|
+
const bits = [];
|
|
503
|
+
if (d.stack.includes("kafkajs") || d.stack.includes("rdkafka") || d.stack.includes("kafka-node"))
|
|
504
|
+
bits.push("Kafka");
|
|
505
|
+
if (d.stack.includes("kinesis"))
|
|
506
|
+
bits.push("Kinesis");
|
|
507
|
+
if (d.stack.includes("pulsar"))
|
|
508
|
+
bits.push("Pulsar");
|
|
509
|
+
if (d.stack.includes("flink"))
|
|
510
|
+
bits.push("Flink");
|
|
511
|
+
if (d.stack.includes("beam"))
|
|
512
|
+
bits.push("Beam");
|
|
513
|
+
if (d.stack.includes("debezium"))
|
|
514
|
+
bits.push("Debezium CDC");
|
|
515
|
+
return `streaming / event-driven stack (${bits.join(", ")}) — exactly-once + backpressure + DLQ + schema evolution gates`;
|
|
516
|
+
},
|
|
517
|
+
},
|
|
518
|
+
// ── cms / content platform (headless CMS, publishing, SEO) ────
|
|
519
|
+
{
|
|
520
|
+
archetype: "cms",
|
|
521
|
+
score: (d) => {
|
|
522
|
+
let s = 0;
|
|
523
|
+
if (d.stack.includes("sanity"))
|
|
524
|
+
s += 9;
|
|
525
|
+
if (d.stack.includes("contentful"))
|
|
526
|
+
s += 9;
|
|
527
|
+
if (d.stack.includes("strapi"))
|
|
528
|
+
s += 9;
|
|
529
|
+
if (d.stack.includes("payload"))
|
|
530
|
+
s += 8;
|
|
531
|
+
if (d.stack.includes("ghost"))
|
|
532
|
+
s += 8;
|
|
533
|
+
if (d.stack.includes("gatsby"))
|
|
534
|
+
s += 5;
|
|
535
|
+
if (d.stack.includes("eleventy"))
|
|
536
|
+
s += 5;
|
|
537
|
+
if (d.stack.includes("hugo"))
|
|
538
|
+
s += 5;
|
|
539
|
+
if (d.stack.includes("astro") && d.readmeKeywords.includes("blog"))
|
|
540
|
+
s += 4;
|
|
541
|
+
if (d.stack.includes("next.js") && d.readmeKeywords.includes("blog"))
|
|
542
|
+
s += 3;
|
|
543
|
+
if (d.readmeKeywords.includes("cms") || d.readmeKeywords.includes("publishing") || d.readmeKeywords.includes("blog"))
|
|
544
|
+
s += 2;
|
|
545
|
+
// De-prioritize if it's clearly an enterprise SaaS app
|
|
546
|
+
if (s > 0 && (d.stack.includes("workos") || d.stack.includes("samlify")))
|
|
547
|
+
s -= 3;
|
|
548
|
+
return s;
|
|
549
|
+
},
|
|
550
|
+
reason: (d) => {
|
|
551
|
+
const bits = [];
|
|
552
|
+
if (d.stack.includes("sanity"))
|
|
553
|
+
bits.push("Sanity");
|
|
554
|
+
if (d.stack.includes("contentful"))
|
|
555
|
+
bits.push("Contentful");
|
|
556
|
+
if (d.stack.includes("strapi"))
|
|
557
|
+
bits.push("Strapi");
|
|
558
|
+
if (d.stack.includes("payload"))
|
|
559
|
+
bits.push("Payload");
|
|
560
|
+
if (d.stack.includes("ghost"))
|
|
561
|
+
bits.push("Ghost");
|
|
562
|
+
if (d.stack.includes("gatsby"))
|
|
563
|
+
bits.push("Gatsby");
|
|
564
|
+
if (d.stack.includes("eleventy"))
|
|
565
|
+
bits.push("Eleventy");
|
|
566
|
+
if (d.stack.includes("hugo"))
|
|
567
|
+
bits.push("Hugo");
|
|
568
|
+
return `CMS / publishing stack (${bits.join(", ")}) — schema.org + Core Web Vitals + DMCA + UGC moderation gates`;
|
|
569
|
+
},
|
|
570
|
+
},
|
|
571
|
+
// ── healthcare (FHIR/HL7/PHI) ───────────────────
|
|
572
|
+
{
|
|
573
|
+
archetype: "healthcare",
|
|
574
|
+
score: (d) => {
|
|
575
|
+
let s = 0;
|
|
576
|
+
if (d.stack.includes("fhir"))
|
|
577
|
+
s += 7;
|
|
578
|
+
if (d.stack.includes("hl7"))
|
|
579
|
+
s += 6;
|
|
580
|
+
if (d.readmeKeywords.includes("healthcare"))
|
|
581
|
+
s += 3;
|
|
582
|
+
return s;
|
|
583
|
+
},
|
|
584
|
+
reason: (d) => {
|
|
585
|
+
const bits = [];
|
|
586
|
+
if (d.stack.includes("fhir"))
|
|
587
|
+
bits.push("FHIR");
|
|
588
|
+
if (d.stack.includes("hl7"))
|
|
589
|
+
bits.push("HL7");
|
|
590
|
+
return `healthcare data tooling: ${bits.join(", ")} — HIPAA/PHI handling gates required`;
|
|
591
|
+
},
|
|
592
|
+
},
|
|
593
|
+
// ── regulated (compliance-first: DORA/NIS2/SOX/FedRAMP — not fintech/healthcare) ──
|
|
594
|
+
{
|
|
595
|
+
archetype: "regulated",
|
|
596
|
+
score: (d) => {
|
|
597
|
+
let s = 0;
|
|
598
|
+
// Compliance automation SaaS installed → near-certain regulated industry
|
|
599
|
+
if (d.stack.includes("compliance-automation"))
|
|
600
|
+
s += 10;
|
|
601
|
+
// Compliance documentation structure
|
|
602
|
+
if (d.stack.includes("compliance-docs"))
|
|
603
|
+
s += 7;
|
|
604
|
+
// Audit log package
|
|
605
|
+
if (d.stack.includes("audit-log"))
|
|
606
|
+
s += 5;
|
|
607
|
+
// README regulatory signals
|
|
608
|
+
const kws = d.readmeKeywords;
|
|
609
|
+
if (kws.includes("regulated"))
|
|
610
|
+
s += 5;
|
|
611
|
+
// FedRAMP / FISMA / CMMC → US federal regulated
|
|
612
|
+
const fedSignals = ["fedramp", "fisma", "cmmc"];
|
|
613
|
+
if (fedSignals.some((k) => kws.includes(k)))
|
|
614
|
+
s += 8;
|
|
615
|
+
// DORA ICT / NIS2 → EU regulated
|
|
616
|
+
if (kws.includes("dora ict") || kws.includes("nis2"))
|
|
617
|
+
s += 6;
|
|
618
|
+
// SOX compliance (non-fintech context)
|
|
619
|
+
if (kws.includes("sox compliance") || kws.includes("sarbanes")) {
|
|
620
|
+
if (!d.stack.includes("plaid") && !d.stack.includes("fintech"))
|
|
621
|
+
s += 5;
|
|
622
|
+
}
|
|
623
|
+
// Generic compliance/audit signals without stronger domain archetype
|
|
624
|
+
if (kws.includes("compliance automation") || kws.includes("audit trail"))
|
|
625
|
+
s += 4;
|
|
626
|
+
if (kws.includes("iso 27001") || kws.includes("soc 2 type"))
|
|
627
|
+
s += 3;
|
|
628
|
+
// Hard exclude: fintech and healthcare have their own dedicated archetypes
|
|
629
|
+
if (d.stack.includes("plaid") || d.stack.includes("wise") || d.stack.includes("fhir"))
|
|
630
|
+
s = 0;
|
|
631
|
+
return s;
|
|
632
|
+
},
|
|
633
|
+
reason: (d) => {
|
|
634
|
+
const bits = [];
|
|
635
|
+
if (d.stack.includes("compliance-automation"))
|
|
636
|
+
bits.push("compliance automation (Vanta/Drata/Secureframe)");
|
|
637
|
+
if (d.stack.includes("compliance-docs"))
|
|
638
|
+
bits.push("ISMS/risk-register/compliance docs");
|
|
639
|
+
if (d.stack.includes("audit-log"))
|
|
640
|
+
bits.push("audit log package");
|
|
641
|
+
const kws = d.readmeKeywords;
|
|
642
|
+
if (kws.includes("regulated"))
|
|
643
|
+
bits.push("regulated-industry README");
|
|
644
|
+
if (kws.includes("fedramp"))
|
|
645
|
+
bits.push("FedRAMP");
|
|
646
|
+
if (kws.includes("fisma"))
|
|
647
|
+
bits.push("FISMA");
|
|
648
|
+
if (kws.includes("cmmc"))
|
|
649
|
+
bits.push("CMMC");
|
|
650
|
+
if (kws.includes("dora ict"))
|
|
651
|
+
bits.push("DORA ICT");
|
|
652
|
+
if (kws.includes("nis2"))
|
|
653
|
+
bits.push("NIS2");
|
|
654
|
+
if (kws.includes("sox compliance") || kws.includes("sarbanes"))
|
|
655
|
+
bits.push("SOX");
|
|
656
|
+
return `regulated-industry signals (${bits.join(", ")}) — DORA/NIS2/SOX/FedRAMP compliance gates required`;
|
|
657
|
+
},
|
|
658
|
+
},
|
|
659
|
+
// ── cli-tool (explicit CLI: bin field + cli entry) ─
|
|
660
|
+
{
|
|
661
|
+
archetype: "cli-tool",
|
|
662
|
+
score: (d) => {
|
|
663
|
+
let s = 0;
|
|
664
|
+
// Explicit cli marker from detect.ts (bin field present)
|
|
665
|
+
if (d.stack.includes("cli"))
|
|
666
|
+
s += 6;
|
|
667
|
+
// Python CLI via [project.scripts] / console_scripts entry
|
|
668
|
+
if (d.stack.includes("python-cli"))
|
|
669
|
+
s += 7;
|
|
670
|
+
if (d.codeStructure.hasCliEntry)
|
|
671
|
+
s += 2;
|
|
672
|
+
if (d.readmeKeywords.includes("cli"))
|
|
673
|
+
s += 1;
|
|
674
|
+
// Penalize if web framework present (CLI + web is rare)
|
|
675
|
+
const webFw = ["next.js", "express", "fastify", "nestjs", "django", "fastapi", "flask", "hono", "koa"];
|
|
676
|
+
if (webFw.some((w) => d.stack.includes(w)))
|
|
677
|
+
s = Math.max(0, s - 4);
|
|
678
|
+
return s;
|
|
679
|
+
},
|
|
680
|
+
reason: (_d) => "package.json bin field + CLI entry detected — argument-parsing, exit-code, --help gates",
|
|
681
|
+
},
|
|
682
|
+
// ── mobile-app ───────────────────────────────────
|
|
683
|
+
{
|
|
684
|
+
archetype: "mobile-app",
|
|
685
|
+
score: (d) => {
|
|
686
|
+
let s = 0;
|
|
687
|
+
if (d.stack.includes("react-native"))
|
|
688
|
+
s += 5;
|
|
689
|
+
if (d.stack.includes("expo"))
|
|
690
|
+
s += 5;
|
|
691
|
+
if (d.stack.includes("ios"))
|
|
692
|
+
s += 5;
|
|
693
|
+
if (d.stack.includes("swift"))
|
|
694
|
+
s += 3;
|
|
695
|
+
return s;
|
|
696
|
+
},
|
|
697
|
+
reason: (d) => {
|
|
698
|
+
const bits = [];
|
|
699
|
+
if (d.stack.includes("react-native"))
|
|
700
|
+
bits.push("React Native");
|
|
701
|
+
if (d.stack.includes("expo"))
|
|
702
|
+
bits.push("Expo");
|
|
703
|
+
if (d.stack.includes("ios"))
|
|
704
|
+
bits.push("iOS project");
|
|
705
|
+
return `mobile framework detected: ${bits.join(", ")}`;
|
|
706
|
+
},
|
|
707
|
+
},
|
|
708
|
+
// ── data-platform ────────────────────────────────
|
|
709
|
+
{
|
|
710
|
+
archetype: "data-platform",
|
|
711
|
+
score: (d) => {
|
|
712
|
+
let s = 0;
|
|
713
|
+
if (d.stack.includes("data-pipeline"))
|
|
714
|
+
s += 6;
|
|
715
|
+
if (d.stack.includes("dbt"))
|
|
716
|
+
s += 3;
|
|
717
|
+
if (d.stack.includes("dagster"))
|
|
718
|
+
s += 3;
|
|
719
|
+
if (d.stack.includes("polars"))
|
|
720
|
+
s += 2;
|
|
721
|
+
if (d.stack.includes("iceberg"))
|
|
722
|
+
s += 3;
|
|
723
|
+
if (d.stack.includes("duckdb"))
|
|
724
|
+
s += 2;
|
|
725
|
+
if (d.readmeKeywords.includes("data"))
|
|
726
|
+
s += 1;
|
|
727
|
+
return s;
|
|
728
|
+
},
|
|
729
|
+
reason: (d) => {
|
|
730
|
+
const bits = [];
|
|
731
|
+
if (d.stack.includes("data-pipeline"))
|
|
732
|
+
bits.push("pandas/airflow/prefect");
|
|
733
|
+
if (d.stack.includes("dbt"))
|
|
734
|
+
bits.push("dbt");
|
|
735
|
+
if (d.stack.includes("dagster"))
|
|
736
|
+
bits.push("dagster");
|
|
737
|
+
return `data pipeline tooling: ${bits.join(", ") || "detected"}`;
|
|
738
|
+
},
|
|
739
|
+
},
|
|
740
|
+
// ── infra ────────────────────────────────────────
|
|
741
|
+
{
|
|
742
|
+
archetype: "infra",
|
|
743
|
+
score: (d) => {
|
|
744
|
+
const hasTerraform = d.stack.includes("terraform");
|
|
745
|
+
const hasHelm = d.stack.includes("helm");
|
|
746
|
+
const hasK8s = d.stack.includes("kubernetes");
|
|
747
|
+
// Require at least one explicit infra signal
|
|
748
|
+
if (!hasTerraform && !hasHelm && !hasK8s)
|
|
749
|
+
return 0;
|
|
750
|
+
let s = 0;
|
|
751
|
+
if (hasTerraform)
|
|
752
|
+
s += 4;
|
|
753
|
+
if (hasHelm)
|
|
754
|
+
s += 4;
|
|
755
|
+
if (hasK8s)
|
|
756
|
+
s += 4;
|
|
757
|
+
// Pure-infra repo (no app code) gets a small bonus
|
|
758
|
+
if (!d.stack.includes("nodejs") && !d.stack.includes("python") && !d.stack.includes("go"))
|
|
759
|
+
s += 2;
|
|
760
|
+
return s;
|
|
761
|
+
},
|
|
762
|
+
reason: (d) => {
|
|
763
|
+
const bits = [];
|
|
764
|
+
if (d.stack.includes("terraform"))
|
|
765
|
+
bits.push("Terraform");
|
|
766
|
+
if (d.stack.includes("helm"))
|
|
767
|
+
bits.push("Helm");
|
|
768
|
+
if (d.stack.includes("kubernetes"))
|
|
769
|
+
bits.push("Kustomize/K8s");
|
|
770
|
+
return `infrastructure-as-code detected: ${bits.join(", ")}`;
|
|
771
|
+
},
|
|
772
|
+
},
|
|
773
|
+
// ── web-service (default for web frameworks) ─────
|
|
774
|
+
{
|
|
775
|
+
archetype: "web-service",
|
|
776
|
+
score: (d) => {
|
|
777
|
+
let s = 0;
|
|
778
|
+
const serverFrameworks = ["express", "fastify", "nestjs", "hono", "koa",
|
|
779
|
+
"django", "fastapi", "flask", "gin", "echo", "chi"];
|
|
780
|
+
const fullstackFw = ["next.js", "nuxt", "remix", "sveltekit", "astro"];
|
|
781
|
+
const uiOnly = ["react", "vue", "angular", "svelte"];
|
|
782
|
+
for (const fw of serverFrameworks)
|
|
783
|
+
if (d.stack.includes(fw))
|
|
784
|
+
s += 3;
|
|
785
|
+
for (const fw of fullstackFw)
|
|
786
|
+
if (d.stack.includes(fw))
|
|
787
|
+
s += 4;
|
|
788
|
+
// UI-only: weaker signal (could be a library)
|
|
789
|
+
for (const fw of uiOnly)
|
|
790
|
+
if (d.stack.includes(fw))
|
|
791
|
+
s += 1;
|
|
792
|
+
if (s > 0)
|
|
793
|
+
s += 1; // baseline web bonus
|
|
794
|
+
// Code-structure boost
|
|
795
|
+
if (d.codeStructure.hasRoutesDir)
|
|
796
|
+
s += 2;
|
|
797
|
+
if (d.codeStructure.hasServerEntry)
|
|
798
|
+
s += 2;
|
|
799
|
+
if (d.scripts.hasStart || d.scripts.hasDev)
|
|
800
|
+
s += 1;
|
|
801
|
+
// Penalize if explicitly a library (publishConfig/cli)
|
|
802
|
+
if (d.stack.includes("library") && !d.codeStructure.hasRoutesDir && !d.codeStructure.hasServerEntry) {
|
|
803
|
+
s = Math.max(0, s - 3);
|
|
804
|
+
}
|
|
805
|
+
return s;
|
|
806
|
+
},
|
|
807
|
+
reason: (d) => {
|
|
808
|
+
const fw = d.stack.find((t) => ["next.js", "express", "fastify", "nestjs", "hono", "koa",
|
|
809
|
+
"django", "fastapi", "flask", "gin", "echo"].includes(t));
|
|
810
|
+
const extras = [];
|
|
811
|
+
if (d.codeStructure.hasRoutesDir)
|
|
812
|
+
extras.push("routes/");
|
|
813
|
+
if (d.codeStructure.hasServerEntry)
|
|
814
|
+
extras.push("server entry");
|
|
815
|
+
return `web framework detected: ${fw ?? "unknown"}${extras.length ? " + " + extras.join(", ") : ""}`;
|
|
816
|
+
},
|
|
817
|
+
},
|
|
818
|
+
// ── library (no app framework, just code) ────────
|
|
819
|
+
// Strong signal: detect.ts marked "library" + no web/server structure
|
|
820
|
+
{
|
|
821
|
+
archetype: "library",
|
|
822
|
+
score: (d) => {
|
|
823
|
+
const isExplicitLib = d.stack.includes("library");
|
|
824
|
+
const isExplicitCli = d.stack.includes("cli");
|
|
825
|
+
// Don't claim library if web-service shape is obvious
|
|
826
|
+
const looksLikeWebService = d.codeStructure.hasRoutesDir || d.codeStructure.hasServerEntry;
|
|
827
|
+
if (looksLikeWebService)
|
|
828
|
+
return 0;
|
|
829
|
+
// Don't claim library if data-pipeline / ml signals dominate
|
|
830
|
+
if (d.stack.includes("data-pipeline") || d.stack.includes("ml"))
|
|
831
|
+
return 0;
|
|
832
|
+
// Don't claim library if a domain-specific archetype is clearly present
|
|
833
|
+
const domainSignals = ["plaid", "wise", "dwolla", "fhir", "hl7", "stripe", "shopify", "solidity",
|
|
834
|
+
"embedded", "unity", "unreal", "godot", "react-native", "expo"];
|
|
835
|
+
if (domainSignals.some((s) => d.stack.includes(s)))
|
|
836
|
+
return 0;
|
|
837
|
+
if (isExplicitLib && !isExplicitCli)
|
|
838
|
+
return 7;
|
|
839
|
+
if (isExplicitLib && isExplicitCli)
|
|
840
|
+
return 4; // cli-tool rule will outscore
|
|
841
|
+
// Weaker signal: plain runtime + no app framework
|
|
842
|
+
const hasApp = d.stack.some((t) => ["next.js", "django", "fastapi", "flask", "express", "fastify", "nestjs", "hono",
|
|
843
|
+
"react-native", "expo", "tauri", "capacitor", "flutter",
|
|
844
|
+
"terraform", "pulumi", "aws-cdk", "helm",
|
|
845
|
+
"embedded", "zephyr", "esp-idf",
|
|
846
|
+
"browser-extension", "unity", "unreal", "godot", "phaser", "cocos"].includes(t));
|
|
847
|
+
if (hasApp)
|
|
848
|
+
return 0;
|
|
849
|
+
if (d.stack.includes("nodejs") || d.stack.includes("python") || d.stack.includes("go") || d.stack.includes("rust")) {
|
|
850
|
+
return 2;
|
|
851
|
+
}
|
|
852
|
+
return 0;
|
|
853
|
+
},
|
|
854
|
+
reason: (d) => {
|
|
855
|
+
if (d.stack.includes("library")) {
|
|
856
|
+
return "package.json/pyproject/Cargo.toml indicates a publishable library";
|
|
857
|
+
}
|
|
858
|
+
return "no web/mobile/infra framework detected — looks like a library/SDK";
|
|
859
|
+
},
|
|
860
|
+
},
|
|
861
|
+
// ── edtech (education technology — COPPA / FERPA / WCAG-AA child safety) ──
|
|
862
|
+
// Distinct from cms (general content) and healthcare (PHI). Drives age-gate,
|
|
863
|
+
// parental-consent, and accessibility patterns.
|
|
864
|
+
{
|
|
865
|
+
archetype: "edtech",
|
|
866
|
+
score: (d) => {
|
|
867
|
+
let s = 0;
|
|
868
|
+
const lmsLibs = ["canvas-lms", "moodle-api", "schoology-sdk", "blackboard-rest",
|
|
869
|
+
"google-classroom", "khan-academy-cli", "learnosity",
|
|
870
|
+
"kahoot-api", "h5p", "scorm", "lti", "lti-1.3"];
|
|
871
|
+
lmsLibs.forEach((l) => { if (d.stack.includes(l))
|
|
872
|
+
s += 6; });
|
|
873
|
+
// Auth/identity providers commonly used in edtech
|
|
874
|
+
if (d.stack.includes("clever-sdk"))
|
|
875
|
+
s += 6;
|
|
876
|
+
if (d.stack.includes("classlink-sso"))
|
|
877
|
+
s += 4;
|
|
878
|
+
const kws = d.readmeKeywords;
|
|
879
|
+
const eduKeywords = ["student", "classroom", "teacher", "k-12", "k12",
|
|
880
|
+
"lms", "learning management", "grade book", "gradebook",
|
|
881
|
+
"enrollment", "transcript", "pupil", "tutoring", "edtech"];
|
|
882
|
+
const matchedKws = eduKeywords.filter((k) => kws.includes(k));
|
|
883
|
+
if (matchedKws.length >= 2)
|
|
884
|
+
s += 5;
|
|
885
|
+
else if (matchedKws.length === 1)
|
|
886
|
+
s += 2;
|
|
887
|
+
// Strong signal: COPPA / FERPA explicitly mentioned
|
|
888
|
+
if (kws.includes("coppa") || kws.includes("ferpa"))
|
|
889
|
+
s += 6;
|
|
890
|
+
if (kws.includes("parental consent") || kws.includes("age gate"))
|
|
891
|
+
s += 4;
|
|
892
|
+
return s;
|
|
893
|
+
},
|
|
894
|
+
reason: (d) => {
|
|
895
|
+
const kws = d.readmeKeywords;
|
|
896
|
+
const bits = [];
|
|
897
|
+
const lmsLibs = ["canvas-lms", "moodle-api", "schoology-sdk", "google-classroom", "lti", "scorm"];
|
|
898
|
+
lmsLibs.forEach((l) => { if (d.stack.includes(l))
|
|
899
|
+
bits.push(l); });
|
|
900
|
+
if (kws.includes("coppa"))
|
|
901
|
+
bits.push("COPPA mention");
|
|
902
|
+
if (kws.includes("ferpa"))
|
|
903
|
+
bits.push("FERPA mention");
|
|
904
|
+
if (kws.includes("k-12") || kws.includes("k12"))
|
|
905
|
+
bits.push("K-12 keyword");
|
|
906
|
+
if (kws.includes("student"))
|
|
907
|
+
bits.push("student-data keyword");
|
|
908
|
+
return `edtech detected (${bits.join(", ") || "education domain signals"}) — COPPA/FERPA/WCAG-AA child-safety gates required`;
|
|
909
|
+
},
|
|
910
|
+
},
|
|
911
|
+
// ── gov-public (government / civic-tech — FedRAMP / NIST 800-53 / Section 508) ──
|
|
912
|
+
// Severe regulatory burden. Distinct from regulated (which is more EU-focused
|
|
913
|
+
// DORA/NIS2). gov-public targets US federal/state + UK gov.uk patterns.
|
|
914
|
+
{
|
|
915
|
+
archetype: "gov-public",
|
|
916
|
+
score: (d) => {
|
|
917
|
+
let s = 0;
|
|
918
|
+
const govLibs = ["login-gov-sdk", "id-me-sdk", "idme-sdk",
|
|
919
|
+
"usds-design-system", "uswds", "uk-gov-design-system",
|
|
920
|
+
"gov-uk-frontend", "verify-gov-uk",
|
|
921
|
+
"usajobs-sdk", "data-gov", "irs-modernized-efile"];
|
|
922
|
+
govLibs.forEach((l) => { if (d.stack.includes(l))
|
|
923
|
+
s += 6; });
|
|
924
|
+
const kws = d.readmeKeywords;
|
|
925
|
+
const govKeywords = ["fedramp", "fisma", "nist 800-53", "nist-800-53",
|
|
926
|
+
"section 508", "section-508", "ato", "civic tech",
|
|
927
|
+
"government", "municipal", "federal", "agency",
|
|
928
|
+
"gov.uk", "usds", "data.gov", "usa.gov", "irs", "ssa",
|
|
929
|
+
"department of", "stateramp", "cjis"];
|
|
930
|
+
const matchedKws = govKeywords.filter((k) => kws.includes(k));
|
|
931
|
+
if (matchedKws.length >= 2)
|
|
932
|
+
s += 6;
|
|
933
|
+
else if (matchedKws.length === 1)
|
|
934
|
+
s += 3;
|
|
935
|
+
// Very strong signals
|
|
936
|
+
if (kws.includes("fedramp") || kws.includes("fisma"))
|
|
937
|
+
s += 4;
|
|
938
|
+
if (kws.includes("section 508") || kws.includes("section-508"))
|
|
939
|
+
s += 3;
|
|
940
|
+
if (kws.includes("ato"))
|
|
941
|
+
s += 3;
|
|
942
|
+
return s;
|
|
943
|
+
},
|
|
944
|
+
reason: (d) => {
|
|
945
|
+
const kws = d.readmeKeywords;
|
|
946
|
+
const bits = [];
|
|
947
|
+
if (d.stack.includes("login-gov-sdk"))
|
|
948
|
+
bits.push("login.gov");
|
|
949
|
+
if (d.stack.includes("usds-design-system") || d.stack.includes("uswds"))
|
|
950
|
+
bits.push("USWDS");
|
|
951
|
+
if (d.stack.includes("uk-gov-design-system") || d.stack.includes("gov-uk-frontend"))
|
|
952
|
+
bits.push("gov.uk Design System");
|
|
953
|
+
if (kws.includes("fedramp"))
|
|
954
|
+
bits.push("FedRAMP mention");
|
|
955
|
+
if (kws.includes("nist-800-53") || kws.includes("nist 800-53"))
|
|
956
|
+
bits.push("NIST 800-53 mention");
|
|
957
|
+
if (kws.includes("section 508") || kws.includes("section-508"))
|
|
958
|
+
bits.push("Section 508 mention");
|
|
959
|
+
return `gov-public detected (${bits.join(", ") || "government domain signals"}) — FedRAMP/NIST 800-53/Section 508 gates required`;
|
|
960
|
+
},
|
|
961
|
+
},
|
|
962
|
+
// ── defense-govcon (US defense contractor — CMMC / NIST 800-171 / DFARS) ──
|
|
963
|
+
// Distinct from gov-public: the buyer is DoD and the obligation is protecting
|
|
964
|
+
// Controlled Unclassified Information (CUI) under DFARS 252.204-7012 + CMMC 2.0,
|
|
965
|
+
// with export controls (ITAR/EAR) and supply-chain bans (Section 889).
|
|
966
|
+
{
|
|
967
|
+
archetype: "defense-govcon",
|
|
968
|
+
score: (d) => {
|
|
969
|
+
let s = 0;
|
|
970
|
+
const kws = d.readmeKeywords;
|
|
971
|
+
// detect.ts "defense" bucket fires on cmmc / nist 800-171 / dfars / cui / itar / section 889.
|
|
972
|
+
// These are unambiguous DoD-contractor signals — more specific than generic
|
|
973
|
+
// gov-public (fedramp/government), so weight high enough to outrank it.
|
|
974
|
+
if (kws.includes("defense"))
|
|
975
|
+
s += 12;
|
|
976
|
+
return s;
|
|
977
|
+
},
|
|
978
|
+
reason: (_d) => "US defense-contractor signals (CMMC / NIST 800-171 / DFARS 252.204-7012 / CUI / ITAR / Section 889) — CMMC assessment + CUI protection gates required",
|
|
979
|
+
},
|
|
980
|
+
// ── insurance (insurtech — NAIC / Solvency II / actuarial / claims fraud) ──
|
|
981
|
+
// Fintech-adjacent but distinct: multi-state filings, anti-discrimination
|
|
982
|
+
// pricing, actuarial model auditability, claims fraud detection.
|
|
983
|
+
{
|
|
984
|
+
archetype: "insurance",
|
|
985
|
+
score: (d) => {
|
|
986
|
+
let s = 0;
|
|
987
|
+
const insuranceLibs = ["acord-standards", "naic-schemas", "drools-rules",
|
|
988
|
+
"solvency2-calc", "openexposure", "ms-actuarial",
|
|
989
|
+
"lloyds-vendor-api", "verisk-sdk", "ccc-one-sdk",
|
|
990
|
+
"guidewire-cloud", "duck-creek", "majesco-sdk",
|
|
991
|
+
"ebix", "aplus-pas"];
|
|
992
|
+
insuranceLibs.forEach((l) => { if (d.stack.includes(l))
|
|
993
|
+
s += 6; });
|
|
994
|
+
const kws = d.readmeKeywords;
|
|
995
|
+
const insuranceKeywords = ["policy", "underwriting", "premium", "claim",
|
|
996
|
+
"actuarial", "reinsurance", "naic", "solvency",
|
|
997
|
+
"broker", "carrier", "mga", "mgu", "tpa",
|
|
998
|
+
"insurance", "insurtech", "insurer", "insured",
|
|
999
|
+
"deductible", "coverage", "bordereau"];
|
|
1000
|
+
const matchedKws = insuranceKeywords.filter((k) => kws.includes(k));
|
|
1001
|
+
if (matchedKws.length >= 3)
|
|
1002
|
+
s += 6;
|
|
1003
|
+
else if (matchedKws.length === 2)
|
|
1004
|
+
s += 3;
|
|
1005
|
+
else if (matchedKws.length === 1)
|
|
1006
|
+
s += 1;
|
|
1007
|
+
// Very strong signals — NAIC/Solvency/IFRS 17 explicit
|
|
1008
|
+
if (kws.includes("naic") || kws.includes("solvency ii") || kws.includes("solvency-ii"))
|
|
1009
|
+
s += 5;
|
|
1010
|
+
if (kws.includes("ifrs 17") || kws.includes("ifrs-17"))
|
|
1011
|
+
s += 4;
|
|
1012
|
+
if (kws.includes("insurtech"))
|
|
1013
|
+
s += 4;
|
|
1014
|
+
// Don't double-score: if also matches commerce/fintech, subtract
|
|
1015
|
+
// since insurance is distinct domain (not generic fintech)
|
|
1016
|
+
if (d.stack.includes("stripe") && !insuranceLibs.some((l) => d.stack.includes(l))) {
|
|
1017
|
+
s = Math.max(0, s - 2);
|
|
1018
|
+
}
|
|
1019
|
+
return s;
|
|
1020
|
+
},
|
|
1021
|
+
reason: (d) => {
|
|
1022
|
+
const kws = d.readmeKeywords;
|
|
1023
|
+
const bits = [];
|
|
1024
|
+
const insuranceLibs = ["acord-standards", "naic-schemas", "guidewire-cloud", "duck-creek", "majesco-sdk"];
|
|
1025
|
+
insuranceLibs.forEach((l) => { if (d.stack.includes(l))
|
|
1026
|
+
bits.push(l); });
|
|
1027
|
+
if (kws.includes("naic"))
|
|
1028
|
+
bits.push("NAIC mention");
|
|
1029
|
+
if (kws.includes("solvency ii") || kws.includes("solvency-ii"))
|
|
1030
|
+
bits.push("Solvency II mention");
|
|
1031
|
+
if (kws.includes("actuarial"))
|
|
1032
|
+
bits.push("actuarial keyword");
|
|
1033
|
+
if (kws.includes("underwriting"))
|
|
1034
|
+
bits.push("underwriting keyword");
|
|
1035
|
+
if (kws.includes("insurtech"))
|
|
1036
|
+
bits.push("insurtech keyword");
|
|
1037
|
+
return `insurance detected (${bits.join(", ") || "insurance domain signals"}) — NAIC/Solvency II/actuarial-audit gates required`;
|
|
1038
|
+
},
|
|
1039
|
+
},
|
|
1040
|
+
];
|
|
1041
|
+
// Tie-break priority — when two rules score equally, prefer the one
|
|
1042
|
+
// higher in this list (more specific / domain-bound first).
|
|
1043
|
+
const TIE_BREAK_PRIORITY = [
|
|
1044
|
+
"browser-extension", "iot-embedded", "web3", "game",
|
|
1045
|
+
"agent-product", "fintech", "insurance", "healthcare", "edtech", "defense-govcon", "gov-public", "marketplace",
|
|
1046
|
+
"mlops", "streaming",
|
|
1047
|
+
"commerce", "enterprise-saas", "ai-system", "devtools",
|
|
1048
|
+
"data-platform", "cms", "infra", "mobile-app",
|
|
1049
|
+
"regulated", "cli-tool", "web-service", "library", "greenfield",
|
|
1050
|
+
];
|
|
1051
|
+
function priorityIndex(a) {
|
|
1052
|
+
const i = TIE_BREAK_PRIORITY.indexOf(a);
|
|
1053
|
+
return i < 0 ? TIE_BREAK_PRIORITY.length : i;
|
|
1054
|
+
}
|
|
1055
|
+
export function pickArchetype(d) {
|
|
1056
|
+
const scored = RULES
|
|
1057
|
+
.map((r) => ({ archetype: r.archetype, score: r.score(d), reason: r.reason(d) }))
|
|
1058
|
+
.filter((r) => r.score > 0)
|
|
1059
|
+
.sort((a, b) => {
|
|
1060
|
+
if (b.score !== a.score)
|
|
1061
|
+
return b.score - a.score;
|
|
1062
|
+
return priorityIndex(a.archetype) - priorityIndex(b.archetype);
|
|
1063
|
+
});
|
|
1064
|
+
if (scored.length === 0) {
|
|
1065
|
+
return {
|
|
1066
|
+
primary: "greenfield",
|
|
1067
|
+
confidence: "low",
|
|
1068
|
+
rationale: "no strong signals detected — treating as greenfield project",
|
|
1069
|
+
alternatives: [],
|
|
1070
|
+
};
|
|
1071
|
+
}
|
|
1072
|
+
const top = scored[0];
|
|
1073
|
+
const nextBest = scored[1]?.score ?? 0;
|
|
1074
|
+
const gap = top.score - nextBest;
|
|
1075
|
+
// Confidence: high when score≥6 and gap≥3; medium ≥4; else low
|
|
1076
|
+
const confidence = top.score >= 6 && gap >= 3 ? "high" :
|
|
1077
|
+
top.score >= 4 && gap >= 1 ? "medium" : "low";
|
|
1078
|
+
// Dedupe alternatives, keep top 3
|
|
1079
|
+
const seen = new Set([top.archetype]);
|
|
1080
|
+
const alternatives = [];
|
|
1081
|
+
for (const r of scored.slice(1)) {
|
|
1082
|
+
if (!seen.has(r.archetype)) {
|
|
1083
|
+
seen.add(r.archetype);
|
|
1084
|
+
alternatives.push(r.archetype);
|
|
1085
|
+
if (alternatives.length >= 3)
|
|
1086
|
+
break;
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
// ── Pack hints for low/medium confidence or niche stacks ────────────────
|
|
1090
|
+
// Surfaces domain-specific packs that the archetype alone doesn't capture.
|
|
1091
|
+
const suggestedPacks = confidence !== "high"
|
|
1092
|
+
? inferPackHints(d)
|
|
1093
|
+
: inferPackHints(d).filter((p) => isNichePack(p)); // always surface niche packs
|
|
1094
|
+
return {
|
|
1095
|
+
primary: top.archetype,
|
|
1096
|
+
confidence,
|
|
1097
|
+
rationale: top.reason,
|
|
1098
|
+
alternatives,
|
|
1099
|
+
...(suggestedPacks.length > 0 ? { suggestedPacks } : {}),
|
|
1100
|
+
};
|
|
1101
|
+
}
|
|
1102
|
+
/** Niche packs that should surface even at high confidence */
|
|
1103
|
+
function isNichePack(_pack) {
|
|
1104
|
+
return false;
|
|
1105
|
+
}
|
|
1106
|
+
/**
|
|
1107
|
+
* Infer likely domain packs from README/infra keywords when the archetype
|
|
1108
|
+
* scorer doesn't have a dedicated high-confidence rule for the domain.
|
|
1109
|
+
*/
|
|
1110
|
+
function inferPackHints(d) {
|
|
1111
|
+
const hints = [];
|
|
1112
|
+
const kws = new Set([...d.readmeKeywords, ...(d.infraKeywords ?? [])]);
|
|
1113
|
+
const has = (...terms) => terms.some((t) => kws.has(t));
|
|
1114
|
+
if (has("recruit", "hiring", "candidate", "ats", "aedt"))
|
|
1115
|
+
hints.push("hr-ai-pack");
|
|
1116
|
+
if (has("voice", "telephony", "ivr", "stt", "tts", "outbound call"))
|
|
1117
|
+
hints.push("voice-pack");
|
|
1118
|
+
if (has("public api", "api key", "developer portal", "openapi"))
|
|
1119
|
+
hints.push("api-platform-pack");
|
|
1120
|
+
return hints;
|
|
1121
|
+
}
|
|
1122
|
+
// Compliance hints — auto-suggested based on stack and README.
|
|
1123
|
+
export function suggestCompliance(d, archetype) {
|
|
1124
|
+
const c = new Set();
|
|
1125
|
+
// ── archetype defaults ───────────────────────────
|
|
1126
|
+
if (archetype === "commerce") {
|
|
1127
|
+
c.add("pci-dss");
|
|
1128
|
+
c.add("gdpr");
|
|
1129
|
+
}
|
|
1130
|
+
if (archetype === "fintech") {
|
|
1131
|
+
c.add("pci-dss");
|
|
1132
|
+
c.add("sox");
|
|
1133
|
+
c.add("kyc-aml");
|
|
1134
|
+
c.add("gdpr");
|
|
1135
|
+
}
|
|
1136
|
+
if (archetype === "healthcare") {
|
|
1137
|
+
c.add("hipaa");
|
|
1138
|
+
c.add("gdpr");
|
|
1139
|
+
c.add("hitech");
|
|
1140
|
+
}
|
|
1141
|
+
if (archetype === "ai-system" || archetype === "agent-product") {
|
|
1142
|
+
c.add("eu-ai-act");
|
|
1143
|
+
// Add OWASP LLM only if it's truly an agent product
|
|
1144
|
+
if (archetype === "agent-product")
|
|
1145
|
+
c.add("owasp-llm-top-10");
|
|
1146
|
+
}
|
|
1147
|
+
if (archetype === "web3") {
|
|
1148
|
+
c.add("soc2");
|
|
1149
|
+
}
|
|
1150
|
+
if (archetype === "iot-embedded") {
|
|
1151
|
+
c.add("iso27001");
|
|
1152
|
+
c.add("etsi-en-303-645");
|
|
1153
|
+
}
|
|
1154
|
+
if (archetype === "browser-extension") {
|
|
1155
|
+
c.add("csp");
|
|
1156
|
+
c.add("mv3-security");
|
|
1157
|
+
c.add("gdpr");
|
|
1158
|
+
}
|
|
1159
|
+
if (archetype === "game") {
|
|
1160
|
+
c.add("coppa");
|
|
1161
|
+
c.add("age-rating");
|
|
1162
|
+
c.add("accessibility");
|
|
1163
|
+
}
|
|
1164
|
+
if (archetype === "devtools") {
|
|
1165
|
+
c.add("openssf");
|
|
1166
|
+
c.add("api-stability");
|
|
1167
|
+
c.add("soc2-type-2");
|
|
1168
|
+
c.add("gdpr");
|
|
1169
|
+
}
|
|
1170
|
+
if (archetype === "regulated") {
|
|
1171
|
+
c.add("iso27001");
|
|
1172
|
+
c.add("gdpr");
|
|
1173
|
+
c.add("compliance-required");
|
|
1174
|
+
// FedRAMP/CMMC if US federal signals present
|
|
1175
|
+
const kws = d.readmeKeywords;
|
|
1176
|
+
if (kws.includes("fedramp") || kws.includes("fisma") || kws.includes("cmmc"))
|
|
1177
|
+
c.add("fedramp");
|
|
1178
|
+
if (kws.includes("dora ict") || kws.includes("nis2")) {
|
|
1179
|
+
c.add("dora-ict");
|
|
1180
|
+
c.add("nis2");
|
|
1181
|
+
}
|
|
1182
|
+
if (kws.includes("sox compliance") || kws.includes("sarbanes"))
|
|
1183
|
+
c.add("sox");
|
|
1184
|
+
}
|
|
1185
|
+
if (archetype === "web-service")
|
|
1186
|
+
c.add("gdpr"); // baseline for user data
|
|
1187
|
+
if (archetype === "cli-tool") { /* CLI tools usually don't have compliance load */ }
|
|
1188
|
+
if (archetype === "marketplace") {
|
|
1189
|
+
c.add("pci-dss");
|
|
1190
|
+
c.add("kyc-aml");
|
|
1191
|
+
c.add("gdpr");
|
|
1192
|
+
c.add("dsa-eu");
|
|
1193
|
+
c.add("p2b-eu");
|
|
1194
|
+
c.add("1099-k");
|
|
1195
|
+
}
|
|
1196
|
+
if (archetype === "enterprise-saas") {
|
|
1197
|
+
c.add("soc2-type-2");
|
|
1198
|
+
c.add("iso27001");
|
|
1199
|
+
c.add("gdpr");
|
|
1200
|
+
c.add("ccpa");
|
|
1201
|
+
}
|
|
1202
|
+
if (archetype === "mlops") {
|
|
1203
|
+
c.add("eu-ai-act");
|
|
1204
|
+
c.add("nist-ai-rmf");
|
|
1205
|
+
c.add("iso42001");
|
|
1206
|
+
}
|
|
1207
|
+
if (archetype === "streaming") {
|
|
1208
|
+
c.add("gdpr"); // event retention rules
|
|
1209
|
+
}
|
|
1210
|
+
if (archetype === "cms") {
|
|
1211
|
+
c.add("dmca");
|
|
1212
|
+
c.add("wcag-2.2");
|
|
1213
|
+
c.add("gdpr");
|
|
1214
|
+
c.add("dsa-eu");
|
|
1215
|
+
}
|
|
1216
|
+
if (archetype === "edtech") {
|
|
1217
|
+
c.add("coppa");
|
|
1218
|
+
c.add("ferpa");
|
|
1219
|
+
c.add("gdpr-k");
|
|
1220
|
+
c.add("wcag-2.2-aa");
|
|
1221
|
+
c.add("section-508");
|
|
1222
|
+
// State student-privacy laws
|
|
1223
|
+
c.add("sopipa-ca");
|
|
1224
|
+
}
|
|
1225
|
+
if (archetype === "gov-public") {
|
|
1226
|
+
c.add("fedramp");
|
|
1227
|
+
c.add("nist-800-53");
|
|
1228
|
+
c.add("fisma");
|
|
1229
|
+
c.add("section-508");
|
|
1230
|
+
c.add("pia");
|
|
1231
|
+
// CJIS only if law-enforcement keywords present
|
|
1232
|
+
const kws = d.readmeKeywords;
|
|
1233
|
+
if (kws.includes("cjis") || kws.includes("law enforcement") || kws.includes("criminal justice")) {
|
|
1234
|
+
c.add("cjis");
|
|
1235
|
+
}
|
|
1236
|
+
// StateRAMP if state-level
|
|
1237
|
+
if (kws.includes("stateramp") || kws.includes("state government"))
|
|
1238
|
+
c.add("stateramp");
|
|
1239
|
+
c.add("ato"); // Authority to Operate
|
|
1240
|
+
}
|
|
1241
|
+
if (archetype === "insurance") {
|
|
1242
|
+
c.add("naic");
|
|
1243
|
+
c.add("solvency-ii");
|
|
1244
|
+
c.add("ifrs-17");
|
|
1245
|
+
c.add("gdpr");
|
|
1246
|
+
c.add("ccpa");
|
|
1247
|
+
c.add("anti-discrimination-pricing");
|
|
1248
|
+
c.add("actuarial-asops");
|
|
1249
|
+
c.add("state-doi"); // Department of Insurance per US state
|
|
1250
|
+
c.add("naic-ai-model-bulletin");
|
|
1251
|
+
c.add("colorado-sb-21-169"); // US insurance-AI rules
|
|
1252
|
+
}
|
|
1253
|
+
if (archetype === "defense-govcon") {
|
|
1254
|
+
c.add("cmmc-2.0");
|
|
1255
|
+
c.add("nist-800-171");
|
|
1256
|
+
c.add("dfars-252.204-7012");
|
|
1257
|
+
c.add("itar");
|
|
1258
|
+
c.add("ear");
|
|
1259
|
+
c.add("section-889");
|
|
1260
|
+
c.add("fedramp");
|
|
1261
|
+
}
|
|
1262
|
+
// ── stack-derived (cross-archetype) ──────────────
|
|
1263
|
+
if (d.stack.includes("stripe") || d.stack.includes("braintree") ||
|
|
1264
|
+
d.stack.includes("adyen") || d.stack.includes("paddle")) {
|
|
1265
|
+
c.add("pci-dss");
|
|
1266
|
+
}
|
|
1267
|
+
if (d.stack.includes("plaid") || d.stack.includes("dwolla") ||
|
|
1268
|
+
d.stack.includes("teller") || d.stack.includes("wise")) {
|
|
1269
|
+
c.add("kyc-aml");
|
|
1270
|
+
c.add("sox");
|
|
1271
|
+
}
|
|
1272
|
+
if (d.stack.includes("fhir") || d.stack.includes("hl7")) {
|
|
1273
|
+
c.add("hipaa");
|
|
1274
|
+
c.add("hitech");
|
|
1275
|
+
}
|
|
1276
|
+
// ── README-derived ───────────────────────────────
|
|
1277
|
+
if (d.readmeKeywords.includes("regulated"))
|
|
1278
|
+
c.add("compliance-required");
|
|
1279
|
+
if (d.readmeKeywords.includes("healthcare"))
|
|
1280
|
+
c.add("hipaa");
|
|
1281
|
+
if (d.readmeKeywords.includes("fintech"))
|
|
1282
|
+
c.add("sox");
|
|
1283
|
+
return Array.from(c).sort();
|
|
1284
|
+
}
|
|
1285
|
+
/**
|
|
1286
|
+
* Which review agents fire for each archetype. The reviewer name maps to
|
|
1287
|
+
* `agents/<name>.md` in the great_cto plugin. Naming aliases exist for
|
|
1288
|
+
* historical reasons (pci-reviewer covers fintech, firmware-reviewer
|
|
1289
|
+
* covers iot-embedded, etc.) — documented in
|
|
1290
|
+
* docs/agents/REVIEWER-NAMING.md (gap A2 closure).
|
|
1291
|
+
*
|
|
1292
|
+
* `greenfield` has no reviewers — pipeline runs in nano mode only.
|
|
1293
|
+
*/
|
|
1294
|
+
export const REVIEWERS_BY_ARCHETYPE = {
|
|
1295
|
+
"web-service": ["security-officer"],
|
|
1296
|
+
"mobile-app": ["mobile-store-reviewer", "security-officer"],
|
|
1297
|
+
"ai-system": ["ai-security-reviewer", "ai-prompt-architect", "ai-eval-engineer"],
|
|
1298
|
+
"agent-product": ["ai-security-reviewer", "ai-prompt-architect", "ai-eval-engineer"],
|
|
1299
|
+
"mlops": ["mlops-reviewer", "ai-security-reviewer"],
|
|
1300
|
+
"data-platform": ["data-platform-reviewer"],
|
|
1301
|
+
"streaming": ["streaming-reviewer"],
|
|
1302
|
+
"infra": ["infra-reviewer"],
|
|
1303
|
+
"library": ["library-reviewer"],
|
|
1304
|
+
"cli-tool": ["cli-reviewer"],
|
|
1305
|
+
"commerce": ["pci-reviewer", "security-officer"],
|
|
1306
|
+
"marketplace": ["marketplace-reviewer", "pci-reviewer"],
|
|
1307
|
+
"fintech": ["pci-reviewer", "regulated-reviewer"],
|
|
1308
|
+
"healthcare": ["healthcare-reviewer", "security-officer"],
|
|
1309
|
+
"web3": ["oracle-reviewer"],
|
|
1310
|
+
"iot-embedded": ["firmware-reviewer"],
|
|
1311
|
+
"regulated": ["regulated-reviewer"],
|
|
1312
|
+
"devtools": ["devtools-reviewer"],
|
|
1313
|
+
"browser-extension": ["web-store-reviewer"],
|
|
1314
|
+
"game": ["game-reviewer"],
|
|
1315
|
+
"cms": ["cms-reviewer"],
|
|
1316
|
+
"enterprise-saas": ["enterprise-saas-reviewer"],
|
|
1317
|
+
"edtech": ["edtech-reviewer"],
|
|
1318
|
+
"gov-public": ["gov-reviewer", "security-officer"],
|
|
1319
|
+
"insurance": ["insurance-reviewer", "regulated-reviewer"],
|
|
1320
|
+
"defense-govcon": ["cmmc-reviewer", "gov-reviewer", "security-officer"],
|
|
1321
|
+
// Product Builder archetypes (A1–A6) — lean reviewer sets; payments → pci.
|
|
1322
|
+
"vertical-saas": ["security-officer"],
|
|
1323
|
+
"booking": ["pci-reviewer", "security-officer"],
|
|
1324
|
+
"crm": ["security-officer"],
|
|
1325
|
+
"dashboard": ["security-officer"],
|
|
1326
|
+
"content-platform": ["pci-reviewer", "security-officer"],
|
|
1327
|
+
"marketplace-lite": ["pci-reviewer", "security-officer"],
|
|
1328
|
+
"greenfield": [],
|
|
1329
|
+
};
|
|
1330
|
+
/**
|
|
1331
|
+
* Which human gates the pipeline opens for each archetype at the medium
|
|
1332
|
+
* project_size. Smaller sizes skip gates (see `gatesFor()` below); larger
|
|
1333
|
+
* sizes may add compliance.
|
|
1334
|
+
*
|
|
1335
|
+
* NOTE: nano → only [plan]. Enterprise → always adds compliance.
|
|
1336
|
+
*/
|
|
1337
|
+
export const GATES_BY_ARCHETYPE = {
|
|
1338
|
+
"web-service": ["plan", "qa", "ship"],
|
|
1339
|
+
"mobile-app": ["plan", "qa", "ship"],
|
|
1340
|
+
"ai-system": ["plan", "cost", "qa", "security", "ship"],
|
|
1341
|
+
"agent-product": ["plan", "cost", "qa", "security", "ship"],
|
|
1342
|
+
"mlops": ["plan", "cost", "qa", "security", "ship"],
|
|
1343
|
+
"data-platform": ["plan", "qa", "ship"],
|
|
1344
|
+
"streaming": ["plan", "qa", "ship"],
|
|
1345
|
+
"infra": ["plan", "qa", "ship"],
|
|
1346
|
+
"library": ["plan", "qa", "ship"],
|
|
1347
|
+
"cli-tool": ["plan", "qa", "ship"],
|
|
1348
|
+
"commerce": ["plan", "qa", "security", "ship", "compliance"],
|
|
1349
|
+
"marketplace": ["plan", "qa", "security", "ship", "compliance"],
|
|
1350
|
+
"fintech": ["plan", "qa", "security", "ship", "compliance"],
|
|
1351
|
+
"healthcare": ["plan", "qa", "security", "ship", "compliance"],
|
|
1352
|
+
"web3": ["plan", "qa", "oracle-review", "security", "ship"],
|
|
1353
|
+
"iot-embedded": ["plan", "qa", "security", "ship"],
|
|
1354
|
+
"regulated": ["plan", "qa", "security", "ship", "compliance"],
|
|
1355
|
+
"devtools": ["plan", "qa", "ship"],
|
|
1356
|
+
"browser-extension": ["plan", "qa", "ship"],
|
|
1357
|
+
"game": ["plan", "qa", "ship"],
|
|
1358
|
+
"cms": ["plan", "qa", "ship"],
|
|
1359
|
+
"enterprise-saas": ["plan", "qa", "security", "ship", "compliance"],
|
|
1360
|
+
"edtech": ["plan", "qa", "edtech-review", "security", "ship", "compliance"],
|
|
1361
|
+
"gov-public": ["plan", "qa", "gov-review", "security", "ship", "compliance"],
|
|
1362
|
+
"insurance": ["plan", "qa", "insurance-review", "security", "ship", "compliance"],
|
|
1363
|
+
"defense-govcon": ["plan", "qa", "cmmc-assessment", "security", "ship", "compliance"],
|
|
1364
|
+
// Product Builder archetypes (A1–A6) — non-regulated baseline; the single CTO gate
|
|
1365
|
+
// emerges at change_tier T1 (effectiveGates → [plan]).
|
|
1366
|
+
"vertical-saas": ["plan", "qa", "ship"],
|
|
1367
|
+
"booking": ["plan", "qa", "ship"],
|
|
1368
|
+
"crm": ["plan", "qa", "ship"],
|
|
1369
|
+
"dashboard": ["plan", "qa", "ship"],
|
|
1370
|
+
"content-platform": ["plan", "qa", "ship"],
|
|
1371
|
+
"marketplace-lite": ["plan", "qa", "ship"],
|
|
1372
|
+
"greenfield": ["plan"],
|
|
1373
|
+
};
|
|
1374
|
+
/**
|
|
1375
|
+
* Returns the subset of gates the pipeline will actually open for a
|
|
1376
|
+
* given archetype + project_size. Used by the orchestrator before
|
|
1377
|
+
* opening any gate to skip unnecessary human checkpoints on smaller
|
|
1378
|
+
* projects (e.g. nano always skips QA).
|
|
1379
|
+
*/
|
|
1380
|
+
export function gatesFor(archetype, size) {
|
|
1381
|
+
const all = GATES_BY_ARCHETYPE[archetype] ?? [];
|
|
1382
|
+
if (size === "nano")
|
|
1383
|
+
return all.filter((g) => g === "plan");
|
|
1384
|
+
if (size === "small")
|
|
1385
|
+
return all.filter((g) => g === "plan" || g === "ship");
|
|
1386
|
+
if (size === "medium")
|
|
1387
|
+
return all;
|
|
1388
|
+
// large + enterprise → ensure compliance is included
|
|
1389
|
+
return all.includes("compliance") ? all : [...all, "compliance"];
|
|
1390
|
+
}
|
|
1391
|
+
/** Gates that form the non-negotiable floor for a *regulated* archetype. */
|
|
1392
|
+
const FLOOR_GATES = ["security", "compliance", "ship"];
|
|
1393
|
+
/**
|
|
1394
|
+
* The human gates the pipeline opens for a change, given its archetype, the
|
|
1395
|
+
* project size, AND the per-change risk tier. Composes with `gatesFor()`:
|
|
1396
|
+
*
|
|
1397
|
+
* - T2 (default): the full size baseline, with `ship` guaranteed even if a
|
|
1398
|
+
* small project_size had stripped it. An irreversible change always gets a
|
|
1399
|
+
* final go/no-go. Nothing is ever removed → fail-safe for an unknown tier.
|
|
1400
|
+
* - T1: only `plan` (you review intent) PLUS the regulated floor. The standalone
|
|
1401
|
+
* qa/security-as-ceremony is dropped because CI + tests cover a reversible change.
|
|
1402
|
+
* - T0: the regulated floor only — for a plain archetype that is the empty set
|
|
1403
|
+
* (CI is the gate); for a regulated one, security+compliance+ship still fire.
|
|
1404
|
+
*
|
|
1405
|
+
* An archetype is "regulated" iff its baseline contains `security` or `compliance`.
|
|
1406
|
+
* That is what keeps the downgrade sound: a fintech repo cannot ship an
|
|
1407
|
+
* irreversible change unreviewed even when the change is labelled a fix.
|
|
1408
|
+
*
|
|
1409
|
+
* NOTE: change-specific T2 triggers (a migration, a new write-connector, a prod
|
|
1410
|
+
* deploy) are resolved upstream by the classifier, which escalates such a change
|
|
1411
|
+
* to T2 before this function sees it. Here we only map an already-decided tier.
|
|
1412
|
+
*/
|
|
1413
|
+
export function effectiveGates(archetype, size, tier = "T2") {
|
|
1414
|
+
const base = gatesFor(archetype, size);
|
|
1415
|
+
// T2 (and any unrecognized tier) → full baseline, never downgraded, ship forced.
|
|
1416
|
+
if (tier !== "T0" && tier !== "T1") {
|
|
1417
|
+
return base.includes("ship") ? base : [...base, "ship"];
|
|
1418
|
+
}
|
|
1419
|
+
// Regulated archetypes carry a floor that survives every downgrade.
|
|
1420
|
+
const isRegulated = base.includes("security") || base.includes("compliance");
|
|
1421
|
+
const floor = isRegulated ? base.filter((g) => FLOOR_GATES.includes(g)) : [];
|
|
1422
|
+
if (tier === "T1") {
|
|
1423
|
+
const keep = new Set(floor);
|
|
1424
|
+
if (base.includes("plan"))
|
|
1425
|
+
keep.add("plan");
|
|
1426
|
+
return base.filter((g) => keep.has(g)); // preserve base order
|
|
1427
|
+
}
|
|
1428
|
+
// T0 — maintenance.
|
|
1429
|
+
return floor;
|
|
1430
|
+
}
|
|
1431
|
+
/**
|
|
1432
|
+
* Returns the ordered list of reviewers for an archetype. Empty for
|
|
1433
|
+
* `greenfield`. Used by the orchestrator to spawn the right
|
|
1434
|
+
* archetype-specific review stages after senior-dev.
|
|
1435
|
+
*/
|
|
1436
|
+
export function reviewersFor(archetype) {
|
|
1437
|
+
return REVIEWERS_BY_ARCHETYPE[archetype] ?? [];
|
|
1438
|
+
}
|
|
1439
|
+
/**
|
|
1440
|
+
* Horizontal builder agents (the "obвязка") wired into the build pipeline per
|
|
1441
|
+
* Product-Builder archetype: integrations + migration + billing are universal (every
|
|
1442
|
+
* SMB product integrates, imports from an incumbent, and is a paid product —
|
|
1443
|
+
* subscription-billing-engineer self-scopes to one-off when there's no subscription).
|
|
1444
|
+
* connector-builder adds to dashboards (ingest), media-pipeline-engineer to content
|
|
1445
|
+
* platforms. geo-routing-engineer and mobile-app-builder are signal-gated in
|
|
1446
|
+
* compileFlow (routing/maps signals; mobile/field signals), not archetype-static.
|
|
1447
|
+
*/
|
|
1448
|
+
const SMB_BUILD_LAYER = ["app-scaffolder", "auth-engineer"];
|
|
1449
|
+
const SMB_OBVYAZKA = ["integrations-engineer", "migration-import-engineer", "subscription-billing-engineer"];
|
|
1450
|
+
export const BUILDERS_BY_ARCHETYPE = {
|
|
1451
|
+
"vertical-saas": [...SMB_BUILD_LAYER, ...SMB_OBVYAZKA],
|
|
1452
|
+
"booking": [...SMB_BUILD_LAYER, ...SMB_OBVYAZKA],
|
|
1453
|
+
"crm": [...SMB_BUILD_LAYER, ...SMB_OBVYAZKA],
|
|
1454
|
+
"dashboard": [...SMB_BUILD_LAYER, ...SMB_OBVYAZKA, "connector-builder"],
|
|
1455
|
+
"content-platform": [...SMB_BUILD_LAYER, ...SMB_OBVYAZKA, "media-pipeline-engineer"],
|
|
1456
|
+
"marketplace-lite": [...SMB_BUILD_LAYER, ...SMB_OBVYAZKA],
|
|
1457
|
+
};
|
|
1458
|
+
/** Builder agents for an archetype (the obвязка layer). Empty for non-SMB archetypes. */
|
|
1459
|
+
export function buildersFor(archetype) {
|
|
1460
|
+
return BUILDERS_BY_ARCHETYPE[archetype] ?? [];
|
|
1461
|
+
}
|