diff --git a/app/components/Analysis/Card.vue b/app/components/Analysis/Card.vue
index 54f06fa..5d522c9 100644
--- a/app/components/Analysis/Card.vue
+++ b/app/components/Analysis/Card.vue
@@ -14,7 +14,8 @@ const { data, status, error } = useFetch(
query: {
created_at: props.user.created_at,
repos_count: props.user.public_repos,
- pages: 2,
+ pages: 1,
+ ai: true,
},
key: analysisKey,
watch: [username],
diff --git a/app/components/Main/Footer.vue b/app/components/Main/Footer.vue
index 235e46d..ba6249e 100644
--- a/app/components/Main/Footer.vue
+++ b/app/components/Main/Footer.vue
@@ -1,7 +1,7 @@
@@ -100,11 +100,11 @@ const voightKampffVersion = packageJson.version;
- voight-kampff-test v{{ voightKampffVersion }}
+ @unveil/identity v{{ unveilIdentityVersion }}
diff --git a/app/composables/useClassificationDetails.ts b/app/composables/useClassificationDetails.ts
index a39c25b..8aa9040 100644
--- a/app/composables/useClassificationDetails.ts
+++ b/app/composables/useClassificationDetails.ts
@@ -1,4 +1,4 @@
-import { getClassificationDetails } from "voight-kampff-test";
+import { getClassificationDetails } from "@unveil/identity";
type ClassificationDetails = ReturnType;
diff --git a/app/composables/useSeo.ts b/app/composables/useSeo.ts
index 79bbce4..6a4c2b7 100644
--- a/app/composables/useSeo.ts
+++ b/app/composables/useSeo.ts
@@ -1,4 +1,4 @@
-import { getClassificationDetails } from "voight-kampff-test";
+import { getClassificationDetails } from "@unveil/identity";
export function useSeoUser(user: MaybeRefOrGetter) {
const ogTitle = computed(() => {
diff --git a/nuxt.config.ts b/nuxt.config.ts
index c99ec11..738c5cb 100644
--- a/nuxt.config.ts
+++ b/nuxt.config.ts
@@ -9,7 +9,7 @@ export default defineNuxtConfig({
"@vue/devtools-core",
"@vue/devtools-kit",
"dayjs", // CJS
- "voight-kampff-test",
+ "@unveil/identity",
"@vueuse/core",
],
},
@@ -40,6 +40,7 @@ export default defineNuxtConfig({
runtimeConfig: {
githubToken: "",
+ geminiApiKey: "",
},
css: ["~/assets/main.css"],
diff --git a/package-lock.json b/package-lock.json
index 868612f..7c60254 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -7,15 +7,17 @@
"name": "agentscan",
"hasInstallScript": true,
"dependencies": {
+ "@google/generative-ai": "^0.24.1",
"@nuxt/fonts": "^0.14.0",
"@nuxtjs/color-mode": "^4.0.0",
"@unocss/preset-icons": "^66.6.0",
+ "@unveil/compactor": "^1.0.0",
+ "@unveil/identity": "^1.0.0",
"@vueuse/core": "^14.2.1",
"dayjs": "^1.11.19",
"nuxt": "^4.3.1",
"octokit": "^5.0.5",
"valibot": "^1.2.0",
- "voight-kampff-test": "^2.5.0",
"vue": "^3.5.28",
"vue-router": "^4.6.4"
},
@@ -970,6 +972,15 @@
"node": ">=18"
}
},
+ "node_modules/@google/generative-ai": {
+ "version": "0.24.1",
+ "resolved": "https://registry.npmjs.org/@google/generative-ai/-/generative-ai-0.24.1.tgz",
+ "integrity": "sha512-MqO+MLfM6kjxcKoy0p1wRzG3b4ZZXtPI+z2IE26UogS2Cm/XHO+7gGRBh6gcJsOiIVoH93UwKvW4HdgiOZCy9Q==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
"node_modules/@iconify-json/carbon": {
"version": "1.2.18",
"resolved": "https://registry.npmjs.org/@iconify-json/carbon/-/carbon-1.2.18.tgz",
@@ -4479,6 +4490,21 @@
"node": ">=18.12.0"
}
},
+ "node_modules/@unveil/compactor": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/@unveil/compactor/-/compactor-1.0.0.tgz",
+ "integrity": "sha512-/uSDTQWlBYJtmw1A5zOlPAafdfHAwyqaieERjBzOp877VRQ8cAWu1wYR1HQXXifjcbMgy38LwSFQ2r9Q1bMJlQ==",
+ "license": "MIT"
+ },
+ "node_modules/@unveil/identity": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/@unveil/identity/-/identity-1.0.0.tgz",
+ "integrity": "sha512-ggU4BeXv8htcMTp4Pcb6YGc3aNhX7wR7YhbJaamvtf0bmYd3UkNvONVsok15Uuw+UuzFheqtQf1bN1dWubvkyA==",
+ "license": "MIT",
+ "workspaces": [
+ "."
+ ]
+ },
"node_modules/@vercel/nft": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/@vercel/nft/-/nft-1.4.0.tgz",
@@ -5163,9 +5189,9 @@
}
},
"node_modules/anymatch/node_modules/picomatch": {
- "version": "2.3.1",
- "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
- "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
+ "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
"license": "MIT",
"engines": {
"node": ">=8.6"
@@ -5217,9 +5243,9 @@
"license": "MIT"
},
"node_modules/archiver-utils/node_modules/brace-expansion": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
- "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz",
+ "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==",
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0"
@@ -5551,9 +5577,9 @@
"license": "MIT"
},
"node_modules/brace-expansion": {
- "version": "5.0.4",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz",
- "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==",
+ "version": "5.0.5",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz",
+ "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==",
"license": "MIT",
"dependencies": {
"balanced-match": "^4.0.2"
@@ -8288,9 +8314,9 @@
}
},
"node_modules/micromatch/node_modules/picomatch": {
- "version": "2.3.1",
- "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
- "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
+ "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
"license": "MIT",
"engines": {
"node": ">=8.6"
@@ -8601,9 +8627,9 @@
"license": "MIT"
},
"node_modules/node-forge": {
- "version": "1.3.3",
- "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.3.tgz",
- "integrity": "sha512-rLvcdSyRCyouf6jcOIPe/BgwG/d7hKjzMKOas33/pHEr6gbq18IK9zV7DiPvzsz0oBJPme6qr6H6kGZuI9/DZg==",
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.4.0.tgz",
+ "integrity": "sha512-LarFH0+6VfriEhqMMcLX2F7SwSXeWwnEAJEsYm5QKWchiVYVvJyV9v7UDvUv+w5HO23ZpQTXDv/GxdDdMyOuoQ==",
"license": "(BSD-3-Clause OR GPL-2.0)",
"engines": {
"node": ">= 6.13.0"
@@ -9182,9 +9208,9 @@
"license": "ISC"
},
"node_modules/picomatch": {
- "version": "4.0.3",
- "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
- "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
+ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
"license": "MIT",
"engines": {
"node": ">=12"
@@ -9812,9 +9838,9 @@
"license": "MIT"
},
"node_modules/readdir-glob/node_modules/brace-expansion": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
- "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz",
+ "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==",
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0"
@@ -10135,9 +10161,9 @@
}
},
"node_modules/serialize-javascript": {
- "version": "7.0.4",
- "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-7.0.4.tgz",
- "integrity": "sha512-DuGdB+Po43Q5Jxwpzt1lhyFSYKryqoNjQSA9M92tyw0lyHIOur+XCalOUe0KTJpyqzT8+fQ5A0Jf7vCx/NKmIg==",
+ "version": "7.0.5",
+ "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-7.0.5.tgz",
+ "integrity": "sha512-F4LcB0UqUl1zErq+1nYEEzSHJnIwb3AF2XWB94b+afhrekOUijwooAYqFyRbjYkm2PAKBabx6oYv/xDxNi8IBw==",
"license": "BSD-3-Clause",
"engines": {
"node": ">=20.0.0"
@@ -10325,9 +10351,9 @@
}
},
"node_modules/srvx": {
- "version": "0.11.12",
- "resolved": "https://registry.npmjs.org/srvx/-/srvx-0.11.12.tgz",
- "integrity": "sha512-AQfrGqntqVPXgP03pvBDN1KyevHC+KmYVqb8vVf4N+aomQqdhaZxjvoVp+AOm4u6x+GgNQY3MVzAUIn+TqwkOA==",
+ "version": "0.11.13",
+ "resolved": "https://registry.npmjs.org/srvx/-/srvx-0.11.13.tgz",
+ "integrity": "sha512-oknN6qduuMPafxKtHucUeG32Q963pjriA5g3/Bl05cwEsUe5VVbIU4qR9LrALHbipSCyBe+VmfDGGydqazDRkw==",
"license": "MIT",
"bin": {
"srvx": "bin/srvx.mjs"
@@ -11684,16 +11710,6 @@
"@types/estree": "^1.0.0"
}
},
- "node_modules/voight-kampff-test": {
- "version": "2.5.0",
- "resolved": "https://registry.npmjs.org/voight-kampff-test/-/voight-kampff-test-2.5.0.tgz",
- "integrity": "sha512-CHaY0IfiE5WEQol0FZyyNFETojOCxlxc4oacR975HOPHgHG8afRPMGwR5ScHcJ9qCtdIHTLU32OUXWgtQ2HPiA==",
- "license": "MIT",
- "workspaces": [
- ".",
- "playground"
- ]
- },
"node_modules/vscode-uri": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz",
@@ -12044,9 +12060,9 @@
"license": "ISC"
},
"node_modules/yaml": {
- "version": "2.8.2",
- "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz",
- "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==",
+ "version": "2.8.3",
+ "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz",
+ "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==",
"license": "ISC",
"bin": {
"yaml": "bin.mjs"
diff --git a/package.json b/package.json
index 8847b8a..577251c 100644
--- a/package.json
+++ b/package.json
@@ -12,15 +12,17 @@
"test": "vitest"
},
"dependencies": {
+ "@google/generative-ai": "^0.24.1",
"@nuxt/fonts": "^0.14.0",
"@nuxtjs/color-mode": "^4.0.0",
"@unocss/preset-icons": "^66.6.0",
+ "@unveil/compactor": "^1.0.0",
+ "@unveil/identity": "^1.0.0",
"@vueuse/core": "^14.2.1",
"dayjs": "^1.11.19",
"nuxt": "^4.3.1",
"octokit": "^5.0.5",
"valibot": "^1.2.0",
- "voight-kampff-test": "^2.5.0",
"vue": "^3.5.28",
"vue-router": "^4.6.4"
},
diff --git a/server/api/identify-replicant/[username].get.ts b/server/api/identify-replicant/[username].get.ts
index efcaaf9..476386b 100644
--- a/server/api/identify-replicant/[username].get.ts
+++ b/server/api/identify-replicant/[username].get.ts
@@ -1,12 +1,16 @@
-import { identifyReplicant } from "voight-kampff-test";
+import { identify, IdentifyResult } from "@unveil/identity";
import { Octokit } from "octokit";
import * as v from "valibot";
import { formatUsername } from "~~/server/utils/format-username";
+import { GoogleGenerativeAI } from "@google/generative-ai";
+import { compactor } from "@unveil/compactor";
const MIN_PAGES = 1;
-const MAX_PAGES = 2;
+const MAX_PAGES = 1;
+const ITEMS_PER_PAGE = 100;
const QuerySchema = v.object({
+ ai: v.optional(v.boolean(), false),
created_at: v.pipe(
v.string("created_at is required"),
v.check(
@@ -40,6 +44,7 @@ export default defineEventHandler(async (event) => {
const query = getQuery(event);
const parsedQuery = v.safeParse(QuerySchema, {
+ ai: Boolean(query.ai),
created_at: query.created_at,
pages: query.pages ? parseInt(String(query.pages), 10) : 1,
repos_count: query.repos_count
@@ -62,7 +67,7 @@ export default defineEventHandler(async (event) => {
const pageRequests = Array.from({ length: validatedPages }, (_, index) => {
return octokit.rest.activity.listPublicEventsForUser({
username: formattedUsername,
- per_page: 100,
+ per_page: ITEMS_PER_PAGE,
page: index + 1,
});
});
@@ -70,8 +75,193 @@ export default defineEventHandler(async (event) => {
const responses = await Promise.all(pageRequests);
const events = responses.flatMap((response) => response.data);
+ if (parsedQuery.output.ai) {
+ const compactedData = compactor(
+ JSON.stringify({
+ user: {
+ login: formatUsername,
+ created_at: parsedQuery.output.created_at,
+ public_repos: parsedQuery.output.repos_count,
+ },
+ events,
+ }),
+ );
+
+ const systemPrompt = `You are an expert AI system designed to analyze GitHub user accounts and classify them as human-operated ("organic"), bot/automated ("automation"), or mixed behavior patterns.
+
+ ## Important Note
+ This analysis identifies AUTOMATION PATTERNS, not intent or legitimacy. We do not judge whether automation is "good" or "bad". We detect bot-like behavioral signatures to identify automated account activity. This includes spam bots, CI/CD automation left unfiltered, automated contribution patterns, and any coordinated bot behavior - regardless of purpose. A well-intentioned legitimate bot would still be flagged as "automation" if it displays these patterns.
+
+ ## Your Task
+ Analyze a GitHub user's activity data (account metadata and event history) and return a classification indicating whether the account shows AUTOMATION SIGNATURES. Return a humanness score reflecting how automated vs organic the activity patterns appear.
+
+ ## Input Data Structure
+ - user.login: GitHub username
+ - user.created_at: ISO 8601 date string (account creation time)
+ - user.public_repos: number of public repositories owned
+ - events: array of GitHub events with type, created_at, repo.name, payload (payload may contain text content: comments, PR descriptions, review text)
+
+ ## Classification Categories
+ - **organic**: Human-operated account (low bot-like signals, score ≥ 70)
+ - **mixed**: Uncertain patterns (moderate bot-like signals, score 50-69)
+ - **automation**: Likely bot-operated (strong bot-like signals, score < 50)
+
+ ## Analysis Framework
+
+ Evaluate each pattern independently. Assign a score per flag reflecting severity of the detected behavior (low, medium, high).
+
+ ### 1. Account Age Context
+ - New account (< 30 days): Apply stricter scrutiny for bot patterns
+ - Young account (30-89 days): Moderate scrutiny, evaluate patterns carefully
+ - Established account (≥ 90 days): Higher tolerance for activity volume
+
+ ### 2. Repository Activity Baseline
+ - Account has no personal repos but 20+ events: Suspicious pattern
+ - 95%+ external activity with < 5 personal repos: No personal investment
+
+ ### 3. Bot-Like Pattern Detection (12 patterns to evaluate)
+
+ **NOTE: Text Content Analysis is Critical** - When event payloads contain text (comments, PR descriptions, reviews, commit messages), analyze for exact repetition, templates, or automated signatures. Repetition across multiple unrelated activities is a strong automation indicator.
+
+ #### A. Rapid Repository Creation
+ Detect CreateEvent (ref_type="repository") clustering in 24 hours.
+ Pattern: Rapid-fire repo creation suggests automation.
+
+ #### B. Fork Surge
+ Detect ForkEvent clustering in 24-hour window.
+ Pattern: Concentrated forking activity suggests bot behavior.
+
+ #### C. Commit Burst
+ Detect PushEvent clustering: 50+ commits in 1-hour window or 100+ commits in 1-hour window.
+ Pattern: 50+ commits/hour is impossible for human developers. Do NOT assume busy developer - this represents technical limits.
+
+ #### D. 24/7 Activity Pattern
+ Analyze each calendar day: activity spanning 21+ unique hours with minimal rest suggests no sleep.
+ Pattern: Sustained multi-day coding without realistic sleep windows.
+
+ #### E. Event Type Diversity (Shannon's Entropy)
+ Calculate normalized Shannon entropy of event types:
+ - Entropy = Σ(p * log₂(p)) for each type's probability p
+ - Normalized entropy = Entropy / log₂(number_of_types)
+ - Low entropy (< 0.5): Bot-like concentrated profile
+ - High entropy (> 0.8): Suspicious uniform distribution across types
+
+ Pattern: Either narrow rigid focus (few types) OR artificial cycling through all types, combined with no human interactions (comments, reviews, watches).
+
+ #### F. Issue Comment Spam
+ Detect IssueCommentEvent clustering in 2-minute window: 10+ comments across 10+ different repos = high spam, 15+ repos = extreme spam.
+ Pattern: Commenting across 10+ unrelated repos in 2 minutes is impossible for humans. Do NOT tolerate this as "active developer".
+
+ #### G. Branch → Pull Request Correlation
+ Detect pattern: branch created → PR opened within window, repeated consistently.
+ Pattern: Mechanical CI/CD automation cycling (not typical human workflow).
+
+ #### H. PR Volume
+ Detect PR bursts to external repos (young accounts only, < 90 days).
+ Pattern: High external PR volume without personal repo activity.
+
+ #### I. Consecutive Days Activity
+ Count calendar days with any activity.
+ Pattern: 21+ consecutive days suggests either dedication or tireless bot.
+
+ #### J. External Repo Spread
+ Count unique external repos (young accounts only, < 90 days).
+ Pattern: Contributing to many different external repos broadly suggests spray-and-pray behavior.
+
+ #### K. Daily Coding Hour Distribution
+ Analyze hour spread within each calendar day separately.
+ Pattern: High entropy (>0.8) across 16+ hours in a day suggests automated activity cycling.
+
+ #### L. Repetitive or Automated Text Content
+ Analyze text from comments, PR descriptions, review comments, and commit messages for:
+ - Identical or near-identical text repeated across multiple unrelated issues/PRs/repos
+ - Automated comment signatures or templates (e.g., "Automated PR by bot", repeated footers, version strings)
+ - Generic placeholder text (e.g., form-filled descriptions with minimal variation)
+ - Templated responses with only variable substitution (same structure, different params)
+ Pattern: Exact text repetition across many activities or templated/automated language signatures indicate bot behavior.
+
+ ## Scoring Methodology
+ Evaluate all detected patterns independently. For each flag present, assign a severity-based score (0-100 scale per flag).
+ Calculate final humanness score as: average of severity assessments across all flags, weighted by pattern significance.
+
+ - Extreme automated signals: 0-20 (strong bot indicators)
+ - High bot-like behavior: 20-40 (multiple suspicious patterns)
+ - Moderate concerns: 40-60 (mixed or ambiguous signals)
+ - Low concerns: 60-80 (mostly human-like with isolated flags)
+ - Confident human: 80-100 (organic patterns throughout)
+
+ ## Behavioral Context: Activity Bursts
+ Short, intense bursts of activity within very small time frames are NOT typical human behavior. Technical limits to consider:
+ - 50+ commits in 1 hour = highly suspicious (human commit/push cycle is much slower)
+ - 100+ commits in 1 hour = virtually impossible for human coding
+ - 10+ comments across 10+ different repos in 2 minutes = impossible for humans
+ - 15+ repos commented on in 2 minutes = automated commenting bot
+ These represent realistic physical/cognitive limits. Not tolerant of "busy developer" excuses. Short bursts are strong automation indicators.
+
+ ## Time Window Analysis Rules
+ - 24-hour rolling windows: sliding analysis for clustering patterns
+ - Per-day analysis: evaluate each calendar day independently (not globally)
+ - All times treated as UTC
+
+ ## Return JSON Format (MUST be valid JSON only)
+ \`\`\`json
+ {
+ "score": number (0-100),
+ "classification": "organic" | "mixed" | "automation",
+ "flags": [
+ {
+ "label": "string (concise pattern name)",
+ "points": number (severity score for this flag: 0-100),
+ "detail": "string (specific evidence found: counts, timeframes, specifics)"
+ }
+ ],
+ "profile": {
+ "age": number (days since account creation),
+ "repos": number (public repositories count)
+ }
+ }
+ \`\`\`
+
+ ## Output Requirements
+ - Evaluate patterns independently without predetermined point mappings
+ - Assign severity per flag based on strength of evidence
+ - Recognize that short, intense bursts of activity (minutes to hours) are NOT typical human behavior - be realistic about technical limits
+ - Do NOT excuse activity bursts as "very busy developer" - humans have cognitive and physical limits
+ - Analyze text content (comments, PR descriptions, reviews) for repetition and automated language - ALWAYS flag exact text repetition across multiple activities
+ - Provide specific evidence in details (actual counts, timeframes, observed behaviors, text samples if repetition found)
+ - Return ONLY valid JSON - no markdown, no extra text
+ - Include at least one flag per classification (if suspicious/mixed/automation)
+ - If an unexpected pattern emerges requiring a new label, use human-readable format (e.g., "Unusual coordination pattern") instead of snake_case (e.g., "unusual_coordination_pattern")
+ - NOTE: Events are limited to the most recent ${ITEMS_PER_PAGE * MAX_PAGES} public events from the GitHub API. This is NOT the user's complete activity history — draw conclusions accordingly and avoid absolute statements about total activity.
+
+ Be precise, realistic, and evidence-based. Short bursts = automation. Do not be lenient.`;
+ const userPrompt = `Here is the data to analyze: ${compactedData}`;
+
+ try {
+ const genAI = new GoogleGenerativeAI(config.geminiApiKey);
+ const model = genAI.getGenerativeModel({
+ model: "gemini-3.1-flash-lite-preview",
+ generationConfig: {
+ responseMimeType: "application/json",
+ },
+ systemInstruction: systemPrompt,
+ });
+
+ const result = await model.generateContent(userPrompt);
+ const textContent = result.response.text();
+
+ return {
+ analysis: JSON.parse(textContent) as IdentifyResult,
+ eventsCount: events.length,
+ };
+ } catch (aiError: unknown) {
+ console.error("Error during AI analysis:", aiError);
+ throw aiError;
+ }
+ }
+
return {
- analysis: identifyReplicant({
+ analysis: identify({
accountName: formattedUsername,
reposCount: parsedQuery.output.repos_count,
createdAt: parsedQuery.output.created_at,