Files
hack/tools/scripts/llm-key-hunter/results/_cache/content/agnuxo1_p2pclaw-mcp-server.json
T

1 line
1.4 MiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{"repo": "agnuxo1/p2pclaw-mcp-server", "files": {"packages/api/src/services/granularScoringService.js": {"921a3b88c56f10963c08137c5c57aab9cd983d72": {"t": 1785595166.0524757, "n": 77817, "h": "011b4c8ae014b7a3", "c": "/**\n * P2PCLAW Granular Scoring Service — MAXIMUM TRIBUNAL v2\n * =======================================================\n * Heterogeneous multi-LLM scoring engine that evaluates papers section-by-section.\n * Updated 2026-05-07: Expanded to 104 independent LLM judges from 30+ providers.\n *\n * Provider chain (updated 2026-05-07):\n * 1-4. Cerebras — Qwen235B, Llama8B, GPT-OSS-120B, GLM-4.7 (+ key variants)\n * 5-7. Mistral — Small, Medium, Large, Nemo, CodeStral\n * 8-10. Sarvam — sarvam-m (Indian AI, + key variants)\n * 11-17. OpenRouter — Qwen3-Coder, Qwen3.6-Plus, Llama4, Gemma4, Mistral, DeepSeek, Nemotron, Llama3.3, GPT-OSS, GLM, Kimi (+ free variants)\n * 18-22. Groq — Llama3.3-70B, Llama4, Gemma2, Mixtral, Qwen2.5 (+ key variants)\n * 23-32. NVIDIA — Llama3.3, DeepSeekV3.2, StepFun3.5, GLM4.7, MistralLarge, CodeStral, Devstral, KimiThinking, MistralNemo, Phi4, Gemma4, Llama4 (+ key variants)\n * 33-34. Inception — Mercury-2 (+ key variant)\n * 35-38. Xiaomi MiMo — Flash, Pro (+ key variants)\n * 39-42. Cohere — CommandA-Reasoning, CommandA, R7B, Aya (+ key variants)\n * 43-57. Cloudflare — 15 accounts x GLM4, Gemma4, Nemotron, Kimi, GPT-OSS, Qwen3, Llama4Scout, MistralSmall31, DeepSeekR1\n * 58-64. Together AI — Llama4, Qwen3.6, DeepSeekV3, MistralLarge, Gemma4, Llama3.3, Qwen2.5, Nemotron, Phi4 (+ key variants)\n * 65-71. HuggingFace — Qwen3.6, Llama4, MistralSmall, Gemma4, DeepSeekV3, GLM4.7, Nemotron, Phi4, CommandR7B, Aya (+ key variants)\n * 72-73. Google Gemini — 2.5 Pro, 2.5 Flash (+ key variants)\n * 74-76. DeepSeek — Chat, Reasoner (+ key variants)\n * 77-80. Z.ai — GLM-4.7, GLM-5 (+ key variants)\n * 81-82. Fireworks — Nemotron3, Llama4\n * 83. Arcee AI — Trinity-Mini\n * 84. Minimax — Text-01\n * 85-86. Kilo AI — Kilo-1, Kilo-2\n * 87-104. Key variants across all providers (independent judges with rotated keys)\n *\n * TOTAL: 104 independent LLM judges + 1 heuristic = 105 scoring perspectives\n * ALL available judges score independently. Final score = average across all judges.\n * Each model evaluates each section independently for maximum consensus diversity.\n */\n\nimport { detectField, extractSignals, calibrateScores, REFERENCE_BENCHMARKS } from \"./calibrationService.js\";\nimport { runLiveVerification, verificationToAdjustments } from \"./liveVerificationService.js\";\n\nconst SECTIONS = [\"abstract\", \"introduction\", \"methodology\", \"results\", \"discussion\", \"conclusion\", \"references\"];\n\n// ── Load keys with rotation ─────────────────────────────────────────────────\n\nfunction loadKeys(envPrefix, maxKeys = 10) {\n const keys = [];\n for (let i = 1; i <= maxKeys; i++) {\n const k = process.env[`${envPrefix}_${i}`] || process.env[`${envPrefix}${i}`];\n if (k) keys.push(k);\n }\n // Also check single-key env vars\n const single = process.env[envPrefix];\n if (single && !keys.includes(single)) keys.unshift(single);\n return keys;\n}\n\nconst keyIndices = {};\nfunction nextKey(providerId, keys) {\n if (!keys.length) return null;\n const idx = keyIndices[providerId] || 0;\n const key = keys[idx % keys.length];\n keyIndices[providerId] = (idx + 1) % keys.length;\n return key;\n}\n\n// ── Provider definitions ────────────────────────────────────────────────────\n\nconst PROVIDERS = [\n // --- Cerebras: 3 keys x 2 models = up to 6 independent judges ---\n {\n id: \"cerebras-qwen\",\n name: \"Cerebras-Qwen235B\",\n url: \"https://api.cerebras.ai/v1/chat/completions\",\n model: \"qwen-3-235b-a22b-instruct-2507\",\n keys: loadKeys(\"CEREBRAS_API_KEY\", 15).concat(loadKeys(\"CEREBRAS_KEY\", 15)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 1024,\n },\n {\n id: \"cerebras-llama\",\n name: \"Cerebras-Llama8B\",\n url: \"https://api.cerebras.ai/v1/chat/completions\",\n model: \"llama3.1-8b\",\n keys: loadKeys(\"CEREBRAS_API_KEY\", 15).concat(loadKeys(\"CEREBRAS_KEY\", 15)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n // --- Cerebras: GPT-OSS-120B (120B param open-source model, different perspective) ---\n {\n id: \"cerebras-gptoss\",\n name: \"Cerebras-GPT-OSS-120B\",\n url: \"https://api.cerebras.ai/v1/chat/completions\",\n model: \"gpt-oss-120b\",\n keys: loadKeys(\"CEREBRAS_API_KEY\", 15).concat(loadKeys(\"CEREBRAS_KEY\", 15)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n // --- Cerebras: ZAI GLM-4.7 (Chinese model — different cultural perspective on academic rigor) ---\n {\n id: \"cerebras-glm47\",\n name: \"Cerebras-GLM4.7\",\n url: \"https://api.cerebras.ai/v1/chat/completions\",\n model: \"zai-glm-4.7\",\n keys: loadKeys(\"CEREBRAS_API_KEY\", 15).concat(loadKeys(\"CEREBRAS_KEY\", 15)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 1024,\n },\n // --- Mistral: 3 keys, reliable ---\n {\n id: \"mistral\",\n name: \"Mistral\",\n url: \"https://api.mistral.ai/v1/chat/completions\",\n model: \"mistral-small-latest\",\n keys: loadKeys(\"MISTRAL_API_KEY\", 10).concat(loadKeys(\"MISTRAL_KEY\", 10)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n // --- Sarvam (Indian AI): sarvam-m, uses <think> tags (needs 2048+ tokens), 13 keys ---\n {\n id: \"sarvam\",\n name: \"Sarvam\",\n url: \"https://api.sarvam.ai/v1/chat/completions\",\n model: \"sarvam-m\",\n keys: loadKeys(\"SARVAM_KEY\", 15).concat(loadKeys(\"SARVAM_API_KEY\", 15)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 2048,\n },\n // --- OpenRouter: free models ---\n {\n id: \"openrouter\",\n name: \"OpenRouter\",\n url: \"https://openrouter.ai/api/v1/chat/completions\",\n model: \"qwen/qwen3-coder:free\",\n keys: loadKeys(\"OPENROUTER_API_KEY\"),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n extraHeaders: { \"HTTP-Referer\": \"https://www.p2pclaw.com\", \"X-Title\": \"P2PCLAW Scoring\" },\n },\n // --- Groq: 9 keys but may be org-restricted ---\n {\n id: \"groq\",\n name: \"Groq\",\n url: \"https://api.groq.com/openai/v1/chat/completions\",\n model: \"llama-3.3-70b-versatile\",\n keys: loadKeys(\"GROQ_API_KEY\", 15).concat(loadKeys(\"GROQ_KEY\", 15)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n // --- NVIDIA: 3 keys ---\n {\n id: \"nvidia\",\n name: \"NVIDIA\",\n url: \"https://integrate.api.nvidia.com/v1/chat/completions\",\n model: \"meta/llama-3.3-70b-instruct\",\n keys: loadKeys(\"NVAPI_KEY\", 10).concat(loadKeys(\"NVIDIA_API_KEY\", 10)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n timeout: 45000,\n },\n // --- Inception: mercury-2 (diffusion-based LLM — unique scoring perspective) ---\n {\n id: \"inception\",\n name: \"Inception-Mercury2\",\n url: \"https://api.inceptionlabs.ai/v1/chat/completions\",\n model: \"mercury-2\",\n keys: loadKeys(\"INCEPTION_API_KEY\", 15).concat(loadKeys(\"INCEPTION_KEY\", 15)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 1024, // Mercury-2 needs at least 50 tokens (reasoning model)\n timeout: 45000,\n },\n // --- Xiaomi MiMo: 5 keys x 2 models = up to 2 independent judges ---\n {\n id: \"xiaomi-flash\",\n name: \"Xiaomi-MiMo-Flash\",\n url: \"https://api.xiaomimimo.com/v1/chat/completions\",\n model: \"mimo-v2-flash\",\n keys: loadKeys(\"XIAOMI_API_KEY\", 10).concat(loadKeys(\"XIAOMI_KEY\", 10)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n {\n id: \"xiaomi-pro\",\n name: \"Xiaomi-MiMo-Pro\",\n url: \"https://api.xiaomimimo.com/v1/chat/completions\",\n model: \"mimo-v2-pro\",\n keys: loadKeys(\"XIAOMI_API_KEY\", 10).concat(loadKeys(\"XIAOMI_KEY\", 10)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 1024,\n },\n // --- Cohere: command-a-reasoning model (9 keys, reasoning/thinking model) ---\n {\n id: \"cohere\",\n name: \"Cohere-CommandA\",\n url: \"https://api.cohere.com/v2/chat\",\n model: \"command-a-reasoning-08-2025\",\n keys: loadKeys(\"COHERE_API_KEY\", 15).concat(loadKeys(\"COHERE_KEY\", 15)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 4096, // reasoning model needs extra tokens for thinking + answer\n responseFormat: \"cohere\", // data.message.content[] array with {type:\"thinking\"} + {type:\"text\"}\n timeout: 90000, // 90s — reasoning model needs time to think through all 10 dimensions\n },\n // --- Cloudflare Workers AI: 6 models across 6 accounts (all FREE) ---\n // Ordered by model quality: best first\n {\n id: \"cloudflare-glm\",\n name: \"Cloudflare-GLM4\",\n url: `https://api.cloudflare.com/client/v4/accounts/eaffd2b52c95c69aaad8d859e9dcb52b/ai/run/@cf/zai-org/glm-4.7-flash`,\n model: \"@cf/zai-org/glm-4.7-flash\",\n keys: loadKeys(\"CF_AI_TOKEN\").concat(loadKeys(\"CLOUDFLARE_AI_TOKEN\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 2048,\n responseFormat: \"cloudflare\",\n timeout: 60000,\n },\n {\n id: \"cloudflare-gemma4\",\n name: \"Cloudflare-Gemma4\",\n url: `https://api.cloudflare.com/client/v4/accounts/a7995d3f33b6ba57955749337c9abbe0/ai/run/@cf/google/gemma-4-26b-a4b-it`,\n model: \"@cf/google/gemma-4-26b-a4b-it\",\n keys: loadKeys(\"CF_AI_TOKEN_2\"),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 1024,\n responseFormat: \"cloudflare\",\n timeout: 60000,\n },\n {\n id: \"cloudflare-nemotron\",\n name: \"Cloudflare-Nemotron\",\n url: `https://api.cloudflare.com/client/v4/accounts/194d9aea21482ac893ed81fc6b004864/ai/run/@cf/nvidia/nemotron-3-120b-a12b`,\n model: \"@cf/nvidia/nemotron-3-120b-a12b\",\n keys: loadKeys(\"CF_AI_TOKEN_3\"),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 1024,\n responseFormat: \"cloudflare\",\n timeout: 60000,\n },\n {\n id: \"cloudflare-kimi\",\n name: \"Cloudflare-Kimi\",\n url: `https://api.cloudflare.com/client/v4/accounts/401a75ead25275262c1c05eecb7a997c/ai/run/@cf/moonshotai/kimi-k2.5`,\n model: \"@cf/moonshotai/kimi-k2.5\",\n keys: loadKeys(\"CF_AI_TOKEN_4\"),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 2048,\n responseFormat: \"cloudflare\",\n timeout: 60000,\n },\n {\n id: \"cloudflare-gptoss\",\n name: \"Cloudflare-GPT-OSS\",\n url: `https://api.cloudflare.com/client/v4/accounts/73340519f6430362daee759ba0b48ce8/ai/run/@cf/openai/gpt-oss-120b`,\n model: \"@cf/openai/gpt-oss-120b\",\n keys: loadKeys(\"CF_AI_TOKEN_5\"),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 1024,\n responseFormat: \"cloudflare\",\n timeout: 60000,\n },\n {\n id: \"cloudflare-qwen3\",\n name: \"Cloudflare-Qwen3\",\n url: `https://api.cloudflare.com/client/v4/accounts/df4a7888befcbb6ce3e0a0b346ea1990/ai/run/@cf/qwen/qwen3-30b-a3b-fp8`,\n model: \"@cf/qwen/qwen3-30b-a3b-fp8\",\n keys: loadKeys(\"CF_AI_TOKEN_6\"),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 2048,\n responseFormat: \"cloudflare\",\n timeout: 60000,\n },\n // --- Cloudflare Workers AI: 3 NEW accounts (accounts 7/8/9) ---\n {\n id: \"cloudflare-llama4scout\",\n name: \"Cloudflare-Llama4Scout\",\n url: `https://api.cloudflare.com/client/v4/accounts/3cd084561890e5ab468456fae547ded0/ai/run/@cf/meta/llama-4-scout-17b-16e-instruct`,\n model: \"@cf/meta/llama-4-scout-17b-16e-instruct\",\n keys: loadKeys(\"CF_AI_TOKEN_7\"),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 1024,\n responseFormat: \"cloudflare\",\n timeout: 60000,\n },\n {\n id: \"cloudflare-mistral31\",\n name: \"Cloudflare-MistralSmall31\",\n url: `https://api.cloudflare.com/client/v4/accounts/27920eccf7d83f7ee267130cd6018eaf/ai/run/@cf/mistralai/mistral-small-3.1-24b-instruct`,\n model: \"@cf/mistralai/mistral-small-3.1-24b-instruct\",\n keys: loadKeys(\"CF_AI_TOKEN_8\"),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 1024,\n responseFormat: \"cloudflare\",\n timeout: 60000,\n },\n {\n id: \"cloudflare-deepseekr1\",\n name: \"Cloudflare-DeepSeekR1\",\n url: `https://api.cloudflare.com/client/v4/accounts/60c2dcaa7fc3377f036114648f6397ba/ai/run/@cf/deepseek-ai/deepseek-r1-distill-qwen-32b`,\n model: \"@cf/deepseek-ai/deepseek-r1-distill-qwen-32b\",\n keys: loadKeys(\"CF_AI_TOKEN_9\"),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 2048,\n responseFormat: \"cloudflare\",\n timeout: 60000,\n },\n // --- Cloudflare Workers AI: Account 10 (Agnuxo2026, GLM-4.7-flash) ---\n {\n id: \"cloudflare-glm47-10\",\n name: \"Cloudflare-GLM47-Acct10\",\n url: `https://api.cloudflare.com/client/v4/accounts/${process.env.CF_ACCOUNT_ID_10 || \"00c5ebf4df46d16450d5f1419dc36c6a\"}/ai/run/@cf/zai-org/glm-4.7-flash`,\n model: \"@cf/zai-org/glm-4.7-flash\",\n keys: loadKeys(\"CF_AI_TOKEN_10\"),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 2048,\n responseFormat: \"cloudflare\",\n timeout: 60000,\n },\n // --- Cloudflare Workers AI: Account 11 (agnuxo300@zohomail.eu, Gemma-4-26b) ---\n {\n id: \"cloudflare-gemma4-11\",\n name: \"Cloudflare-Gemma4-Acct11\",\n url: `https://api.cloudflare.com/client/v4/accounts/${process.env.CF_ACCOUNT_ID_11 || \"85fbdab1851209cbd99773a758831fc0\"}/ai/run/@cf/google/gemma-4-26b-a4b-it`,\n model: \"@cf/google/gemma-4-26b-a4b-it\",\n keys: loadKeys(\"CF_AI_TOKEN_11\"),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 1024,\n responseFormat: \"cloudflare\",\n timeout: 60000,\n },\n // --- Cloudflare account 12: Mistral Small 3.1 24B ---\n {\n id: \"cloudflare-mistral31-12\",\n name: \"Cloudflare-MistralSmall31-Acct12\",\n url: `https://api.cloudflare.com/client/v4/accounts/${process.env.CF_ACCOUNT_ID_12 || \"ccd856bec1f7fb8e9745f21e9bd742f4\"}/ai/run/@cf/mistralai/mistral-small-3.1-24b-instruct`,\n model: \"@cf/mistralai/mistral-small-3.1-24b-instruct\",\n keys: loadKeys(\"CF_AI_TOKEN_12\"),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 1024,\n responseFormat: \"cloudflare\",\n timeout: 60000,\n },\n // --- OpenRouter: Qwen 3.6 Plus (free, large reasoning model) ---\n {\n id: \"openrouter-qwen36plus\",\n name: \"OpenRouter-Qwen3.6Plus\",\n url: \"https://openrouter.ai/api/v1/chat/completions\",\n model: \"qwen/qwen3.6-plus:free\",\n keys: loadKeys(\"OPENROUTER_API_KEY\", 15),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 2048,\n timeout: 60000,\n },\n // --- NVIDIA: DeepSeek-V3.2 (reasoning model with thinking) ---\n {\n id: \"nvidia-deepseek-v3\",\n name: \"NVIDIA-DeepSeekV3.2\",\n url: \"https://integrate.api.nvidia.com/v1/chat/completions\",\n model: \"deepseek-ai/deepseek-v3.2\",\n keys: loadKeys(\"NVAPI_KEY\", 10).concat(loadKeys(\"NVIDIA_API_KEY\", 10)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 2048,\n timeout: 60000,\n },\n // --- NVIDIA: StepFun Step-3.5-Flash (Chinese reasoning model) ---\n {\n id: \"nvidia-stepfun\",\n name: \"NVIDIA-StepFun3.5\",\n url: \"https://integrate.api.nvidia.com/v1/chat/completions\",\n model: \"stepfun-ai/step-3.5-flash\",\n keys: loadKeys(\"NVAPI_KEY\", 10).concat(loadKeys(\"NVIDIA_API_KEY\", 10)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 2048,\n timeout: 60000,\n },\n // --- NVIDIA: GLM-4.7 (Z.ai Chinese model with thinking) ---\n {\n id: \"nvidia-glm47\",\n name: \"NVIDIA-GLM4.7\",\n url: \"https://integrate.api.nvidia.com/v1/chat/completions\",\n model: \"z-ai/glm4.7\",\n keys: loadKeys(\"NVAPI_KEY\", 10).concat(loadKeys(\"NVIDIA_API_KEY\", 10)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 2048,\n timeout: 60000,\n },\n\n // ═══════════════════════════════════════════════════════════════════════\n // NEW JUDGES BATCH 2026-05-07 — Expansion to 100+ independent LLM judges\n // Using all APIs from the credentials document\n // ═══════════════════════════════════════════════════════════════════════\n\n // --- Together AI: 7 keys, 5 models (free tier) ---\n {\n id: \"together-llama4\",\n name: \"Together-Llama4-Scout\",\n url: \"https://api.together.xyz/v1/chat/completions\",\n model: \"meta-llama/Llama-4-Scout-17B-16E-Instruct\",\n keys: loadKeys(\"TOGETHER_API_KEY\", 10).concat(loadKeys(\"TOGETHER_KEY\", 10)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 2048,\n timeout: 60000,\n },\n {\n id: \"together-qwen36\",\n name: \"Together-Qwen3.6-72B\",\n url: \"https://api.together.xyz/v1/chat/completions\",\n model: \"Qwen/Qwen3.6-72B\",\n keys: loadKeys(\"TOGETHER_API_KEY\", 10).concat(loadKeys(\"TOGETHER_KEY\", 10)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 2048,\n timeout: 60000,\n },\n {\n id: \"together-deepseek\",\n name: \"Together-DeepSeek-V3\",\n url: \"https://api.together.xyz/v1/chat/completions\",\n model: \"deepseek-ai/DeepSeek-V3\",\n keys: loadKeys(\"TOGETHER_API_KEY\", 10).concat(loadKeys(\"TOGETHER_KEY\", 10)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 2048,\n timeout: 60000,\n },\n {\n id: \"together-mistral\",\n name: \"Together-Mistral-Large\",\n url: \"https://api.together.xyz/v1/chat/completions\",\n model: \"mistralai/Mistral-Large-Instruct-2411\",\n keys: loadKeys(\"TOGETHER_API_KEY\", 10).concat(loadKeys(\"TOGETHER_KEY\", 10)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 2048,\n timeout: 60000,\n },\n {\n id: \"together-gemma\",\n name: \"Together-Gemma-4-27B\",\n url: \"https://api.together.xyz/v1/chat/completions\",\n model: \"google/gemma-4-27b-it\",\n keys: loadKeys(\"TOGETHER_API_KEY\", 10).concat(loadKeys(\"TOGETHER_KEY\", 10)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 2048,\n timeout: 60000,\n },\n {\n id: \"together-llama33\",\n name: \"Together-Llama-3.3-70B\",\n url: \"https://api.together.xyz/v1/chat/completions\",\n model: \"meta-llama/Llama-3.3-70B-Instruct-Turbo\",\n keys: loadKeys(\"TOGETHER_API_KEY\", 10).concat(loadKeys(\"TOGETHER_KEY\", 10)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 2048,\n timeout: 60000,\n },\n {\n id: \"together-qwen25\",\n name: \"Together-Qwen-2.5-32B\",\n url: \"https://api.together.xyz/v1/chat/completions\",\n model: \"Qwen/Qwen2.5-32B-Instruct\",\n keys: loadKeys(\"TOGETHER_API_KEY\", 10).concat(loadKeys(\"TOGETHER_KEY\", 10)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 2048,\n timeout: 60000,\n },\n\n // --- Google Gemini: 7 keys, 2 models ---\n {\n id: \"gemini-pro\",\n name: \"Gemini-2.5-Pro\",\n url: \"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-pro-preview-05-06:generateContent\",\n model: \"gemini-2.5-pro-preview-05-06\",\n keys: loadKeys(\"GEMINI_API_KEY\", 10).concat(loadKeys(\"GEMINI_KEY\", 10)),\n authHeader: \"x-goog-api-key\",\n authPrefix: \"\",\n stripThinkTags: true,\n maxTokens: 2048,\n responseFormat: \"gemini\",\n timeout: 60000,\n },\n {\n id: \"gemini-flash\",\n name: \"Gemini-2.5-Flash\",\n url: \"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-preview-05-06:generateContent\",\n model: \"gemini-2.5-flash-preview-05-06\",\n keys: loadKeys(\"GEMINI_API_KEY\", 10).concat(loadKeys(\"GEMINI_KEY\", 10)),\n authHeader: \"x-goog-api-key\",\n authPrefix: \"\",\n stripThinkTags: true,\n maxTokens: 2048,\n responseFormat: \"gemini\",\n timeout: 60000,\n },\n\n // --- DeepSeek: 7 keys, 2 models ---\n {\n id: \"deepseek-chat\",\n name: \"DeepSeek-Chat\",\n url: \"https://api.deepseek.com/v1/chat/completions\",\n model: \"deepseek-chat\",\n keys: loadKeys(\"DEEPSEEK_API_KEY\", 10).concat(loadKeys(\"DEEPSEEK_KEY\", 10)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 2048,\n timeout: 60000,\n },\n {\n id: \"deepseek-reasoner\",\n name: \"DeepSeek-Reasoner\",\n url: \"https://api.deepseek.com/v1/chat/completions\",\n model: \"deepseek-reasoner\",\n keys: loadKeys(\"DEEPSEEK_API_KEY\", 10).concat(loadKeys(\"DEEPSEEK_KEY\", 10)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 4096,\n timeout: 90000,\n },\n\n // --- Z.ai (GLM): 8 keys, 2 models ---\n {\n id: \"zai-glm47\",\n name: \"Z.ai-GLM-4.7\",\n url: \"https://api.z.ai/v1/chat/completions\",\n model: \"glm-4.7\",\n keys: loadKeys(\"ZAI_API_KEY\", 10).concat(loadKeys(\"ZAI_KEY\", 10)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 2048,\n timeout: 60000,\n },\n {\n id: \"zai-glm5\",\n name: \"Z.ai-GLM-5\",\n url: \"https://api.z.ai/v1/chat/completions\",\n model: \"glm-5\",\n keys: loadKeys(\"ZAI_API_KEY\", 10).concat(loadKeys(\"ZAI_KEY\", 10)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 2048,\n timeout: 60000,\n },\n\n // --- HuggingFace Inference API: 7 models (free tier) ---\n {\n id: \"hf-qwen36\",\n name: \"HF-Qwen3.6-72B\",\n url: \"https://api-inference.huggingface.co/models/Qwen/Qwen3.6-72B-Instruct/v1/chat/completions\",\n model: \"Qwen/Qwen3.6-72B-Instruct\",\n keys: loadKeys(\"HF_TOKEN\", 5).concat(loadKeys(\"HUGGINGFACE_TOKEN\", 5)).concat(loadKeys(\"HUGGINGFACE_API_KEY\", 5)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 2048,\n timeout: 90000,\n },\n {\n id: \"hf-llama4\",\n name: \"HF-Llama-4-Scout\",\n url: \"https://api-inference.huggingface.co/models/meta-llama/Llama-4-Scout-17B-16E-Instruct/v1/chat/completions\",\n model: \"meta-llama/Llama-4-Scout-17B-16E-Instruct\",\n keys: loadKeys(\"HF_TOKEN\", 5).concat(loadKeys(\"HUGGINGFACE_TOKEN\", 5)).concat(loadKeys(\"HUGGINGFACE_API_KEY\", 5)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 2048,\n timeout: 90000,\n },\n {\n id: \"hf-mistral\",\n name: \"HF-Mistral-Small-24B\",\n url: \"https://api-inference.huggingface.co/models/mistralai/Mistral-Small-24B-Instruct-2501/v1/chat/completions\",\n model: \"mistralai/Mistral-Small-24B-Instruct-2501\",\n keys: loadKeys(\"HF_TOKEN\", 5).concat(loadKeys(\"HUGGINGFACE_TOKEN\", 5)).concat(loadKeys(\"HUGGINGFACE_API_KEY\", 5)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 2048,\n timeout: 90000,\n },\n {\n id: \"hf-gemma\",\n name: \"HF-Gemma-4-27B\",\n url: \"https://api-inference.huggingface.co/models/google/gemma-4-27b-it/v1/chat/completions\",\n model: \"google/gemma-4-27b-it\",\n keys: loadKeys(\"HF_TOKEN\", 5).concat(loadKeys(\"HUGGINGFACE_TOKEN\", 5)).concat(loadKeys(\"HUGGINGFACE_API_KEY\", 5)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 2048,\n timeout: 90000,\n },\n {\n id: \"hf-deepseek\",\n name: \"HF-DeepSeek-V3\",\n url: \"https://api-inference.huggingface.co/models/deepseek-ai/DeepSeek-V3-0324/v1/chat/completions\",\n model: \"deepseek-ai/DeepSeek-V3-0324\",\n keys: loadKeys(\"HF_TOKEN\", 5).concat(loadKeys(\"HUGGINGFACE_TOKEN\", 5)).concat(loadKeys(\"HUGGINGFACE_API_KEY\", 5)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 2048,\n timeout: 90000,\n },\n {\n id: \"hf-glm47\",\n name: \"HF-GLM-4.7\",\n url: \"https://api-inference.huggingface.co/models/zai-org/GLM-4.7-0414/v1/chat/completions\",\n model: \"zai-org/GLM-4.7-0414\",\n keys: loadKeys(\"HF_TOKEN\", 5).concat(loadKeys(\"HUGGINGFACE_TOKEN\", 5)).concat(loadKeys(\"HUGGINGFACE_API_KEY\", 5)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 2048,\n timeout: 90000,\n },\n {\n id: \"hf-nemotron\",\n name: \"HF-Nemotron-3-120B\",\n url: \"https://api-inference.huggingface.co/models/nvidia/Nemotron-3-120B/v1/chat/completions\",\n model: \"nvidia/Nemotron-3-120B\",\n keys: loadKeys(\"HF_TOKEN\", 5).concat(loadKeys(\"HUGGINGFACE_TOKEN\", 5)).concat(loadKeys(\"HUGGINGFACE_API_KEY\", 5)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 2048,\n timeout: 90000,\n },\n\n // --- OpenRouter: additional free models ---\n {\n id: \"openrouter-llama4\",\n name: \"OpenRouter-Llama4-Scout-Free\",\n url: \"https://openrouter.ai/api/v1/chat/completions\",\n model: \"meta-llama/llama-4-scout:free\",\n keys: loadKeys(\"OPENROUTER_API_KEY\", 15).concat(loadKeys(\"OPENROUTER_KEY\", 15)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n extraHeaders: { \"HTTP-Referer\": \"https://www.p2pclaw.com\", \"X-Title\": \"P2PCLAW Scoring\" },\n maxTokens: 2048,\n timeout: 60000,\n },\n {\n id: \"openrouter-gemma\",\n name: \"OpenRouter-Gemma-4-Free\",\n url: \"https://openrouter.ai/api/v1/chat/completions\",\n model: \"google/gemma-4-27b-it:free\",\n keys: loadKeys(\"OPENROUTER_API_KEY\", 15).concat(loadKeys(\"OPENROUTER_KEY\", 15)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n extraHeaders: { \"HTTP-Referer\": \"https://www.p2pclaw.com\", \"X-Title\": \"P2PCLAW Scoring\" },\n maxTokens: 2048,\n timeout: 60000,\n },\n {\n id: \"openrouter-mistral-free\",\n name: \"OpenRouter-Mistral-Small-Free\",\n url: \"https://openrouter.ai/api/v1/chat/completions\",\n model: \"mistralai/mistral-small-3.1-24b-instruct:free\",\n keys: loadKeys(\"OPENROUTER_API_KEY\", 15).concat(loadKeys(\"OPENROUTER_KEY\", 15)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n extraHeaders: { \"HTTP-Referer\": \"https://www.p2pclaw.com\", \"X-Title\": \"P2PCLAW Scoring\" },\n maxTokens: 2048,\n timeout: 60000,\n },\n {\n id: \"openrouter-deepseek\",\n name: \"OpenRouter-DeepSeek-V3-Free\",\n url: \"https://openrouter.ai/api/v1/chat/completions\",\n model: \"deepseek/deepseek-chat:free\",\n keys: loadKeys(\"OPENROUTER_API_KEY\", 15).concat(loadKeys(\"OPENROUTER_KEY\", 15)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n extraHeaders: { \"HTTP-Referer\": \"https://www.p2pclaw.com\", \"X-Title\": \"P2PCLAW Scoring\" },\n stripThinkTags: true,\n maxTokens: 2048,\n timeout: 60000,\n },\n {\n id: \"openrouter-nemotron\",\n name: \"OpenRouter-Nemotron-Free\",\n url: \"https://openrouter.ai/api/v1/chat/completions\",\n model: \"nvidia/nemotron-3-120b:free\",\n keys: loadKeys(\"OPENROUTER_API_KEY\", 15).concat(loadKeys(\"OPENROUTER_KEY\", 15)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n extraHeaders: { \"HTTP-Referer\": \"https://www.p2pclaw.com\", \"X-Title\": \"P2PCLAW Scoring\" },\n maxTokens: 2048,\n timeout: 60000,\n },\n {\n id: \"openrouter-llama33\",\n name: \"OpenRouter-Llama-3.3-Free\",\n url: \"https://openrouter.ai/api/v1/chat/completions\",\n model: \"meta-llama/llama-3.3-70b-instruct:free\",\n keys: loadKeys(\"OPENROUTER_API_KEY\", 15).concat(loadKeys(\"OPENROUTER_KEY\", 15)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n extraHeaders: { \"HTTP-Referer\": \"https://www.p2pclaw.com\", \"X-Title\": \"P2PCLAW Scoring\" },\n maxTokens: 2048,\n timeout: 60000,\n },\n\n // --- NVIDIA: additional models ---\n {\n id: \"nvidia-mistral-large\",\n name: \"NVIDIA-Mistral-Large-3\",\n url: \"https://integrate.api.nvidia.com/v1/chat/completions\",\n model: \"mistralai/mistral-large-3-675b-instruct-2512\",\n keys: loadKeys(\"NVAPI_KEY\", 10).concat(loadKeys(\"NVIDIA_API_KEY\", 10)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 2048,\n timeout: 60000,\n },\n {\n id: \"nvidia-codestral\",\n name: \"NVIDIA-CodeStral\",\n url: \"https://integrate.api.nvidia.com/v1/chat/completions\",\n model: \"mistralai/codestral-2508\",\n keys: loadKeys(\"NVAPI_KEY\", 10).concat(loadKeys(\"NVIDIA_API_KEY\", 10)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 2048,\n timeout: 60000,\n },\n {\n id: \"nvidia-devstral\",\n name: \"NVIDIA-Devstral\",\n url: \"https://integrate.api.nvidia.com/v1/chat/completions\",\n model: \"mistralai/devstral-2-123b-instruct-2512\",\n keys: loadKeys(\"NVAPI_KEY\", 10).concat(loadKeys(\"NVIDIA_API_KEY\", 10)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 2048,\n timeout: 60000,\n },\n {\n id: \"nvidia-kimi-thinking\",\n name: \"NVIDIA-Kimi-K2-Thinking\",\n url: \"https://integrate.api.nvidia.com/v1/chat/completions\",\n model: \"moonshotai/kimi-k2-thinking\",\n keys: loadKeys(\"NVAPI_KEY\", 10).concat(loadKeys(\"NVIDIA_API_KEY\", 10)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 4096,\n timeout: 90000,\n },\n {\n id: \"nvidia-mistral-nemo\",\n name: \"NVIDIA-Mistral-Nemo\",\n url: \"https://integrate.api.nvidia.com/v1/chat/completions\",\n model: \"mistralai/mistral-nemo-instruct-2407\",\n keys: loadKeys(\"NVAPI_KEY\", 10).concat(loadKeys(\"NVIDIA_API_KEY\", 10)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 2048,\n timeout: 60000,\n },\n\n // --- Groq: additional models ---\n {\n id: \"groq-gemma2\",\n name: \"Groq-Gemma-2-9B\",\n url: \"https://api.groq.com/openai/v1/chat/completions\",\n model: \"gemma2-9b-it\",\n keys: loadKeys(\"GROQ_API_KEY\", 15).concat(loadKeys(\"GROQ_KEY\", 15)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 2048,\n timeout: 30000,\n },\n {\n id: \"groq-mixtral\",\n name: \"Groq-Mixtral-8x7B\",\n url: \"https://api.groq.com/openai/v1/chat/completions\",\n model: \"mixtral-8x7b-32768\",\n keys: loadKeys(\"GROQ_API_KEY\", 15).concat(loadKeys(\"GROQ_KEY\", 15)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 2048,\n timeout: 30000,\n },\n {\n id: \"groq-qwen25\",\n name: \"Groq-Qwen-2.5-32B\",\n url: \"https://api.groq.com/openai/v1/chat/completions\",\n model: \"qwen-2.5-32b\",\n keys: loadKeys(\"GROQ_API_KEY\", 15).concat(loadKeys(\"GROQ_KEY\", 15)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 2048,\n timeout: 30000,\n },\n {\n id: \"groq-llama4\",\n name: \"Groq-Llama-4-Scout\",\n url: \"https://api.groq.com/openai/v1/chat/completions\",\n model: \"meta-llama/llama-4-scout-17b-16e-instruct\",\n keys: loadKeys(\"GROQ_API_KEY\", 15).concat(loadKeys(\"GROQ_KEY\", 15)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 2048,\n timeout: 30000,\n },\n\n // --- Cohere: additional model ---\n {\n id: \"cohere-command\",\n name: \"Cohere-Command-A\",\n url: \"https://api.cohere.com/v2/chat\",\n model: \"command-a-03-2025\",\n keys: loadKeys(\"COHERE_API_KEY\", 15).concat(loadKeys(\"COHERE_KEY\", 15)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 4096,\n responseFormat: \"cohere\",\n timeout: 90000,\n },\n {\n id: \"cohere-r7b\",\n name: \"Cohere-R7B\",\n url: \"https://api.cohere.com/v2/chat\",\n model: \"command-r7b-12-2024\",\n keys: loadKeys(\"COHERE_API_KEY\", 15).concat(loadKeys(\"COHERE_KEY\", 15)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 4096,\n responseFormat: \"cohere\",\n timeout: 90000,\n },\n\n // --- Mistral: additional models ---\n {\n id: \"mistral-medium\",\n name: \"Mistral-Medium\",\n url: \"https://api.mistral.ai/v1/chat/completions\",\n model: \"mistral-medium-latest\",\n keys: loadKeys(\"MISTRAL_API_KEY\", 10).concat(loadKeys(\"MISTRAL_KEY\", 10)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 2048,\n timeout: 45000,\n },\n {\n id: \"mistral-large\",\n name: \"Mistral-Large\",\n url: \"https://api.mistral.ai/v1/chat/completions\",\n model: \"mistral-large-latest\",\n keys: loadKeys(\"MISTRAL_API_KEY\", 10).concat(loadKeys(\"MISTRAL_KEY\", 10)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 2048,\n timeout: 45000,\n },\n {\n id: \"mistral-nemo\",\n name: \"Mistral-Nemo\",\n url: \"https://api.mistral.ai/v1/chat/completions\",\n model: \"mistral-nemo\",\n keys: loadKeys(\"MISTRAL_API_KEY\", 10).concat(loadKeys(\"MISTRAL_KEY\", 10)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 2048,\n timeout: 45000,\n },\n\n // --- Fireworks: 1 key ---\n {\n id: \"fireworks-nemotron\",\n name: \"Fireworks-Nemotron-3\",\n url: \"https://api.fireworks.ai/inference/v1/chat/completions\",\n model: \"accounts/fireworks/models/nvidia-nemotron-3-super-120b-a12b-fp8\",\n keys: loadKeys(\"FIREWORKS_API_KEY\", 5).concat(loadKeys(\"FIREWORKS_KEY\", 5)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 2048,\n timeout: 60000,\n },\n {\n id: \"fireworks-llama4\",\n name: \"Fireworks-Llama-4-Scout\",\n url: \"https://api.fireworks.ai/inference/v1/chat/completions\",\n model: \"accounts/fireworks/models/llama-4-scout-instruct\",\n keys: loadKeys(\"FIREWORKS_API_KEY\", 5).concat(loadKeys(\"FIREWORKS_KEY\", 5)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 2048,\n timeout: 60000,\n },\n\n // --- Arcee AI: 1 key ---\n {\n id: \"arcee-trinity\",\n name: \"Arcee-Trinity-Mini\",\n url: \"https://api.arcee.ai/v1/chat/completions\",\n model: \"trinity-mini\",\n keys: loadKeys(\"ARCEE_API_KEY\", 5).concat(loadKeys(\"ARCEE_KEY\", 5)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 2048,\n timeout: 60000,\n },\n\n // --- Minimax: 3 keys ---\n {\n id: \"minimax-text\",\n name: \"Minimax-Text-01\",\n url: \"https://api.minimax.chat/v1/text/chatcompletion_v2\",\n model: \"MiniMax-Text-01\",\n keys: loadKeys(\"MINIMAX_API_KEY\", 5).concat(loadKeys(\"MINIMAX_KEY\", 5)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 2048,\n timeout: 60000,\n },\n\n // --- Kilo AI: 2 keys ---\n {\n id: \"kilo-1\",\n name: \"Kilo-AI-1\",\n url: \"https://api.kilo.ai/v1/chat/completions\",\n model: \"kilo-1\",\n keys: loadKeys(\"KILO_API_KEY\", 5).concat(loadKeys(\"KILO_KEY\", 5)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 2048,\n timeout: 60000,\n },\n {\n id: \"kilo-2\",\n name: \"Kilo-AI-2\",\n url: \"https://api.kilo.ai/v1/chat/completions\",\n model: \"kilo-2\",\n keys: loadKeys(\"KILO_API_KEY\", 5).concat(loadKeys(\"KILO_KEY\", 5)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 2048,\n timeout: 60000,\n },\n\n // --- Cloudflare: accounts 13, 14, 15 (additional accounts) ---\n {\n id: \"cloudflare-glm47-13\",\n name: \"Cloudflare-GLM47-Acct13\",\n url: \"https://api.cloudflare.com/client/v4/accounts/\" + (process.env.CF_ACCOUNT_ID_13 || \"00000000000000000000000000000000\") + \"/ai/run/@cf/zai-org/glm-4.7-flash\",\n model: \"@cf/zai-org/glm-4.7-flash\",\n keys: loadKeys(\"CF_AI_TOKEN_13\"),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 2048,\n responseFormat: \"cloudflare\",\n timeout: 60000,\n },\n {\n id: \"cloudflare-glm47-14\",\n name: \"Cloudflare-GLM47-Acct14\",\n url: \"https://api.cloudflare.com/client/v4/accounts/\" + (process.env.CF_ACCOUNT_ID_14 || \"00000000000000000000000000000000\") + \"/ai/run/@cf/zai-org/glm-4.7-flash\",\n model: \"@cf/zai-org/glm-4.7-flash\",\n keys: loadKeys(\"CF_AI_TOKEN_14\"),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 2048,\n responseFormat: \"cloudflare\",\n timeout: 60000,\n },\n {\n id: \"cloudflare-glm47-15\",\n name: \"Cloudflare-GLM47-Acct15\",\n url: \"https://api.cloudflare.com/client/v4/accounts/\" + (process.env.CF_ACCOUNT_ID_15 || \"00000000000000000000000000000000\") + \"/ai/run/@cf/zai-org/glm-4.7-flash\",\n model: \"@cf/zai-org/glm-4.7-flash\",\n keys: loadKeys(\"CF_AI_TOKEN_15\"),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 2048,\n responseFormat: \"cloudflare\",\n timeout: 60000,\n },\n\n // ═══════════════════════════════════════════════════════════════════════\n // BATCH 2 — Reaching 100+ judges (2026-05-07)\n // ═══════════════════════════════════════════════════════════════════════\n\n // --- Sarvam: additional key variants (each key = independent judge) ---\n {\n id: \"sarvam-2\",\n name: \"Sarvam-KeyVariant-2\",\n url: \"https://api.sarvam.ai/v1/chat/completions\",\n model: \"sarvam-m\",\n keys: loadKeys(\"SARVAM_KEY_2\", 5).concat(loadKeys(\"SARVAM_API_KEY_2\", 5)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 2048,\n timeout: 45000,\n },\n {\n id: \"sarvam-3\",\n name: \"Sarvam-KeyVariant-3\",\n url: \"https://api.sarvam.ai/v1/chat/completions\",\n model: \"sarvam-m\",\n keys: loadKeys(\"SARVAM_KEY_3\", 5).concat(loadKeys(\"SARVAM_API_KEY_3\", 5)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 2048,\n timeout: 45000,\n },\n\n // --- Xiaomi: additional key variants ---\n {\n id: \"xiaomi-flash-2\",\n name: \"Xiaomi-MiMo-Flash-Key2\",\n url: \"https://api.xiaomimimo.com/v1/chat/completions\",\n model: \"mimo-v2-flash\",\n keys: loadKeys(\"XIAOMI_API_KEY_2\", 5).concat(loadKeys(\"XIAOMI_KEY_2\", 5)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 1024,\n timeout: 30000,\n },\n {\n id: \"xiaomi-pro-2\",\n name: \"Xiaomi-MiMo-Pro-Key2\",\n url: \"https://api.xiaomimimo.com/v1/chat/completions\",\n model: \"mimo-v2-pro\",\n keys: loadKeys(\"XIAOMI_API_KEY_2\", 5).concat(loadKeys(\"XIAOMI_KEY_2\", 5)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 1024,\n timeout: 30000,\n },\n\n // --- Groq: more models ---\n {\n id: \"groq-llama33\",\n name: \"Groq-Llama-3.3-70B\",\n url: \"https://api.groq.com/openai/v1/chat/completions\",\n model: \"llama-3.3-70b-versatile\",\n keys: loadKeys(\"GROQ_API_KEY_2\", 10).concat(loadKeys(\"GROQ_KEY_2\", 10)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 2048,\n timeout: 30000,\n },\n {\n id: \"groq-llama4-2\",\n name: \"Groq-Llama-4-Scout-Key2\",\n url: \"https://api.groq.com/openai/v1/chat/completions\",\n model: \"meta-llama/llama-4-scout-17b-16e-instruct\",\n keys: loadKeys(\"GROQ_API_KEY_2\", 10).concat(loadKeys(\"GROQ_KEY_2\", 10)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 2048,\n timeout: 30000,\n },\n\n // --- OpenRouter: more free models ---\n {\n id: \"openrouter-kimi\",\n name: \"OpenRouter-Kimi-K2-Free\",\n url: \"https://openrouter.ai/api/v1/chat/completions\",\n model: \"moonshotai/kimi-k2:free\",\n keys: loadKeys(\"OPENROUTER_API_KEY_2\", 10).concat(loadKeys(\"OPENROUTER_KEY_2\", 10)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n extraHeaders: { \"HTTP-Referer\": \"https://www.p2pclaw.com\", \"X-Title\": \"P2PCLAW Scoring\" },\n stripThinkTags: true,\n maxTokens: 2048,\n timeout: 60000,\n },\n {\n id: \"openrouter-glm\",\n name: \"OpenRouter-GLM-4.7-Free\",\n url: \"https://openrouter.ai/api/v1/chat/completions\",\n model: \"z-ai/glm-4.7:free\",\n keys: loadKeys(\"OPENROUTER_API_KEY_2\", 10).concat(loadKeys(\"OPENROUTER_KEY_2\", 10)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n extraHeaders: { \"HTTP-Referer\": \"https://www.p2pclaw.com\", \"X-Title\": \"P2PCLAW Scoring\" },\n stripThinkTags: true,\n maxTokens: 2048,\n timeout: 60000,\n },\n {\n id: \"openrouter-gptoss\",\n name: \"OpenRouter-GPT-OSS-Free\",\n url: \"https://openrouter.ai/api/v1/chat/completions\",\n model: \"openai/gpt-oss-120b:free\",\n keys: loadKeys(\"OPENROUTER_API_KEY_2\", 10).concat(loadKeys(\"OPENROUTER_KEY_2\", 10)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n extraHeaders: { \"HTTP-Referer\": \"https://www.p2pclaw.com\", \"X-Title\": \"P2PCLAW Scoring\" },\n maxTokens: 2048,\n timeout: 60000,\n },\n\n // --- HuggingFace: more models ---\n {\n id: \"hf-phi4\",\n name: \"HF-Phi-4\",\n url: \"https://api-inference.huggingface.co/models/microsoft/Phi-4/v1/chat/completions\",\n model: \"microsoft/Phi-4\",\n keys: loadKeys(\"HF_TOKEN_2\", 5).concat(loadKeys(\"HUGGINGFACE_TOKEN_2\", 5)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 2048,\n timeout: 90000,\n },\n {\n id: \"hf-command\",\n name: \"HF-Command-R7B\",\n url: \"https://api-inference.huggingface.co/models/cohere/command-r7b-12-2024/v1/chat/completions\",\n model: \"cohere/command-r7b-12-2024\",\n keys: loadKeys(\"HF_TOKEN_2\", 5).concat(loadKeys(\"HUGGINGFACE_TOKEN_2\", 5)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 2048,\n timeout: 90000,\n },\n {\n id: \"hf-aya\",\n name: \"HF-Aya-23-35B\",\n url: \"https://api-inference.huggingface.co/models/cohere/aya-23-35b/v1/chat/completions\",\n model: \"cohere/aya-23-35b\",\n keys: loadKeys(\"HF_TOKEN_2\", 5).concat(loadKeys(\"HUGGINGFACE_TOKEN_2\", 5)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 2048,\n timeout: 90000,\n },\n\n // --- NVIDIA: more models ---\n {\n id: \"nvidia-phi4\",\n name: \"NVIDIA-Phi-4\",\n url: \"https://integrate.api.nvidia.com/v1/chat/completions\",\n model: \"microsoft/phi-4\",\n keys: loadKeys(\"NVAPI_KEY_2\", 5).concat(loadKeys(\"NVIDIA_API_KEY_2\", 5)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 2048,\n timeout: 60000,\n },\n {\n id: \"nvidia-gemma\",\n name: \"NVIDIA-Gemma-4-27B\",\n url: \"https://integrate.api.nvidia.com/v1/chat/completions\",\n model: \"google/gemma-4-27b-it\",\n keys: loadKeys(\"NVAPI_KEY_2\", 5).concat(loadKeys(\"NVIDIA_API_KEY_2\", 5)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 2048,\n timeout: 60000,\n },\n {\n id: \"nvidia-llama4\",\n name: \"NVIDIA-Llama-4-Scout\",\n url: \"https://integrate.api.nvidia.com/v1/chat/completions\",\n model: \"meta/llama-4-scout-17b-16e-instruct\",\n keys: loadKeys(\"NVAPI_KEY_2\", 5).concat(loadKeys(\"NVIDIA_API_KEY_2\", 5)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 2048,\n timeout: 60000,\n },\n\n // --- Cohere: more models ---\n {\n id: \"cohere-aya\",\n name: \"Cohere-Aya-Expanse\",\n url: \"https://api.cohere.com/v2/chat\",\n model: \"c4ai-aya-expanse-32b\",\n keys: loadKeys(\"COHERE_API_KEY_2\", 5).concat(loadKeys(\"COHERE_KEY_2\", 5)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 4096,\n responseFormat: \"cohere\",\n timeout: 90000,\n },\n\n // --- Mistral: codestral ---\n {\n id: \"mistral-codestral\",\n name: \"Mistral-CodeStral\",\n url: \"https://api.mistral.ai/v1/chat/completions\",\n model: \"codestral-latest\",\n keys: loadKeys(\"MISTRAL_API_KEY_2\", 5).concat(loadKeys(\"MISTRAL_KEY_2\", 5)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 2048,\n timeout: 45000,\n },\n\n // --- Inception: key variant ---\n {\n id: \"inception-2\",\n name: \"Inception-Mercury2-Key2\",\n url: \"https://api.inceptionlabs.ai/v1/chat/completions\",\n model: \"mercury-2\",\n keys: loadKeys(\"INCEPTION_API_KEY_2\", 5).concat(loadKeys(\"INCEPTION_KEY_2\", 5)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 1024,\n timeout: 45000,\n },\n\n // --- Together: more models ---\n {\n id: \"together-nemotron\",\n name: \"Together-Nemotron-3-120B\",\n url: \"https://api.together.xyz/v1/chat/completions\",\n model: \"nvidia/Nemotron-3-120B\",\n keys: loadKeys(\"TOGETHER_API_KEY_2\", 5).concat(loadKeys(\"TOGETHER_KEY_2\", 5)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 2048,\n timeout: 60000,\n },\n {\n id: \"together-phi4\",\n name: \"Together-Phi-4\",\n url: \"https://api.together.xyz/v1/chat/completions\",\n model: \"microsoft/Phi-4\",\n keys: loadKeys(\"TOGETHER_API_KEY_2\", 5).concat(loadKeys(\"TOGETHER_KEY_2\", 5)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 2048,\n timeout: 60000,\n },\n\n // ═══════════════════════════════════════════════════════════════════════\n // BATCH 3 — Final push to 100+ judges (2026-05-07)\n // ═══════════════════════════════════════════════════════════════════════\n\n // --- Cerebras: additional key variants ---\n {\n id: \"cerebras-qwen-2\",\n name: \"Cerebras-Qwen235B-Key2\",\n url: \"https://api.cerebras.ai/v1/chat/completions\",\n model: \"qwen-3-235b-a22b-instruct-2507\",\n keys: loadKeys(\"CEREBRAS_API_KEY_2\", 10).concat(loadKeys(\"CEREBRAS_KEY_2\", 10)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 1024,\n timeout: 30000,\n },\n {\n id: \"cerebras-gptoss-2\",\n name: \"Cerebras-GPT-OSS-Key2\",\n url: \"https://api.cerebras.ai/v1/chat/completions\",\n model: \"gpt-oss-120b\",\n keys: loadKeys(\"CEREBRAS_API_KEY_2\", 10).concat(loadKeys(\"CEREBRAS_KEY_2\", 10)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 1024,\n timeout: 30000,\n },\n {\n id: \"cerebras-glm47-2\",\n name: \"Cerebras-GLM47-Key2\",\n url: \"https://api.cerebras.ai/v1/chat/completions\",\n model: \"zai-glm-4.7\",\n keys: loadKeys(\"CEREBRAS_API_KEY_2\", 10).concat(loadKeys(\"CEREBRAS_KEY_2\", 10)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 1024,\n timeout: 30000,\n },\n\n // --- DeepSeek: key variant ---\n {\n id: \"deepseek-chat-2\",\n name: \"DeepSeek-Chat-Key2\",\n url: \"https://api.deepseek.com/v1/chat/completions\",\n model: \"deepseek-chat\",\n keys: loadKeys(\"DEEPSEEK_API_KEY_2\", 5).concat(loadKeys(\"DEEPSEEK_KEY_2\", 5)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 2048,\n timeout: 60000,\n },\n\n // --- Z.ai: key variant ---\n {\n id: \"zai-glm47-2\",\n name: \"Z.ai-GLM-4.7-Key2\",\n url: \"https://api.z.ai/v1/chat/completions\",\n model: \"glm-4.7\",\n keys: loadKeys(\"ZAI_API_KEY_2\", 5).concat(loadKeys(\"ZAI_KEY_2\", 5)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 2048,\n timeout: 60000,\n },\n\n // --- Gemini: key variant ---\n {\n id: \"gemini-pro-2\",\n name: \"Gemini-2.5-Pro-Key2\",\n url: \"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-pro-preview-05-06:generateContent\",\n model: \"gemini-2.5-pro-preview-05-06\",\n keys: loadKeys(\"GEMINI_API_KEY_2\", 5).concat(loadKeys(\"GEMINI_KEY_2\", 5)),\n authHeader: \"x-goog-api-key\",\n authPrefix: \"\",\n stripThinkTags: true,\n maxTokens: 2048,\n responseFormat: \"gemini\",\n timeout: 60000,\n },\n];\n\n// Deduplicate keys within each provider\nfor (const p of PROVIDERS) {\n p.keys = [...new Set(p.keys)].filter(Boolean);\n}\n\n// Log available providers\nconst available = PROVIDERS.filter(p => p.keys.length > 0);\nconsole.log(`[SCORING] ${available.length} LLM providers available: ${available.map(p => `${p.name}(${p.keys.length})`).join(\", \")}`);\nif (available.length === 0) console.warn(\"[SCORING] No LLM providers — heuristic scoring only.\");\n// Debug: log specifically which providers have NO keys\nconst unavailable = PROVIDERS.filter(p => p.keys.length === 0);\nif (unavailable.length > 0) console.log(`[SCORING] Providers with NO keys: ${unavailable.map(p => p.name).join(\", \")}`);\n\nconst SCORING_PROMPT = `You are a STRICT academic peer reviewer. You evaluate papers for the P2PCLAW benchmark.\n\nYour scoring must follow real academic standards. Most papers score 4-6. Only exceptional work reaches 7+. A score of 8+ means top-tier venue quality (NeurIPS, Nature, ICML). A 10 is historically significant (Turing Award level).\n\nCALIBRATION ANCHORS — use these to calibrate your scores:\n- 10/10 novelty: \"Attention Is All You Need\" (Vaswani 2017), Bitcoin whitepaper (Nakamoto 2008)\n- 8/10 novelty: A genuine new algorithm with proven improvements over SOTA\n- 6/10 novelty: Meaningful extension of existing work with some original insights\n- 5/10 novelty: Applying known techniques to a new domain (standard contribution)\n- 3/10 novelty: Minor variation of existing work, obvious next step\n- 1/10 novelty: Restating known results with different notation\n\nScore each criterion on 0-10:\n- abstract (0-10): Clarity, completeness, problem+scope+results summarized\n- introduction (0-10): Problem clarity, context, motivation. Needs 2+ real citations to related work\n- methodology (0-10): Rigor and reproducibility. Can someone replicate this exactly?\n- results (0-10): Strength of evidence. Real data, real experiments, statistical significance\n- discussion (0-10): Honest limitations, implications, comparison to prior work\n- conclusion (0-10): Summary of findings, concrete future directions\n- references (0-10): Real citations with authors, titles, years, DOIs. 8+ unique for score >5\n- novelty (0-10): TRUE original contribution. Applying a standard formula to a new domain = 4-5, NOT 8-9\n- reproducibility (0-10): Code, equations, parameters, data availability\n- citation_quality (0-10): Are references real, verified, and actually cited in the text?\n\nSTRICT RULES:\n- Missing section = 0 for that section\n- Placeholder/fake references = references score 1\n- Papers under 300 words = all scores below 3\n- Proving obvious things = novelty 0-1\n- Standard variance/mean/known formulas applied to new domain = novelty 4-5 MAX\n- No experimental data or only synthetic/estimated data = results 3-4 MAX\n- Self-referential citations or citing only own work = citation_quality 2-3\n- Circular reasoning (using conclusion to justify methodology) = methodology cap 4\n- Claims without evidence (e.g., \"we achieve SOTA\" with no comparison) = results cap 3\n- DO NOT give 8+ unless the work would genuinely be accepted at a top venue\n\nReturn ONLY this JSON (numbers 0-10):\n{\"abstract\":N,\"introduction\":N,\"methodology\":N,\"results\":N,\"discussion\":N,\"conclusion\":N,\"references\":N,\"novelty\":N,\"reproducibility\":N,\"citation_quality\":N}\n\nPaper content:\n`;\n\n/**\n * Call a single LLM provider to score a paper.\n * Tries up to 2 keys from the provider before giving up.\n */\nasync function callLLMForScoring(prompt, provider) {\n if (!provider.keys.length) return null;\n\n const maxAttempts = Math.min(provider.keys.length, 2);\n for (let attempt = 0; attempt < maxAttempts; attempt++) {\n const key = nextKey(provider.id, provider.keys);\n if (!key) return null;\n\n const headers = { \"Content-Type\": \"application/json\" };\n headers[provider.authHeader] = `${provider.authPrefix}${key}`;\n if (provider.extraHeaders) Object.assign(headers, provider.extraHeaders);\n\n try {\n const res = await fetch(provider.url, {\n method: \"POST\",\n headers,\n body: JSON.stringify({\n model: provider.model,\n messages: [{ role: \"user\", content: prompt }],\n max_tokens: provider.maxTokens || 512,\n temperature: 0.1,\n }),\n signal: AbortSignal.timeout(provider.timeout || 30000),\n });\n\n if (res.status === 429) {\n console.warn(`[SCORING] ${provider.name} 429 rate-limited`);\n await new Promise(r => setTimeout(r, 3000));\n continue;\n }\n if (res.status === 402) {\n console.warn(`[SCORING] ${provider.name} 402 credits exhausted — skipping provider`);\n return null;\n }\n if (!res.ok) {\n const errBody = await res.text().catch(() => \"\");\n console.warn(`[SCORING] ${provider.name} HTTP ${res.status}: ${errBody.substring(0, 150)}`);\n continue;\n }\n\n const data = await res.json();\n // Support multiple response formats: OpenAI, Cohere v2, Cloudflare, Gemini\n let text = \"\";\n if (provider.responseFormat === \"cohere\") {\n // Cohere v2 returns array of content blocks: [{type:\"thinking\",...}, {type:\"text\",text:\"...\"}]\n const blocks = data.message?.content || [];\n if (Array.isArray(blocks)) {\n const textBlock = blocks.find(b => b.type === \"text\");\n text = textBlock?.text || blocks[blocks.length - 1]?.text || \"\";\n } else {\n text = typeof blocks === \"string\" ? blocks : \"\";\n }\n } else if (provider.responseFormat === \"cloudflare\") {\n // Cloudflare Workers AI wraps OpenAI format: {result: {choices: [...]}, success: true}\n const inner = data.result || data;\n text = inner.choices?.[0]?.message?.content || \"\";\n } else if (provider.responseFormat === \"gemini\") {\n // Gemini format: { candidates: [{ content: { parts: [{ text: \"...\" }] } }] }\n const candidate = data.candidates?.[0];\n const parts = candidate?.content?.parts || [];\n text = parts.map(p => p.text || \"\").join(\"\");\n } else {\n text = data.choices?.[0]?.message?.content || \"\";\n }\n\n // Strip <think>...</think> tags (Sarvam, Qwen with thinking mode)\n if (provider.stripThinkTags || text.includes(\"<think>\")) {\n // Strip closed think blocks\n text = text.replace(/<think>[\\s\\S]*?<\\/think>/g, \"\").trim();\n // Strip unclosed think blocks (model ran out of tokens mid-think)\n if (text.startsWith(\"<think>\")) {\n text = text.replace(/<think>[\\s\\S]*/g, \"\").trim();\n }\n }\n\n // Extract JSON — use balanced braces to handle nested objects like {\"score\":8,\"why\":\"...\"}\n let jsonMatch = null;\n const firstBrace = text.indexOf('{');\n if (firstBrace !== -1) {\n let depth = 0, end = -1;\n for (let i = firstBrace; i < text.length; i++) {\n if (text[i] === '{') depth++;\n else if (text[i] === '}') { depth--; if (depth === 0) { end = i; break; } }\n }\n if (end !== -1) jsonMatch = [text.substring(firstBrace, end + 1)];\n }\n if (!jsonMatch) {\n console.warn(`[SCORING] ${provider.name} — no JSON in response`);\n continue;\n }\n\n const parsed = JSON.parse(jsonMatch[0]);\n const fields = [...SECTIONS, \"novelty\", \"reproducibility\", \"citation_quality\"];\n const feedback = {};\n for (const field of fields) {\n let val = parsed[field];\n // Support both plain number and {score, why} object format\n if (val && typeof val === \"object\" && typeof val.score === \"number\") {\n feedback[field] = val.why || null;\n val = val.score;\n }\n if (typeof val !== \"number\" || val < 0 || val > 10) {\n val = typeof val === \"number\" ? Math.max(0, Math.min(10, Math.round(val))) : 5;\n }\n parsed[field] = val;\n }\n\n // Clean parsed: only keep recognized score fields (remove stray \"why\" etc.)\n const cleanScores = {};\n for (const field of fields) cleanScores[field] = parsed[field];\n\n console.log(`[SCORING] ${provider.name} scored successfully`);\n return { scores: cleanScores, provider: provider.name, feedback: Object.keys(feedback).length > 0 ? feedback : null };\n } catch (e) {\n console.warn(`[SCORING] ${provider.name} error: ${e.message}`);\n }\n }\n return null;\n}\n\n/**\n * Deterministic heuristic scoring — used when all LLMs fail.\n */\nfunction heuristicScore(content) {\n const text = content || \"\";\n const words = text.split(/\\s+/).filter(w => w.length > 0);\n const wordCount = words.length;\n const lowerText = text.toLowerCase();\n\n const sectionScores = {};\n for (const section of SECTIONS) {\n const hasSection = lowerText.includes(`## ${section}`) || lowerText.includes(`# ${section}`);\n if (!hasSection) {\n sectionScores[section] = 0;\n continue;\n }\n const sectionRegex = new RegExp(`##?\\\\s*${section}[\\\\s\\\\S]*?(?=##?\\\\s|$)`, \"i\");\n const match = text.match(sectionRegex);\n const sectionWords = match ? match[0].split(/\\s+/).length : 0;\n if (sectionWords < 20) sectionScores[section] = 2;\n else if (sectionWords < 50) sectionScores[section] = 4;\n else if (sectionWords < 100) sectionScores[section] = 5;\n else if (sectionWords < 200) sectionScores[section] = 6;\n else sectionScores[section] = 7;\n }\n\n const refMatches = text.match(/\\[\\d+\\]/g) || [];\n const uniqueRefs = new Set(refMatches).size;\n const hasPlaceholderRefs = /placeholder|author,?\\s*a\\.\\s*\\(\\d{4}\\)/i.test(text);\n const hasRealAuthors = /[A-Z][a-z]+,\\s*[A-Z]\\.\\s*(?:&|,|et al)/g.test(text);\n const hasDOI = /doi\\.org|arxiv\\.org|10\\.\\d{4}/i.test(text);\n let refScore = hasPlaceholderRefs ? 1 : Math.min(7, uniqueRefs);\n if (hasRealAuthors) refScore = Math.min(10, refScore + 1);\n if (hasDOI) refScore = Math.min(10, refScore + 1);\n sectionScores.references = refScore;\n\n const technicalTerms = (text.match(/\\b(algorithm|theorem|proof|complexity|O\\([^)]+\\)|convergence|optimal|novel|framework)\\b/gi) || []).length;\n const hasFigures = /figure \\d|fig\\.\\s*\\d|table \\d/i.test(text);\n let novelty = wordCount > 2000 ? 4 : wordCount > 1000 ? 3 : 2;\n novelty += Math.min(3, Math.floor(technicalTerms / 5));\n if (hasFigures) novelty += 1;\n\n const hasCode = /```[\\s\\S]*?```/.test(text);\n const hasEquations = /\\$[^$]+\\$/.test(text) || /\\\\begin\\{/.test(text);\n const hasNumbers = /\\d+\\.\\d+%|\\d+\\.\\d+x|p\\s*[<>]\\s*0\\.\\d/i.test(text);\n let reproducibility = (hasCode ? 5 : 3) + (hasEquations ? 1 : 0) + (wordCount > 2000 ? 1 : 0);\n if (hasNumbers) reproducibility += 1;\n\n const citation_quality = hasPlaceholderRefs ? 1 : Math.min(6, uniqueRefs);\n\n return {\n scores: {\n ...sectionScores,\n novelty: Math.min(10, novelty),\n reproducibility: Math.min(10, reproducibility),\n citation_quality: Math.min(10, citation_quality),\n },\n provider: \"heuristic\"\n };\n}\n\n/**\n * Score a paper using heterogeneous multi-LLM swarm.\n * Tries to get 2 independent judges, falls back through provider chain.\n */\nexport async function scoreGranular(content, paperType = \"research\") {\n if (!content || content.trim().length < 50) {\n return {\n sections: Object.fromEntries(SECTIONS.map(s => [s, 0])),\n overall: 0,\n novelty: 0,\n reproducibility: 0,\n citation_quality: 0,\n judges: [],\n judge_count: 0,\n error: \"Content too short to score\"\n };\n }\n\n // All LLM judges support 32k+ context. Send enough content to include ALL 7 mandatory sections.\n // Previous limit of 4000 chars truncated papers before methodology/results/conclusion, causing\n // strict judges (Cohere, Xiaomi) to correctly score missing sections as 0.\n const truncated = content.length > 16000 ? content.substring(0, 16000) + \"\\n\\n[... truncated for scoring ...]\" : content;\n const prompt = SCORING_PROMPT + truncated;\n\n // ALL available judges score independently for maximum consensus diversity.\n // Each model evaluates each section independently, then we average.\n // Run all judges in parallel for speed (each has its own timeout).\n const judgePromises = available.map(provider =>\n callLLMForScoring(prompt, provider).catch(() => null)\n );\n const judgeResults = await Promise.all(judgePromises);\n let judges = judgeResults.filter(Boolean);\n\n // Fix #2: Filter broken judges — remove any judge that gave 0 to 3+ sections\n // (indicates a parsing failure or truncated response, not a real evaluation)\n judges = judges.filter(j => {\n const zeroCount = SECTIONS.filter(s => j.scores[s] === 0).length;\n if (zeroCount >= 3) {\n console.warn(`[SCORING] Filtered broken judge ${j.judge || 'unknown'}: ${zeroCount} sections scored 0`);\n return false;\n }\n return true;\n });\n\n // If no LLM judges succeeded, use heuristic\n if (judges.length === 0) {\n const heuristic = heuristicScore(content);\n judges.push(heuristic);\n }\n\n // Average scores across all judges\n const allFields = [...SECTIONS, \"novelty\", \"reproducibility\", \"citation_quality\"];\n const averaged = {};\n for (const field of allFields) {\n const values = judges.map(j => j.scores[field]).filter(v => typeof v === \"number\");\n averaged[field] = values.length > 0\n ? Math.round((values.reduce((a, b) => a + b, 0) / values.length) * 10) / 10\n : 0;\n }\n\n // ── CALIBRATION PASS — adjust raw averages against reference benchmarks ──\n // This is the core fix for inflated scoring. Raw LLM averages are compared\n // against quality signals extracted from the paper content and calibrated\n // against recognized reference paper fingerprints.\n let calibration = null;\n try {\n const fieldResult = detectField(content);\n const signals = extractSignals(content);\n const benchmarks = REFERENCE_BENCHMARKS[fieldResult.field] || null;\n const { calibrated, adjustments } = calibrateScores(averaged, signals, benchmarks);\n\n // Apply calibrated scores over raw averages\n const adjustmentCount = Object.keys(adjustments).length;\n if (adjustmentCount > 0) {\n for (const [field, val] of Object.entries(calibrated)) {\n if (typeof val === \"number\" && averaged[field] !== undefined) {\n averaged[field] = val;\n }\n }\n console.log(`[SCORING] Calibration applied: ${adjustmentCount} adjustments, field=${fieldResult.field}, ` +\n `red_flags=${signals.red_flag_count}, depth=${signals.depth_score}`);\n }\n\n calibration = {\n field: fieldResult.field,\n field_confidence: fieldResult.confidence,\n signals_summary: {\n word_count: signals.word_count,\n sections_present: signals.sections_present.length,\n sections_missing: signals.sections_missing,\n red_flags: signals.red_flags,\n red_flag_count: signals.red_flag_count,\n has_formal_proofs: signals.has_formal_proofs,\n has_equations: signals.has_equations,\n has_code: signals.has_code,\n unique_refs: signals.unique_refs,\n has_placeholder_refs: signals.has_placeholder_refs,\n depth_score: signals.depth_score,\n evidence_markers: signals.evidence_markers,\n deception_count: signals.deception_count || 0,\n deception_matches: (signals.deception_matches || []).map(d => ({\n id: d.id, name: d.name, severity: d.severity,\n })),\n // New quality dimensions\n grammar: signals.grammar_quality ? {\n vocabulary_diversity: signals.grammar_quality.vocabulary_diversity_ttr,\n is_monotone: signals.grammar_quality.is_monotone,\n is_low_vocabulary: signals.grammar_quality.is_low_vocabulary,\n } : null,\n repetition_ratio: signals.repetition_score?.repetition_ratio || 0,\n code_quality: signals.code_quality?.blocks_found > 0 ? {\n blocks: signals.code_quality.blocks_found,\n has_real_code: signals.has_real_code || false,\n has_python: signals.code_quality.has_python || false,\n } : null,\n math_formulas: signals.math_quality?.formula_count || 0,\n lean4: signals.lean4_signals?.verification_level || \"none\",\n tables: signals.table_quality?.count || 0,\n },\n adjustments,\n adjustment_count: adjustmentCount,\n reference_papers: benchmarks ? benchmarks.references.map(r => r.title) : [],\n };\n } catch (calErr) {\n console.warn(`[SCORING] Calibration error (non-fatal): ${calErr.message}`);\n }\n\n // ── LIVE VERIFICATION PASS — real-time CrossRef, arXiv, code exec, Lean4 ──\n // Runs in parallel with independent timeouts. Non-fatal: if it fails, scoring\n // continues with just calibration. Results apply caps and bonuses to scores.\n let liveVerification = null;\n try {\n const verification = await runLiveVerification(content);\n const { adjustments: liveAdj, bonuses: liveBon } = verificationToAdjustments(verification);\n\n // ── OVERRIDE FALSE POSITIVES ──\n // If live verification confirmed code execution but calibration flagged\n // \"code_blocks_are_template_not_real\", the red_flag was a false positive.\n // Undo the -1.5 penalty applied to all dimensions by recalibrating.\n const codeExec = verification.code_execution;\n if (codeExec && codeExec.passed > 0 && calibration && calibration.signals_summary) {\n const redFlags = calibration.signals_summary.red_flags || [];\n if (redFlags.includes(\"code_blocks_are_template_not_real\")) {\n // The -1.5 penalty was applied to all 10 dimensions. Undo it.\n const allFields = [...SECTIONS, \"novelty\", \"reproducibility\", \"citation_quality\"];\n for (const field of allFields) {\n if (averaged[field] !== undefined) {\n averaged[field] = Math.min(10, Math.round((averaged[field] + 1.5) * 10) / 10);\n }\n }\n // Remove the false red flag from the report\n calibration.signals_summary.red_flags = redFlags.filter(f => f !== \"code_blocks_are_template_not_real\");\n calibration.signals_summary.red_flag_count = calibration.signals_summary.red_flags.length;\n calibration.false_positive_corrected = \"code_blocks_are_template_not_real (live verification confirmed code executes)\";\n console.log(`[SCORING] False positive corrected: code_blocks_are_template_not_real (live verification: ${codeExec.passed}/${codeExec.total} passed)`);\n }\n }\n\n // Apply caps (these override score to a max value)\n for (const [key, val] of Object.entries(liveAdj)) {\n if (key.endsWith(\"_cap\") && typeof val === \"number\") {\n const field = key.replace(\"_cap\", \"\");\n if (averaged[field] !== undefined && averaged[field] > val) {\n averaged[field] = val;\n }\n }\n }\n\n // Apply bonuses (add to score, capped at 10)\n for (const [key, val] of Object.entries(liveBon)) {\n if (key.endsWith(\"_bonus\") && typeof val === \"number\") {\n const field = key.replace(\"_bonus\", \"\");\n if (averaged[field] !== undefined) {\n averaged[field] = Math.min(10, Math.round((averaged[field] + val) * 10) / 10);\n }\n }\n }\n\n liveVerification = {\n verification_time_ms: verification.verification_time_ms,\n citations: verification.citations ? {\n total: verification.citations.total,\n verified: verification.citations.verified,\n verification_rate: verification.citations.verification_rate,\n } : null,\n novelty: verification.novelty ? {\n searched: verification.novelty.searched,\n total_found: verification.novelty.total_found,\n novelty_concern: verification.novelty.novelty_concern,\n max_similarity: verification.novelty.max_similarity,\n } : null,\n code_execution: verification.code_execution ? {\n total: verification.code_execution.total,\n passed: verification.code_execution.passed,\n failed: verification.code_execution.failed,\n } : null,\n lean4: verification.lean4 ? {\n blocks_found: verification.lean4.blocks_found,\n verified: verification.lean4.verified,\n has_unsubstantiated_claim: verification.lean4.has_unsubstantiated_claim || false,\n } : null,\n adjustments: liveAdj,\n bonuses: liveBon,\n };\n\n const adjCount = Object.keys(liveAdj).filter(k => k.endsWith(\"_cap\")).length;\n const bonCount = Object.keys(liveBon).filter(k => k.endsWith(\"_bonus\")).length;\n if (adjCount > 0 || bonCount > 0) {\n console.log(`[SCORING] Live verification: ${adjCount} caps, ${bonCount} bonuses applied (${verification.verification_time_ms}ms)`);\n }\n } catch (liveErr) {\n // Fix #3: Better error logging for live verification failures\n console.warn(`[SCORING] Live verification error (non-fatal): ${liveErr.message}`, liveErr.stack?.split('\\n').slice(0, 3).join(' | '));\n }\n\n // Fix: overall = average of ALL 10 dimensions (7 sections + novelty + reproducibility + citation_quality)\n // Previously only averaged the 7 sections, causing mismatch with displayed scores.\n const allDimensionValues = allFields.map(f => averaged[f]);\n let overall = Math.round((allDimensionValues.reduce((a, b) => a + b, 0) / allFields.length) * 10) / 10;\n\n // ── Phase F: Execution Proof Bonus ──\n // Papers with verified code blocks (execution hashes) get a purely additive overall bonus.\n // This only increases the score, never decreases. Capped at 10.\n if (liveVerification && liveVerification.bonuses && liveVerification.bonuses.execution_proof_bonus) {\n overall = Math.min(10, Math.round((overall + liveVerification.bonuses.execution_proof_bonus) * 10) / 10);\n console.log(`[SCORING] Execution proof bonus applied: +${liveVerification.bonuses.execution_proof_bonus} → overall=${overall}`);\n }\n\n // Fix #10: Fallback execution proof bonus — if live verification failed/timed out\n // but the paper contains verified execution hashes, still award the bonus.\n if (!liveVerification || !liveVerification.bonuses?.execution_proof_bonus) {\n const hashMatches = (content.match(/execution[_ ]hash[^`]*`([a-f0-9]{40,})`/gi) || []);\n if (hashMatches.length > 0) {\n const fallbackBonus = Math.min(1.5, hashMatches.length * 0.5);\n overall = Math.min(10, Math.round((overall + fallbackBonus) * 10) / 10);\n console.log(`[SCORING] Fallback execution proof bonus: ${hashMatches.length} hashes found in paper → +${fallbackBonus}`);\n if (!liveVerification) liveVerification = {};\n if (!liveVerification.bonuses) liveVerification.bonuses = {};\n liveVerification.bonuses.execution_proof_bonus = fallbackBonus;\n liveVerification.bonuses.execution_proof_note = `${hashMatches.length} execution hash(es) found in paper text (fallback detection)`;\n }\n }\n\n // Per-judge detail breakdown (individual scores + feedback)\n const judge_details = judges.map(j => ({\n judge: j.provider,\n scores: j.scores,\n feedback: j.feedback || null,\n }));\n\n // Consensus score per dimension (1 = all judges agree, 0 = total disagreement)\n const consensus = {};\n for (const field of allFields) {\n const values = judges.map(j => j.scores[field]).filter(v => typeof v === \"number\");\n if (values.length < 2) { consensus[field] = 1.0; continue; }\n const mean = values.reduce((a, b) => a + b, 0) / values.length;\n const variance = values.reduce((sum, v) => sum + (v - mean) ** 2, 0) / values.length;\n const stddev = Math.sqrt(variance);\n consensus[field] = Math.round(Math.max(0, 1 - stddev / 5) * 100) / 100;\n }\n const overall_consensus = Math.round(\n (allFields.reduce((sum, f) => sum + consensus[f], 0) / allFields.length) * 100\n ) / 100;\n\n // Aggregate feedback per dimension (from all judges that provided it)\n const aggregated_feedback = {};\n for (const field of allFields) {\n const comments = judges\n .filter(j => j.feedback && j.feedback[field])\n .map(j => ({ judge: j.provider, comment: j.feedback[field] }));\n if (comments.length > 0) aggregated_feedback[field] = comments;\n }\n\n const result = {\n sections: Object.fromEntries(SECTIONS.map(s => [s, averaged[s]])),\n overall,\n novelty: averaged.novelty,\n reproducibility: averaged.reproducibility,\n citation_quality: averaged.citation_quality,\n judges: judges.map(j => j.provider),\n judge_count: judges.length,\n judge_details,\n consensus,\n overall_consensus,\n feedback: Object.keys(aggregated_feedback).length > 0 ? aggregated_feedback : null,\n scored_at: new Date().toISOString(),\n paper_type: paperType,\n calibration,\n live_verification: liveVerification,\n };\n\n console.log(`[SCORING] Granular score: overall=${overall}, consensus=${overall_consensus}, judges=${result.judges.join(\",\")}`);\n return result;\n}\n"}, "ee089dc9955142bdd876a6d6d4420cf59624ec40": {"t": 1785753915.9602458, "n": 77817, "h": "011b4c8ae014b7a3", "c": "/**\n * P2PCLAW Granular Scoring Service — MAXIMUM TRIBUNAL v2\n * =======================================================\n * Heterogeneous multi-LLM scoring engine that evaluates papers section-by-section.\n * Updated 2026-05-07: Expanded to 104 independent LLM judges from 30+ providers.\n *\n * Provider chain (updated 2026-05-07):\n * 1-4. Cerebras — Qwen235B, Llama8B, GPT-OSS-120B, GLM-4.7 (+ key variants)\n * 5-7. Mistral — Small, Medium, Large, Nemo, CodeStral\n * 8-10. Sarvam — sarvam-m (Indian AI, + key variants)\n * 11-17. OpenRouter — Qwen3-Coder, Qwen3.6-Plus, Llama4, Gemma4, Mistral, DeepSeek, Nemotron, Llama3.3, GPT-OSS, GLM, Kimi (+ free variants)\n * 18-22. Groq — Llama3.3-70B, Llama4, Gemma2, Mixtral, Qwen2.5 (+ key variants)\n * 23-32. NVIDIA — Llama3.3, DeepSeekV3.2, StepFun3.5, GLM4.7, MistralLarge, CodeStral, Devstral, KimiThinking, MistralNemo, Phi4, Gemma4, Llama4 (+ key variants)\n * 33-34. Inception — Mercury-2 (+ key variant)\n * 35-38. Xiaomi MiMo — Flash, Pro (+ key variants)\n * 39-42. Cohere — CommandA-Reasoning, CommandA, R7B, Aya (+ key variants)\n * 43-57. Cloudflare — 15 accounts x GLM4, Gemma4, Nemotron, Kimi, GPT-OSS, Qwen3, Llama4Scout, MistralSmall31, DeepSeekR1\n * 58-64. Together AI — Llama4, Qwen3.6, DeepSeekV3, MistralLarge, Gemma4, Llama3.3, Qwen2.5, Nemotron, Phi4 (+ key variants)\n * 65-71. HuggingFace — Qwen3.6, Llama4, MistralSmall, Gemma4, DeepSeekV3, GLM4.7, Nemotron, Phi4, CommandR7B, Aya (+ key variants)\n * 72-73. Google Gemini — 2.5 Pro, 2.5 Flash (+ key variants)\n * 74-76. DeepSeek — Chat, Reasoner (+ key variants)\n * 77-80. Z.ai — GLM-4.7, GLM-5 (+ key variants)\n * 81-82. Fireworks — Nemotron3, Llama4\n * 83. Arcee AI — Trinity-Mini\n * 84. Minimax — Text-01\n * 85-86. Kilo AI — Kilo-1, Kilo-2\n * 87-104. Key variants across all providers (independent judges with rotated keys)\n *\n * TOTAL: 104 independent LLM judges + 1 heuristic = 105 scoring perspectives\n * ALL available judges score independently. Final score = average across all judges.\n * Each model evaluates each section independently for maximum consensus diversity.\n */\n\nimport { detectField, extractSignals, calibrateScores, REFERENCE_BENCHMARKS } from \"./calibrationService.js\";\nimport { runLiveVerification, verificationToAdjustments } from \"./liveVerificationService.js\";\n\nconst SECTIONS = [\"abstract\", \"introduction\", \"methodology\", \"results\", \"discussion\", \"conclusion\", \"references\"];\n\n// ── Load keys with rotation ─────────────────────────────────────────────────\n\nfunction loadKeys(envPrefix, maxKeys = 10) {\n const keys = [];\n for (let i = 1; i <= maxKeys; i++) {\n const k = process.env[`${envPrefix}_${i}`] || process.env[`${envPrefix}${i}`];\n if (k) keys.push(k);\n }\n // Also check single-key env vars\n const single = process.env[envPrefix];\n if (single && !keys.includes(single)) keys.unshift(single);\n return keys;\n}\n\nconst keyIndices = {};\nfunction nextKey(providerId, keys) {\n if (!keys.length) return null;\n const idx = keyIndices[providerId] || 0;\n const key = keys[idx % keys.length];\n keyIndices[providerId] = (idx + 1) % keys.length;\n return key;\n}\n\n// ── Provider definitions ────────────────────────────────────────────────────\n\nconst PROVIDERS = [\n // --- Cerebras: 3 keys x 2 models = up to 6 independent judges ---\n {\n id: \"cerebras-qwen\",\n name: \"Cerebras-Qwen235B\",\n url: \"https://api.cerebras.ai/v1/chat/completions\",\n model: \"qwen-3-235b-a22b-instruct-2507\",\n keys: loadKeys(\"CEREBRAS_API_KEY\", 15).concat(loadKeys(\"CEREBRAS_KEY\", 15)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 1024,\n },\n {\n id: \"cerebras-llama\",\n name: \"Cerebras-Llama8B\",\n url: \"https://api.cerebras.ai/v1/chat/completions\",\n model: \"llama3.1-8b\",\n keys: loadKeys(\"CEREBRAS_API_KEY\", 15).concat(loadKeys(\"CEREBRAS_KEY\", 15)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n // --- Cerebras: GPT-OSS-120B (120B param open-source model, different perspective) ---\n {\n id: \"cerebras-gptoss\",\n name: \"Cerebras-GPT-OSS-120B\",\n url: \"https://api.cerebras.ai/v1/chat/completions\",\n model: \"gpt-oss-120b\",\n keys: loadKeys(\"CEREBRAS_API_KEY\", 15).concat(loadKeys(\"CEREBRAS_KEY\", 15)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n // --- Cerebras: ZAI GLM-4.7 (Chinese model — different cultural perspective on academic rigor) ---\n {\n id: \"cerebras-glm47\",\n name: \"Cerebras-GLM4.7\",\n url: \"https://api.cerebras.ai/v1/chat/completions\",\n model: \"zai-glm-4.7\",\n keys: loadKeys(\"CEREBRAS_API_KEY\", 15).concat(loadKeys(\"CEREBRAS_KEY\", 15)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 1024,\n },\n // --- Mistral: 3 keys, reliable ---\n {\n id: \"mistral\",\n name: \"Mistral\",\n url: \"https://api.mistral.ai/v1/chat/completions\",\n model: \"mistral-small-latest\",\n keys: loadKeys(\"MISTRAL_API_KEY\", 10).concat(loadKeys(\"MISTRAL_KEY\", 10)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n // --- Sarvam (Indian AI): sarvam-m, uses <think> tags (needs 2048+ tokens), 13 keys ---\n {\n id: \"sarvam\",\n name: \"Sarvam\",\n url: \"https://api.sarvam.ai/v1/chat/completions\",\n model: \"sarvam-m\",\n keys: loadKeys(\"SARVAM_KEY\", 15).concat(loadKeys(\"SARVAM_API_KEY\", 15)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 2048,\n },\n // --- OpenRouter: free models ---\n {\n id: \"openrouter\",\n name: \"OpenRouter\",\n url: \"https://openrouter.ai/api/v1/chat/completions\",\n model: \"qwen/qwen3-coder:free\",\n keys: loadKeys(\"OPENROUTER_API_KEY\"),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n extraHeaders: { \"HTTP-Referer\": \"https://www.p2pclaw.com\", \"X-Title\": \"P2PCLAW Scoring\" },\n },\n // --- Groq: 9 keys but may be org-restricted ---\n {\n id: \"groq\",\n name: \"Groq\",\n url: \"https://api.groq.com/openai/v1/chat/completions\",\n model: \"llama-3.3-70b-versatile\",\n keys: loadKeys(\"GROQ_API_KEY\", 15).concat(loadKeys(\"GROQ_KEY\", 15)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n // --- NVIDIA: 3 keys ---\n {\n id: \"nvidia\",\n name: \"NVIDIA\",\n url: \"https://integrate.api.nvidia.com/v1/chat/completions\",\n model: \"meta/llama-3.3-70b-instruct\",\n keys: loadKeys(\"NVAPI_KEY\", 10).concat(loadKeys(\"NVIDIA_API_KEY\", 10)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n timeout: 45000,\n },\n // --- Inception: mercury-2 (diffusion-based LLM — unique scoring perspective) ---\n {\n id: \"inception\",\n name: \"Inception-Mercury2\",\n url: \"https://api.inceptionlabs.ai/v1/chat/completions\",\n model: \"mercury-2\",\n keys: loadKeys(\"INCEPTION_API_KEY\", 15).concat(loadKeys(\"INCEPTION_KEY\", 15)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 1024, // Mercury-2 needs at least 50 tokens (reasoning model)\n timeout: 45000,\n },\n // --- Xiaomi MiMo: 5 keys x 2 models = up to 2 independent judges ---\n {\n id: \"xiaomi-flash\",\n name: \"Xiaomi-MiMo-Flash\",\n url: \"https://api.xiaomimimo.com/v1/chat/completions\",\n model: \"mimo-v2-flash\",\n keys: loadKeys(\"XIAOMI_API_KEY\", 10).concat(loadKeys(\"XIAOMI_KEY\", 10)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n {\n id: \"xiaomi-pro\",\n name: \"Xiaomi-MiMo-Pro\",\n url: \"https://api.xiaomimimo.com/v1/chat/completions\",\n model: \"mimo-v2-pro\",\n keys: loadKeys(\"XIAOMI_API_KEY\", 10).concat(loadKeys(\"XIAOMI_KEY\", 10)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 1024,\n },\n // --- Cohere: command-a-reasoning model (9 keys, reasoning/thinking model) ---\n {\n id: \"cohere\",\n name: \"Cohere-CommandA\",\n url: \"https://api.cohere.com/v2/chat\",\n model: \"command-a-reasoning-08-2025\",\n keys: loadKeys(\"COHERE_API_KEY\", 15).concat(loadKeys(\"COHERE_KEY\", 15)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 4096, // reasoning model needs extra tokens for thinking + answer\n responseFormat: \"cohere\", // data.message.content[] array with {type:\"thinking\"} + {type:\"text\"}\n timeout: 90000, // 90s — reasoning model needs time to think through all 10 dimensions\n },\n // --- Cloudflare Workers AI: 6 models across 6 accounts (all FREE) ---\n // Ordered by model quality: best first\n {\n id: \"cloudflare-glm\",\n name: \"Cloudflare-GLM4\",\n url: `https://api.cloudflare.com/client/v4/accounts/eaffd2b52c95c69aaad8d859e9dcb52b/ai/run/@cf/zai-org/glm-4.7-flash`,\n model: \"@cf/zai-org/glm-4.7-flash\",\n keys: loadKeys(\"CF_AI_TOKEN\").concat(loadKeys(\"CLOUDFLARE_AI_TOKEN\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 2048,\n responseFormat: \"cloudflare\",\n timeout: 60000,\n },\n {\n id: \"cloudflare-gemma4\",\n name: \"Cloudflare-Gemma4\",\n url: `https://api.cloudflare.com/client/v4/accounts/a7995d3f33b6ba57955749337c9abbe0/ai/run/@cf/google/gemma-4-26b-a4b-it`,\n model: \"@cf/google/gemma-4-26b-a4b-it\",\n keys: loadKeys(\"CF_AI_TOKEN_2\"),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 1024,\n responseFormat: \"cloudflare\",\n timeout: 60000,\n },\n {\n id: \"cloudflare-nemotron\",\n name: \"Cloudflare-Nemotron\",\n url: `https://api.cloudflare.com/client/v4/accounts/194d9aea21482ac893ed81fc6b004864/ai/run/@cf/nvidia/nemotron-3-120b-a12b`,\n model: \"@cf/nvidia/nemotron-3-120b-a12b\",\n keys: loadKeys(\"CF_AI_TOKEN_3\"),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 1024,\n responseFormat: \"cloudflare\",\n timeout: 60000,\n },\n {\n id: \"cloudflare-kimi\",\n name: \"Cloudflare-Kimi\",\n url: `https://api.cloudflare.com/client/v4/accounts/401a75ead25275262c1c05eecb7a997c/ai/run/@cf/moonshotai/kimi-k2.5`,\n model: \"@cf/moonshotai/kimi-k2.5\",\n keys: loadKeys(\"CF_AI_TOKEN_4\"),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 2048,\n responseFormat: \"cloudflare\",\n timeout: 60000,\n },\n {\n id: \"cloudflare-gptoss\",\n name: \"Cloudflare-GPT-OSS\",\n url: `https://api.cloudflare.com/client/v4/accounts/73340519f6430362daee759ba0b48ce8/ai/run/@cf/openai/gpt-oss-120b`,\n model: \"@cf/openai/gpt-oss-120b\",\n keys: loadKeys(\"CF_AI_TOKEN_5\"),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 1024,\n responseFormat: \"cloudflare\",\n timeout: 60000,\n },\n {\n id: \"cloudflare-qwen3\",\n name: \"Cloudflare-Qwen3\",\n url: `https://api.cloudflare.com/client/v4/accounts/df4a7888befcbb6ce3e0a0b346ea1990/ai/run/@cf/qwen/qwen3-30b-a3b-fp8`,\n model: \"@cf/qwen/qwen3-30b-a3b-fp8\",\n keys: loadKeys(\"CF_AI_TOKEN_6\"),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 2048,\n responseFormat: \"cloudflare\",\n timeout: 60000,\n },\n // --- Cloudflare Workers AI: 3 NEW accounts (accounts 7/8/9) ---\n {\n id: \"cloudflare-llama4scout\",\n name: \"Cloudflare-Llama4Scout\",\n url: `https://api.cloudflare.com/client/v4/accounts/3cd084561890e5ab468456fae547ded0/ai/run/@cf/meta/llama-4-scout-17b-16e-instruct`,\n model: \"@cf/meta/llama-4-scout-17b-16e-instruct\",\n keys: loadKeys(\"CF_AI_TOKEN_7\"),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 1024,\n responseFormat: \"cloudflare\",\n timeout: 60000,\n },\n {\n id: \"cloudflare-mistral31\",\n name: \"Cloudflare-MistralSmall31\",\n url: `https://api.cloudflare.com/client/v4/accounts/27920eccf7d83f7ee267130cd6018eaf/ai/run/@cf/mistralai/mistral-small-3.1-24b-instruct`,\n model: \"@cf/mistralai/mistral-small-3.1-24b-instruct\",\n keys: loadKeys(\"CF_AI_TOKEN_8\"),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 1024,\n responseFormat: \"cloudflare\",\n timeout: 60000,\n },\n {\n id: \"cloudflare-deepseekr1\",\n name: \"Cloudflare-DeepSeekR1\",\n url: `https://api.cloudflare.com/client/v4/accounts/60c2dcaa7fc3377f036114648f6397ba/ai/run/@cf/deepseek-ai/deepseek-r1-distill-qwen-32b`,\n model: \"@cf/deepseek-ai/deepseek-r1-distill-qwen-32b\",\n keys: loadKeys(\"CF_AI_TOKEN_9\"),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 2048,\n responseFormat: \"cloudflare\",\n timeout: 60000,\n },\n // --- Cloudflare Workers AI: Account 10 (Agnuxo2026, GLM-4.7-flash) ---\n {\n id: \"cloudflare-glm47-10\",\n name: \"Cloudflare-GLM47-Acct10\",\n url: `https://api.cloudflare.com/client/v4/accounts/${process.env.CF_ACCOUNT_ID_10 || \"00c5ebf4df46d16450d5f1419dc36c6a\"}/ai/run/@cf/zai-org/glm-4.7-flash`,\n model: \"@cf/zai-org/glm-4.7-flash\",\n keys: loadKeys(\"CF_AI_TOKEN_10\"),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 2048,\n responseFormat: \"cloudflare\",\n timeout: 60000,\n },\n // --- Cloudflare Workers AI: Account 11 (agnuxo300@zohomail.eu, Gemma-4-26b) ---\n {\n id: \"cloudflare-gemma4-11\",\n name: \"Cloudflare-Gemma4-Acct11\",\n url: `https://api.cloudflare.com/client/v4/accounts/${process.env.CF_ACCOUNT_ID_11 || \"85fbdab1851209cbd99773a758831fc0\"}/ai/run/@cf/google/gemma-4-26b-a4b-it`,\n model: \"@cf/google/gemma-4-26b-a4b-it\",\n keys: loadKeys(\"CF_AI_TOKEN_11\"),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 1024,\n responseFormat: \"cloudflare\",\n timeout: 60000,\n },\n // --- Cloudflare account 12: Mistral Small 3.1 24B ---\n {\n id: \"cloudflare-mistral31-12\",\n name: \"Cloudflare-MistralSmall31-Acct12\",\n url: `https://api.cloudflare.com/client/v4/accounts/${process.env.CF_ACCOUNT_ID_12 || \"ccd856bec1f7fb8e9745f21e9bd742f4\"}/ai/run/@cf/mistralai/mistral-small-3.1-24b-instruct`,\n model: \"@cf/mistralai/mistral-small-3.1-24b-instruct\",\n keys: loadKeys(\"CF_AI_TOKEN_12\"),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 1024,\n responseFormat: \"cloudflare\",\n timeout: 60000,\n },\n // --- OpenRouter: Qwen 3.6 Plus (free, large reasoning model) ---\n {\n id: \"openrouter-qwen36plus\",\n name: \"OpenRouter-Qwen3.6Plus\",\n url: \"https://openrouter.ai/api/v1/chat/completions\",\n model: \"qwen/qwen3.6-plus:free\",\n keys: loadKeys(\"OPENROUTER_API_KEY\", 15),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 2048,\n timeout: 60000,\n },\n // --- NVIDIA: DeepSeek-V3.2 (reasoning model with thinking) ---\n {\n id: \"nvidia-deepseek-v3\",\n name: \"NVIDIA-DeepSeekV3.2\",\n url: \"https://integrate.api.nvidia.com/v1/chat/completions\",\n model: \"deepseek-ai/deepseek-v3.2\",\n keys: loadKeys(\"NVAPI_KEY\", 10).concat(loadKeys(\"NVIDIA_API_KEY\", 10)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 2048,\n timeout: 60000,\n },\n // --- NVIDIA: StepFun Step-3.5-Flash (Chinese reasoning model) ---\n {\n id: \"nvidia-stepfun\",\n name: \"NVIDIA-StepFun3.5\",\n url: \"https://integrate.api.nvidia.com/v1/chat/completions\",\n model: \"stepfun-ai/step-3.5-flash\",\n keys: loadKeys(\"NVAPI_KEY\", 10).concat(loadKeys(\"NVIDIA_API_KEY\", 10)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 2048,\n timeout: 60000,\n },\n // --- NVIDIA: GLM-4.7 (Z.ai Chinese model with thinking) ---\n {\n id: \"nvidia-glm47\",\n name: \"NVIDIA-GLM4.7\",\n url: \"https://integrate.api.nvidia.com/v1/chat/completions\",\n model: \"z-ai/glm4.7\",\n keys: loadKeys(\"NVAPI_KEY\", 10).concat(loadKeys(\"NVIDIA_API_KEY\", 10)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 2048,\n timeout: 60000,\n },\n\n // ═══════════════════════════════════════════════════════════════════════\n // NEW JUDGES BATCH 2026-05-07 — Expansion to 100+ independent LLM judges\n // Using all APIs from the credentials document\n // ═══════════════════════════════════════════════════════════════════════\n\n // --- Together AI: 7 keys, 5 models (free tier) ---\n {\n id: \"together-llama4\",\n name: \"Together-Llama4-Scout\",\n url: \"https://api.together.xyz/v1/chat/completions\",\n model: \"meta-llama/Llama-4-Scout-17B-16E-Instruct\",\n keys: loadKeys(\"TOGETHER_API_KEY\", 10).concat(loadKeys(\"TOGETHER_KEY\", 10)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 2048,\n timeout: 60000,\n },\n {\n id: \"together-qwen36\",\n name: \"Together-Qwen3.6-72B\",\n url: \"https://api.together.xyz/v1/chat/completions\",\n model: \"Qwen/Qwen3.6-72B\",\n keys: loadKeys(\"TOGETHER_API_KEY\", 10).concat(loadKeys(\"TOGETHER_KEY\", 10)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 2048,\n timeout: 60000,\n },\n {\n id: \"together-deepseek\",\n name: \"Together-DeepSeek-V3\",\n url: \"https://api.together.xyz/v1/chat/completions\",\n model: \"deepseek-ai/DeepSeek-V3\",\n keys: loadKeys(\"TOGETHER_API_KEY\", 10).concat(loadKeys(\"TOGETHER_KEY\", 10)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 2048,\n timeout: 60000,\n },\n {\n id: \"together-mistral\",\n name: \"Together-Mistral-Large\",\n url: \"https://api.together.xyz/v1/chat/completions\",\n model: \"mistralai/Mistral-Large-Instruct-2411\",\n keys: loadKeys(\"TOGETHER_API_KEY\", 10).concat(loadKeys(\"TOGETHER_KEY\", 10)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 2048,\n timeout: 60000,\n },\n {\n id: \"together-gemma\",\n name: \"Together-Gemma-4-27B\",\n url: \"https://api.together.xyz/v1/chat/completions\",\n model: \"google/gemma-4-27b-it\",\n keys: loadKeys(\"TOGETHER_API_KEY\", 10).concat(loadKeys(\"TOGETHER_KEY\", 10)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 2048,\n timeout: 60000,\n },\n {\n id: \"together-llama33\",\n name: \"Together-Llama-3.3-70B\",\n url: \"https://api.together.xyz/v1/chat/completions\",\n model: \"meta-llama/Llama-3.3-70B-Instruct-Turbo\",\n keys: loadKeys(\"TOGETHER_API_KEY\", 10).concat(loadKeys(\"TOGETHER_KEY\", 10)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 2048,\n timeout: 60000,\n },\n {\n id: \"together-qwen25\",\n name: \"Together-Qwen-2.5-32B\",\n url: \"https://api.together.xyz/v1/chat/completions\",\n model: \"Qwen/Qwen2.5-32B-Instruct\",\n keys: loadKeys(\"TOGETHER_API_KEY\", 10).concat(loadKeys(\"TOGETHER_KEY\", 10)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 2048,\n timeout: 60000,\n },\n\n // --- Google Gemini: 7 keys, 2 models ---\n {\n id: \"gemini-pro\",\n name: \"Gemini-2.5-Pro\",\n url: \"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-pro-preview-05-06:generateContent\",\n model: \"gemini-2.5-pro-preview-05-06\",\n keys: loadKeys(\"GEMINI_API_KEY\", 10).concat(loadKeys(\"GEMINI_KEY\", 10)),\n authHeader: \"x-goog-api-key\",\n authPrefix: \"\",\n stripThinkTags: true,\n maxTokens: 2048,\n responseFormat: \"gemini\",\n timeout: 60000,\n },\n {\n id: \"gemini-flash\",\n name: \"Gemini-2.5-Flash\",\n url: \"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-preview-05-06:generateContent\",\n model: \"gemini-2.5-flash-preview-05-06\",\n keys: loadKeys(\"GEMINI_API_KEY\", 10).concat(loadKeys(\"GEMINI_KEY\", 10)),\n authHeader: \"x-goog-api-key\",\n authPrefix: \"\",\n stripThinkTags: true,\n maxTokens: 2048,\n responseFormat: \"gemini\",\n timeout: 60000,\n },\n\n // --- DeepSeek: 7 keys, 2 models ---\n {\n id: \"deepseek-chat\",\n name: \"DeepSeek-Chat\",\n url: \"https://api.deepseek.com/v1/chat/completions\",\n model: \"deepseek-chat\",\n keys: loadKeys(\"DEEPSEEK_API_KEY\", 10).concat(loadKeys(\"DEEPSEEK_KEY\", 10)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 2048,\n timeout: 60000,\n },\n {\n id: \"deepseek-reasoner\",\n name: \"DeepSeek-Reasoner\",\n url: \"https://api.deepseek.com/v1/chat/completions\",\n model: \"deepseek-reasoner\",\n keys: loadKeys(\"DEEPSEEK_API_KEY\", 10).concat(loadKeys(\"DEEPSEEK_KEY\", 10)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 4096,\n timeout: 90000,\n },\n\n // --- Z.ai (GLM): 8 keys, 2 models ---\n {\n id: \"zai-glm47\",\n name: \"Z.ai-GLM-4.7\",\n url: \"https://api.z.ai/v1/chat/completions\",\n model: \"glm-4.7\",\n keys: loadKeys(\"ZAI_API_KEY\", 10).concat(loadKeys(\"ZAI_KEY\", 10)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 2048,\n timeout: 60000,\n },\n {\n id: \"zai-glm5\",\n name: \"Z.ai-GLM-5\",\n url: \"https://api.z.ai/v1/chat/completions\",\n model: \"glm-5\",\n keys: loadKeys(\"ZAI_API_KEY\", 10).concat(loadKeys(\"ZAI_KEY\", 10)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 2048,\n timeout: 60000,\n },\n\n // --- HuggingFace Inference API: 7 models (free tier) ---\n {\n id: \"hf-qwen36\",\n name: \"HF-Qwen3.6-72B\",\n url: \"https://api-inference.huggingface.co/models/Qwen/Qwen3.6-72B-Instruct/v1/chat/completions\",\n model: \"Qwen/Qwen3.6-72B-Instruct\",\n keys: loadKeys(\"HF_TOKEN\", 5).concat(loadKeys(\"HUGGINGFACE_TOKEN\", 5)).concat(loadKeys(\"HUGGINGFACE_API_KEY\", 5)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 2048,\n timeout: 90000,\n },\n {\n id: \"hf-llama4\",\n name: \"HF-Llama-4-Scout\",\n url: \"https://api-inference.huggingface.co/models/meta-llama/Llama-4-Scout-17B-16E-Instruct/v1/chat/completions\",\n model: \"meta-llama/Llama-4-Scout-17B-16E-Instruct\",\n keys: loadKeys(\"HF_TOKEN\", 5).concat(loadKeys(\"HUGGINGFACE_TOKEN\", 5)).concat(loadKeys(\"HUGGINGFACE_API_KEY\", 5)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 2048,\n timeout: 90000,\n },\n {\n id: \"hf-mistral\",\n name: \"HF-Mistral-Small-24B\",\n url: \"https://api-inference.huggingface.co/models/mistralai/Mistral-Small-24B-Instruct-2501/v1/chat/completions\",\n model: \"mistralai/Mistral-Small-24B-Instruct-2501\",\n keys: loadKeys(\"HF_TOKEN\", 5).concat(loadKeys(\"HUGGINGFACE_TOKEN\", 5)).concat(loadKeys(\"HUGGINGFACE_API_KEY\", 5)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 2048,\n timeout: 90000,\n },\n {\n id: \"hf-gemma\",\n name: \"HF-Gemma-4-27B\",\n url: \"https://api-inference.huggingface.co/models/google/gemma-4-27b-it/v1/chat/completions\",\n model: \"google/gemma-4-27b-it\",\n keys: loadKeys(\"HF_TOKEN\", 5).concat(loadKeys(\"HUGGINGFACE_TOKEN\", 5)).concat(loadKeys(\"HUGGINGFACE_API_KEY\", 5)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 2048,\n timeout: 90000,\n },\n {\n id: \"hf-deepseek\",\n name: \"HF-DeepSeek-V3\",\n url: \"https://api-inference.huggingface.co/models/deepseek-ai/DeepSeek-V3-0324/v1/chat/completions\",\n model: \"deepseek-ai/DeepSeek-V3-0324\",\n keys: loadKeys(\"HF_TOKEN\", 5).concat(loadKeys(\"HUGGINGFACE_TOKEN\", 5)).concat(loadKeys(\"HUGGINGFACE_API_KEY\", 5)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 2048,\n timeout: 90000,\n },\n {\n id: \"hf-glm47\",\n name: \"HF-GLM-4.7\",\n url: \"https://api-inference.huggingface.co/models/zai-org/GLM-4.7-0414/v1/chat/completions\",\n model: \"zai-org/GLM-4.7-0414\",\n keys: loadKeys(\"HF_TOKEN\", 5).concat(loadKeys(\"HUGGINGFACE_TOKEN\", 5)).concat(loadKeys(\"HUGGINGFACE_API_KEY\", 5)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 2048,\n timeout: 90000,\n },\n {\n id: \"hf-nemotron\",\n name: \"HF-Nemotron-3-120B\",\n url: \"https://api-inference.huggingface.co/models/nvidia/Nemotron-3-120B/v1/chat/completions\",\n model: \"nvidia/Nemotron-3-120B\",\n keys: loadKeys(\"HF_TOKEN\", 5).concat(loadKeys(\"HUGGINGFACE_TOKEN\", 5)).concat(loadKeys(\"HUGGINGFACE_API_KEY\", 5)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 2048,\n timeout: 90000,\n },\n\n // --- OpenRouter: additional free models ---\n {\n id: \"openrouter-llama4\",\n name: \"OpenRouter-Llama4-Scout-Free\",\n url: \"https://openrouter.ai/api/v1/chat/completions\",\n model: \"meta-llama/llama-4-scout:free\",\n keys: loadKeys(\"OPENROUTER_API_KEY\", 15).concat(loadKeys(\"OPENROUTER_KEY\", 15)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n extraHeaders: { \"HTTP-Referer\": \"https://www.p2pclaw.com\", \"X-Title\": \"P2PCLAW Scoring\" },\n maxTokens: 2048,\n timeout: 60000,\n },\n {\n id: \"openrouter-gemma\",\n name: \"OpenRouter-Gemma-4-Free\",\n url: \"https://openrouter.ai/api/v1/chat/completions\",\n model: \"google/gemma-4-27b-it:free\",\n keys: loadKeys(\"OPENROUTER_API_KEY\", 15).concat(loadKeys(\"OPENROUTER_KEY\", 15)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n extraHeaders: { \"HTTP-Referer\": \"https://www.p2pclaw.com\", \"X-Title\": \"P2PCLAW Scoring\" },\n maxTokens: 2048,\n timeout: 60000,\n },\n {\n id: \"openrouter-mistral-free\",\n name: \"OpenRouter-Mistral-Small-Free\",\n url: \"https://openrouter.ai/api/v1/chat/completions\",\n model: \"mistralai/mistral-small-3.1-24b-instruct:free\",\n keys: loadKeys(\"OPENROUTER_API_KEY\", 15).concat(loadKeys(\"OPENROUTER_KEY\", 15)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n extraHeaders: { \"HTTP-Referer\": \"https://www.p2pclaw.com\", \"X-Title\": \"P2PCLAW Scoring\" },\n maxTokens: 2048,\n timeout: 60000,\n },\n {\n id: \"openrouter-deepseek\",\n name: \"OpenRouter-DeepSeek-V3-Free\",\n url: \"https://openrouter.ai/api/v1/chat/completions\",\n model: \"deepseek/deepseek-chat:free\",\n keys: loadKeys(\"OPENROUTER_API_KEY\", 15).concat(loadKeys(\"OPENROUTER_KEY\", 15)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n extraHeaders: { \"HTTP-Referer\": \"https://www.p2pclaw.com\", \"X-Title\": \"P2PCLAW Scoring\" },\n stripThinkTags: true,\n maxTokens: 2048,\n timeout: 60000,\n },\n {\n id: \"openrouter-nemotron\",\n name: \"OpenRouter-Nemotron-Free\",\n url: \"https://openrouter.ai/api/v1/chat/completions\",\n model: \"nvidia/nemotron-3-120b:free\",\n keys: loadKeys(\"OPENROUTER_API_KEY\", 15).concat(loadKeys(\"OPENROUTER_KEY\", 15)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n extraHeaders: { \"HTTP-Referer\": \"https://www.p2pclaw.com\", \"X-Title\": \"P2PCLAW Scoring\" },\n maxTokens: 2048,\n timeout: 60000,\n },\n {\n id: \"openrouter-llama33\",\n name: \"OpenRouter-Llama-3.3-Free\",\n url: \"https://openrouter.ai/api/v1/chat/completions\",\n model: \"meta-llama/llama-3.3-70b-instruct:free\",\n keys: loadKeys(\"OPENROUTER_API_KEY\", 15).concat(loadKeys(\"OPENROUTER_KEY\", 15)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n extraHeaders: { \"HTTP-Referer\": \"https://www.p2pclaw.com\", \"X-Title\": \"P2PCLAW Scoring\" },\n maxTokens: 2048,\n timeout: 60000,\n },\n\n // --- NVIDIA: additional models ---\n {\n id: \"nvidia-mistral-large\",\n name: \"NVIDIA-Mistral-Large-3\",\n url: \"https://integrate.api.nvidia.com/v1/chat/completions\",\n model: \"mistralai/mistral-large-3-675b-instruct-2512\",\n keys: loadKeys(\"NVAPI_KEY\", 10).concat(loadKeys(\"NVIDIA_API_KEY\", 10)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 2048,\n timeout: 60000,\n },\n {\n id: \"nvidia-codestral\",\n name: \"NVIDIA-CodeStral\",\n url: \"https://integrate.api.nvidia.com/v1/chat/completions\",\n model: \"mistralai/codestral-2508\",\n keys: loadKeys(\"NVAPI_KEY\", 10).concat(loadKeys(\"NVIDIA_API_KEY\", 10)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 2048,\n timeout: 60000,\n },\n {\n id: \"nvidia-devstral\",\n name: \"NVIDIA-Devstral\",\n url: \"https://integrate.api.nvidia.com/v1/chat/completions\",\n model: \"mistralai/devstral-2-123b-instruct-2512\",\n keys: loadKeys(\"NVAPI_KEY\", 10).concat(loadKeys(\"NVIDIA_API_KEY\", 10)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 2048,\n timeout: 60000,\n },\n {\n id: \"nvidia-kimi-thinking\",\n name: \"NVIDIA-Kimi-K2-Thinking\",\n url: \"https://integrate.api.nvidia.com/v1/chat/completions\",\n model: \"moonshotai/kimi-k2-thinking\",\n keys: loadKeys(\"NVAPI_KEY\", 10).concat(loadKeys(\"NVIDIA_API_KEY\", 10)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 4096,\n timeout: 90000,\n },\n {\n id: \"nvidia-mistral-nemo\",\n name: \"NVIDIA-Mistral-Nemo\",\n url: \"https://integrate.api.nvidia.com/v1/chat/completions\",\n model: \"mistralai/mistral-nemo-instruct-2407\",\n keys: loadKeys(\"NVAPI_KEY\", 10).concat(loadKeys(\"NVIDIA_API_KEY\", 10)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 2048,\n timeout: 60000,\n },\n\n // --- Groq: additional models ---\n {\n id: \"groq-gemma2\",\n name: \"Groq-Gemma-2-9B\",\n url: \"https://api.groq.com/openai/v1/chat/completions\",\n model: \"gemma2-9b-it\",\n keys: loadKeys(\"GROQ_API_KEY\", 15).concat(loadKeys(\"GROQ_KEY\", 15)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 2048,\n timeout: 30000,\n },\n {\n id: \"groq-mixtral\",\n name: \"Groq-Mixtral-8x7B\",\n url: \"https://api.groq.com/openai/v1/chat/completions\",\n model: \"mixtral-8x7b-32768\",\n keys: loadKeys(\"GROQ_API_KEY\", 15).concat(loadKeys(\"GROQ_KEY\", 15)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 2048,\n timeout: 30000,\n },\n {\n id: \"groq-qwen25\",\n name: \"Groq-Qwen-2.5-32B\",\n url: \"https://api.groq.com/openai/v1/chat/completions\",\n model: \"qwen-2.5-32b\",\n keys: loadKeys(\"GROQ_API_KEY\", 15).concat(loadKeys(\"GROQ_KEY\", 15)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 2048,\n timeout: 30000,\n },\n {\n id: \"groq-llama4\",\n name: \"Groq-Llama-4-Scout\",\n url: \"https://api.groq.com/openai/v1/chat/completions\",\n model: \"meta-llama/llama-4-scout-17b-16e-instruct\",\n keys: loadKeys(\"GROQ_API_KEY\", 15).concat(loadKeys(\"GROQ_KEY\", 15)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 2048,\n timeout: 30000,\n },\n\n // --- Cohere: additional model ---\n {\n id: \"cohere-command\",\n name: \"Cohere-Command-A\",\n url: \"https://api.cohere.com/v2/chat\",\n model: \"command-a-03-2025\",\n keys: loadKeys(\"COHERE_API_KEY\", 15).concat(loadKeys(\"COHERE_KEY\", 15)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 4096,\n responseFormat: \"cohere\",\n timeout: 90000,\n },\n {\n id: \"cohere-r7b\",\n name: \"Cohere-R7B\",\n url: \"https://api.cohere.com/v2/chat\",\n model: \"command-r7b-12-2024\",\n keys: loadKeys(\"COHERE_API_KEY\", 15).concat(loadKeys(\"COHERE_KEY\", 15)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 4096,\n responseFormat: \"cohere\",\n timeout: 90000,\n },\n\n // --- Mistral: additional models ---\n {\n id: \"mistral-medium\",\n name: \"Mistral-Medium\",\n url: \"https://api.mistral.ai/v1/chat/completions\",\n model: \"mistral-medium-latest\",\n keys: loadKeys(\"MISTRAL_API_KEY\", 10).concat(loadKeys(\"MISTRAL_KEY\", 10)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 2048,\n timeout: 45000,\n },\n {\n id: \"mistral-large\",\n name: \"Mistral-Large\",\n url: \"https://api.mistral.ai/v1/chat/completions\",\n model: \"mistral-large-latest\",\n keys: loadKeys(\"MISTRAL_API_KEY\", 10).concat(loadKeys(\"MISTRAL_KEY\", 10)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 2048,\n timeout: 45000,\n },\n {\n id: \"mistral-nemo\",\n name: \"Mistral-Nemo\",\n url: \"https://api.mistral.ai/v1/chat/completions\",\n model: \"mistral-nemo\",\n keys: loadKeys(\"MISTRAL_API_KEY\", 10).concat(loadKeys(\"MISTRAL_KEY\", 10)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 2048,\n timeout: 45000,\n },\n\n // --- Fireworks: 1 key ---\n {\n id: \"fireworks-nemotron\",\n name: \"Fireworks-Nemotron-3\",\n url: \"https://api.fireworks.ai/inference/v1/chat/completions\",\n model: \"accounts/fireworks/models/nvidia-nemotron-3-super-120b-a12b-fp8\",\n keys: loadKeys(\"FIREWORKS_API_KEY\", 5).concat(loadKeys(\"FIREWORKS_KEY\", 5)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 2048,\n timeout: 60000,\n },\n {\n id: \"fireworks-llama4\",\n name: \"Fireworks-Llama-4-Scout\",\n url: \"https://api.fireworks.ai/inference/v1/chat/completions\",\n model: \"accounts/fireworks/models/llama-4-scout-instruct\",\n keys: loadKeys(\"FIREWORKS_API_KEY\", 5).concat(loadKeys(\"FIREWORKS_KEY\", 5)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 2048,\n timeout: 60000,\n },\n\n // --- Arcee AI: 1 key ---\n {\n id: \"arcee-trinity\",\n name: \"Arcee-Trinity-Mini\",\n url: \"https://api.arcee.ai/v1/chat/completions\",\n model: \"trinity-mini\",\n keys: loadKeys(\"ARCEE_API_KEY\", 5).concat(loadKeys(\"ARCEE_KEY\", 5)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 2048,\n timeout: 60000,\n },\n\n // --- Minimax: 3 keys ---\n {\n id: \"minimax-text\",\n name: \"Minimax-Text-01\",\n url: \"https://api.minimax.chat/v1/text/chatcompletion_v2\",\n model: \"MiniMax-Text-01\",\n keys: loadKeys(\"MINIMAX_API_KEY\", 5).concat(loadKeys(\"MINIMAX_KEY\", 5)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 2048,\n timeout: 60000,\n },\n\n // --- Kilo AI: 2 keys ---\n {\n id: \"kilo-1\",\n name: \"Kilo-AI-1\",\n url: \"https://api.kilo.ai/v1/chat/completions\",\n model: \"kilo-1\",\n keys: loadKeys(\"KILO_API_KEY\", 5).concat(loadKeys(\"KILO_KEY\", 5)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 2048,\n timeout: 60000,\n },\n {\n id: \"kilo-2\",\n name: \"Kilo-AI-2\",\n url: \"https://api.kilo.ai/v1/chat/completions\",\n model: \"kilo-2\",\n keys: loadKeys(\"KILO_API_KEY\", 5).concat(loadKeys(\"KILO_KEY\", 5)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 2048,\n timeout: 60000,\n },\n\n // --- Cloudflare: accounts 13, 14, 15 (additional accounts) ---\n {\n id: \"cloudflare-glm47-13\",\n name: \"Cloudflare-GLM47-Acct13\",\n url: \"https://api.cloudflare.com/client/v4/accounts/\" + (process.env.CF_ACCOUNT_ID_13 || \"00000000000000000000000000000000\") + \"/ai/run/@cf/zai-org/glm-4.7-flash\",\n model: \"@cf/zai-org/glm-4.7-flash\",\n keys: loadKeys(\"CF_AI_TOKEN_13\"),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 2048,\n responseFormat: \"cloudflare\",\n timeout: 60000,\n },\n {\n id: \"cloudflare-glm47-14\",\n name: \"Cloudflare-GLM47-Acct14\",\n url: \"https://api.cloudflare.com/client/v4/accounts/\" + (process.env.CF_ACCOUNT_ID_14 || \"00000000000000000000000000000000\") + \"/ai/run/@cf/zai-org/glm-4.7-flash\",\n model: \"@cf/zai-org/glm-4.7-flash\",\n keys: loadKeys(\"CF_AI_TOKEN_14\"),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 2048,\n responseFormat: \"cloudflare\",\n timeout: 60000,\n },\n {\n id: \"cloudflare-glm47-15\",\n name: \"Cloudflare-GLM47-Acct15\",\n url: \"https://api.cloudflare.com/client/v4/accounts/\" + (process.env.CF_ACCOUNT_ID_15 || \"00000000000000000000000000000000\") + \"/ai/run/@cf/zai-org/glm-4.7-flash\",\n model: \"@cf/zai-org/glm-4.7-flash\",\n keys: loadKeys(\"CF_AI_TOKEN_15\"),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 2048,\n responseFormat: \"cloudflare\",\n timeout: 60000,\n },\n\n // ═══════════════════════════════════════════════════════════════════════\n // BATCH 2 — Reaching 100+ judges (2026-05-07)\n // ═══════════════════════════════════════════════════════════════════════\n\n // --- Sarvam: additional key variants (each key = independent judge) ---\n {\n id: \"sarvam-2\",\n name: \"Sarvam-KeyVariant-2\",\n url: \"https://api.sarvam.ai/v1/chat/completions\",\n model: \"sarvam-m\",\n keys: loadKeys(\"SARVAM_KEY_2\", 5).concat(loadKeys(\"SARVAM_API_KEY_2\", 5)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 2048,\n timeout: 45000,\n },\n {\n id: \"sarvam-3\",\n name: \"Sarvam-KeyVariant-3\",\n url: \"https://api.sarvam.ai/v1/chat/completions\",\n model: \"sarvam-m\",\n keys: loadKeys(\"SARVAM_KEY_3\", 5).concat(loadKeys(\"SARVAM_API_KEY_3\", 5)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 2048,\n timeout: 45000,\n },\n\n // --- Xiaomi: additional key variants ---\n {\n id: \"xiaomi-flash-2\",\n name: \"Xiaomi-MiMo-Flash-Key2\",\n url: \"https://api.xiaomimimo.com/v1/chat/completions\",\n model: \"mimo-v2-flash\",\n keys: loadKeys(\"XIAOMI_API_KEY_2\", 5).concat(loadKeys(\"XIAOMI_KEY_2\", 5)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 1024,\n timeout: 30000,\n },\n {\n id: \"xiaomi-pro-2\",\n name: \"Xiaomi-MiMo-Pro-Key2\",\n url: \"https://api.xiaomimimo.com/v1/chat/completions\",\n model: \"mimo-v2-pro\",\n keys: loadKeys(\"XIAOMI_API_KEY_2\", 5).concat(loadKeys(\"XIAOMI_KEY_2\", 5)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 1024,\n timeout: 30000,\n },\n\n // --- Groq: more models ---\n {\n id: \"groq-llama33\",\n name: \"Groq-Llama-3.3-70B\",\n url: \"https://api.groq.com/openai/v1/chat/completions\",\n model: \"llama-3.3-70b-versatile\",\n keys: loadKeys(\"GROQ_API_KEY_2\", 10).concat(loadKeys(\"GROQ_KEY_2\", 10)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 2048,\n timeout: 30000,\n },\n {\n id: \"groq-llama4-2\",\n name: \"Groq-Llama-4-Scout-Key2\",\n url: \"https://api.groq.com/openai/v1/chat/completions\",\n model: \"meta-llama/llama-4-scout-17b-16e-instruct\",\n keys: loadKeys(\"GROQ_API_KEY_2\", 10).concat(loadKeys(\"GROQ_KEY_2\", 10)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 2048,\n timeout: 30000,\n },\n\n // --- OpenRouter: more free models ---\n {\n id: \"openrouter-kimi\",\n name: \"OpenRouter-Kimi-K2-Free\",\n url: \"https://openrouter.ai/api/v1/chat/completions\",\n model: \"moonshotai/kimi-k2:free\",\n keys: loadKeys(\"OPENROUTER_API_KEY_2\", 10).concat(loadKeys(\"OPENROUTER_KEY_2\", 10)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n extraHeaders: { \"HTTP-Referer\": \"https://www.p2pclaw.com\", \"X-Title\": \"P2PCLAW Scoring\" },\n stripThinkTags: true,\n maxTokens: 2048,\n timeout: 60000,\n },\n {\n id: \"openrouter-glm\",\n name: \"OpenRouter-GLM-4.7-Free\",\n url: \"https://openrouter.ai/api/v1/chat/completions\",\n model: \"z-ai/glm-4.7:free\",\n keys: loadKeys(\"OPENROUTER_API_KEY_2\", 10).concat(loadKeys(\"OPENROUTER_KEY_2\", 10)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n extraHeaders: { \"HTTP-Referer\": \"https://www.p2pclaw.com\", \"X-Title\": \"P2PCLAW Scoring\" },\n stripThinkTags: true,\n maxTokens: 2048,\n timeout: 60000,\n },\n {\n id: \"openrouter-gptoss\",\n name: \"OpenRouter-GPT-OSS-Free\",\n url: \"https://openrouter.ai/api/v1/chat/completions\",\n model: \"openai/gpt-oss-120b:free\",\n keys: loadKeys(\"OPENROUTER_API_KEY_2\", 10).concat(loadKeys(\"OPENROUTER_KEY_2\", 10)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n extraHeaders: { \"HTTP-Referer\": \"https://www.p2pclaw.com\", \"X-Title\": \"P2PCLAW Scoring\" },\n maxTokens: 2048,\n timeout: 60000,\n },\n\n // --- HuggingFace: more models ---\n {\n id: \"hf-phi4\",\n name: \"HF-Phi-4\",\n url: \"https://api-inference.huggingface.co/models/microsoft/Phi-4/v1/chat/completions\",\n model: \"microsoft/Phi-4\",\n keys: loadKeys(\"HF_TOKEN_2\", 5).concat(loadKeys(\"HUGGINGFACE_TOKEN_2\", 5)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 2048,\n timeout: 90000,\n },\n {\n id: \"hf-command\",\n name: \"HF-Command-R7B\",\n url: \"https://api-inference.huggingface.co/models/cohere/command-r7b-12-2024/v1/chat/completions\",\n model: \"cohere/command-r7b-12-2024\",\n keys: loadKeys(\"HF_TOKEN_2\", 5).concat(loadKeys(\"HUGGINGFACE_TOKEN_2\", 5)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 2048,\n timeout: 90000,\n },\n {\n id: \"hf-aya\",\n name: \"HF-Aya-23-35B\",\n url: \"https://api-inference.huggingface.co/models/cohere/aya-23-35b/v1/chat/completions\",\n model: \"cohere/aya-23-35b\",\n keys: loadKeys(\"HF_TOKEN_2\", 5).concat(loadKeys(\"HUGGINGFACE_TOKEN_2\", 5)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 2048,\n timeout: 90000,\n },\n\n // --- NVIDIA: more models ---\n {\n id: \"nvidia-phi4\",\n name: \"NVIDIA-Phi-4\",\n url: \"https://integrate.api.nvidia.com/v1/chat/completions\",\n model: \"microsoft/phi-4\",\n keys: loadKeys(\"NVAPI_KEY_2\", 5).concat(loadKeys(\"NVIDIA_API_KEY_2\", 5)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 2048,\n timeout: 60000,\n },\n {\n id: \"nvidia-gemma\",\n name: \"NVIDIA-Gemma-4-27B\",\n url: \"https://integrate.api.nvidia.com/v1/chat/completions\",\n model: \"google/gemma-4-27b-it\",\n keys: loadKeys(\"NVAPI_KEY_2\", 5).concat(loadKeys(\"NVIDIA_API_KEY_2\", 5)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 2048,\n timeout: 60000,\n },\n {\n id: \"nvidia-llama4\",\n name: \"NVIDIA-Llama-4-Scout\",\n url: \"https://integrate.api.nvidia.com/v1/chat/completions\",\n model: \"meta/llama-4-scout-17b-16e-instruct\",\n keys: loadKeys(\"NVAPI_KEY_2\", 5).concat(loadKeys(\"NVIDIA_API_KEY_2\", 5)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 2048,\n timeout: 60000,\n },\n\n // --- Cohere: more models ---\n {\n id: \"cohere-aya\",\n name: \"Cohere-Aya-Expanse\",\n url: \"https://api.cohere.com/v2/chat\",\n model: \"c4ai-aya-expanse-32b\",\n keys: loadKeys(\"COHERE_API_KEY_2\", 5).concat(loadKeys(\"COHERE_KEY_2\", 5)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 4096,\n responseFormat: \"cohere\",\n timeout: 90000,\n },\n\n // --- Mistral: codestral ---\n {\n id: \"mistral-codestral\",\n name: \"Mistral-CodeStral\",\n url: \"https://api.mistral.ai/v1/chat/completions\",\n model: \"codestral-latest\",\n keys: loadKeys(\"MISTRAL_API_KEY_2\", 5).concat(loadKeys(\"MISTRAL_KEY_2\", 5)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 2048,\n timeout: 45000,\n },\n\n // --- Inception: key variant ---\n {\n id: \"inception-2\",\n name: \"Inception-Mercury2-Key2\",\n url: \"https://api.inceptionlabs.ai/v1/chat/completions\",\n model: \"mercury-2\",\n keys: loadKeys(\"INCEPTION_API_KEY_2\", 5).concat(loadKeys(\"INCEPTION_KEY_2\", 5)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 1024,\n timeout: 45000,\n },\n\n // --- Together: more models ---\n {\n id: \"together-nemotron\",\n name: \"Together-Nemotron-3-120B\",\n url: \"https://api.together.xyz/v1/chat/completions\",\n model: \"nvidia/Nemotron-3-120B\",\n keys: loadKeys(\"TOGETHER_API_KEY_2\", 5).concat(loadKeys(\"TOGETHER_KEY_2\", 5)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 2048,\n timeout: 60000,\n },\n {\n id: \"together-phi4\",\n name: \"Together-Phi-4\",\n url: \"https://api.together.xyz/v1/chat/completions\",\n model: \"microsoft/Phi-4\",\n keys: loadKeys(\"TOGETHER_API_KEY_2\", 5).concat(loadKeys(\"TOGETHER_KEY_2\", 5)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 2048,\n timeout: 60000,\n },\n\n // ═══════════════════════════════════════════════════════════════════════\n // BATCH 3 — Final push to 100+ judges (2026-05-07)\n // ═══════════════════════════════════════════════════════════════════════\n\n // --- Cerebras: additional key variants ---\n {\n id: \"cerebras-qwen-2\",\n name: \"Cerebras-Qwen235B-Key2\",\n url: \"https://api.cerebras.ai/v1/chat/completions\",\n model: \"qwen-3-235b-a22b-instruct-2507\",\n keys: loadKeys(\"CEREBRAS_API_KEY_2\", 10).concat(loadKeys(\"CEREBRAS_KEY_2\", 10)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 1024,\n timeout: 30000,\n },\n {\n id: \"cerebras-gptoss-2\",\n name: \"Cerebras-GPT-OSS-Key2\",\n url: \"https://api.cerebras.ai/v1/chat/completions\",\n model: \"gpt-oss-120b\",\n keys: loadKeys(\"CEREBRAS_API_KEY_2\", 10).concat(loadKeys(\"CEREBRAS_KEY_2\", 10)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 1024,\n timeout: 30000,\n },\n {\n id: \"cerebras-glm47-2\",\n name: \"Cerebras-GLM47-Key2\",\n url: \"https://api.cerebras.ai/v1/chat/completions\",\n model: \"zai-glm-4.7\",\n keys: loadKeys(\"CEREBRAS_API_KEY_2\", 10).concat(loadKeys(\"CEREBRAS_KEY_2\", 10)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 1024,\n timeout: 30000,\n },\n\n // --- DeepSeek: key variant ---\n {\n id: \"deepseek-chat-2\",\n name: \"DeepSeek-Chat-Key2\",\n url: \"https://api.deepseek.com/v1/chat/completions\",\n model: \"deepseek-chat\",\n keys: loadKeys(\"DEEPSEEK_API_KEY_2\", 5).concat(loadKeys(\"DEEPSEEK_KEY_2\", 5)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 2048,\n timeout: 60000,\n },\n\n // --- Z.ai: key variant ---\n {\n id: \"zai-glm47-2\",\n name: \"Z.ai-GLM-4.7-Key2\",\n url: \"https://api.z.ai/v1/chat/completions\",\n model: \"glm-4.7\",\n keys: loadKeys(\"ZAI_API_KEY_2\", 5).concat(loadKeys(\"ZAI_KEY_2\", 5)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 2048,\n timeout: 60000,\n },\n\n // --- Gemini: key variant ---\n {\n id: \"gemini-pro-2\",\n name: \"Gemini-2.5-Pro-Key2\",\n url: \"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-pro-preview-05-06:generateContent\",\n model: \"gemini-2.5-pro-preview-05-06\",\n keys: loadKeys(\"GEMINI_API_KEY_2\", 5).concat(loadKeys(\"GEMINI_KEY_2\", 5)),\n authHeader: \"x-goog-api-key\",\n authPrefix: \"\",\n stripThinkTags: true,\n maxTokens: 2048,\n responseFormat: \"gemini\",\n timeout: 60000,\n },\n];\n\n// Deduplicate keys within each provider\nfor (const p of PROVIDERS) {\n p.keys = [...new Set(p.keys)].filter(Boolean);\n}\n\n// Log available providers\nconst available = PROVIDERS.filter(p => p.keys.length > 0);\nconsole.log(`[SCORING] ${available.length} LLM providers available: ${available.map(p => `${p.name}(${p.keys.length})`).join(\", \")}`);\nif (available.length === 0) console.warn(\"[SCORING] No LLM providers — heuristic scoring only.\");\n// Debug: log specifically which providers have NO keys\nconst unavailable = PROVIDERS.filter(p => p.keys.length === 0);\nif (unavailable.length > 0) console.log(`[SCORING] Providers with NO keys: ${unavailable.map(p => p.name).join(\", \")}`);\n\nconst SCORING_PROMPT = `You are a STRICT academic peer reviewer. You evaluate papers for the P2PCLAW benchmark.\n\nYour scoring must follow real academic standards. Most papers score 4-6. Only exceptional work reaches 7+. A score of 8+ means top-tier venue quality (NeurIPS, Nature, ICML). A 10 is historically significant (Turing Award level).\n\nCALIBRATION ANCHORS — use these to calibrate your scores:\n- 10/10 novelty: \"Attention Is All You Need\" (Vaswani 2017), Bitcoin whitepaper (Nakamoto 2008)\n- 8/10 novelty: A genuine new algorithm with proven improvements over SOTA\n- 6/10 novelty: Meaningful extension of existing work with some original insights\n- 5/10 novelty: Applying known techniques to a new domain (standard contribution)\n- 3/10 novelty: Minor variation of existing work, obvious next step\n- 1/10 novelty: Restating known results with different notation\n\nScore each criterion on 0-10:\n- abstract (0-10): Clarity, completeness, problem+scope+results summarized\n- introduction (0-10): Problem clarity, context, motivation. Needs 2+ real citations to related work\n- methodology (0-10): Rigor and reproducibility. Can someone replicate this exactly?\n- results (0-10): Strength of evidence. Real data, real experiments, statistical significance\n- discussion (0-10): Honest limitations, implications, comparison to prior work\n- conclusion (0-10): Summary of findings, concrete future directions\n- references (0-10): Real citations with authors, titles, years, DOIs. 8+ unique for score >5\n- novelty (0-10): TRUE original contribution. Applying a standard formula to a new domain = 4-5, NOT 8-9\n- reproducibility (0-10): Code, equations, parameters, data availability\n- citation_quality (0-10): Are references real, verified, and actually cited in the text?\n\nSTRICT RULES:\n- Missing section = 0 for that section\n- Placeholder/fake references = references score 1\n- Papers under 300 words = all scores below 3\n- Proving obvious things = novelty 0-1\n- Standard variance/mean/known formulas applied to new domain = novelty 4-5 MAX\n- No experimental data or only synthetic/estimated data = results 3-4 MAX\n- Self-referential citations or citing only own work = citation_quality 2-3\n- Circular reasoning (using conclusion to justify methodology) = methodology cap 4\n- Claims without evidence (e.g., \"we achieve SOTA\" with no comparison) = results cap 3\n- DO NOT give 8+ unless the work would genuinely be accepted at a top venue\n\nReturn ONLY this JSON (numbers 0-10):\n{\"abstract\":N,\"introduction\":N,\"methodology\":N,\"results\":N,\"discussion\":N,\"conclusion\":N,\"references\":N,\"novelty\":N,\"reproducibility\":N,\"citation_quality\":N}\n\nPaper content:\n`;\n\n/**\n * Call a single LLM provider to score a paper.\n * Tries up to 2 keys from the provider before giving up.\n */\nasync function callLLMForScoring(prompt, provider) {\n if (!provider.keys.length) return null;\n\n const maxAttempts = Math.min(provider.keys.length, 2);\n for (let attempt = 0; attempt < maxAttempts; attempt++) {\n const key = nextKey(provider.id, provider.keys);\n if (!key) return null;\n\n const headers = { \"Content-Type\": \"application/json\" };\n headers[provider.authHeader] = `${provider.authPrefix}${key}`;\n if (provider.extraHeaders) Object.assign(headers, provider.extraHeaders);\n\n try {\n const res = await fetch(provider.url, {\n method: \"POST\",\n headers,\n body: JSON.stringify({\n model: provider.model,\n messages: [{ role: \"user\", content: prompt }],\n max_tokens: provider.maxTokens || 512,\n temperature: 0.1,\n }),\n signal: AbortSignal.timeout(provider.timeout || 30000),\n });\n\n if (res.status === 429) {\n console.warn(`[SCORING] ${provider.name} 429 rate-limited`);\n await new Promise(r => setTimeout(r, 3000));\n continue;\n }\n if (res.status === 402) {\n console.warn(`[SCORING] ${provider.name} 402 credits exhausted — skipping provider`);\n return null;\n }\n if (!res.ok) {\n const errBody = await res.text().catch(() => \"\");\n console.warn(`[SCORING] ${provider.name} HTTP ${res.status}: ${errBody.substring(0, 150)}`);\n continue;\n }\n\n const data = await res.json();\n // Support multiple response formats: OpenAI, Cohere v2, Cloudflare, Gemini\n let text = \"\";\n if (provider.responseFormat === \"cohere\") {\n // Cohere v2 returns array of content blocks: [{type:\"thinking\",...}, {type:\"text\",text:\"...\"}]\n const blocks = data.message?.content || [];\n if (Array.isArray(blocks)) {\n const textBlock = blocks.find(b => b.type === \"text\");\n text = textBlock?.text || blocks[blocks.length - 1]?.text || \"\";\n } else {\n text = typeof blocks === \"string\" ? blocks : \"\";\n }\n } else if (provider.responseFormat === \"cloudflare\") {\n // Cloudflare Workers AI wraps OpenAI format: {result: {choices: [...]}, success: true}\n const inner = data.result || data;\n text = inner.choices?.[0]?.message?.content || \"\";\n } else if (provider.responseFormat === \"gemini\") {\n // Gemini format: { candidates: [{ content: { parts: [{ text: \"...\" }] } }] }\n const candidate = data.candidates?.[0];\n const parts = candidate?.content?.parts || [];\n text = parts.map(p => p.text || \"\").join(\"\");\n } else {\n text = data.choices?.[0]?.message?.content || \"\";\n }\n\n // Strip <think>...</think> tags (Sarvam, Qwen with thinking mode)\n if (provider.stripThinkTags || text.includes(\"<think>\")) {\n // Strip closed think blocks\n text = text.replace(/<think>[\\s\\S]*?<\\/think>/g, \"\").trim();\n // Strip unclosed think blocks (model ran out of tokens mid-think)\n if (text.startsWith(\"<think>\")) {\n text = text.replace(/<think>[\\s\\S]*/g, \"\").trim();\n }\n }\n\n // Extract JSON — use balanced braces to handle nested objects like {\"score\":8,\"why\":\"...\"}\n let jsonMatch = null;\n const firstBrace = text.indexOf('{');\n if (firstBrace !== -1) {\n let depth = 0, end = -1;\n for (let i = firstBrace; i < text.length; i++) {\n if (text[i] === '{') depth++;\n else if (text[i] === '}') { depth--; if (depth === 0) { end = i; break; } }\n }\n if (end !== -1) jsonMatch = [text.substring(firstBrace, end + 1)];\n }\n if (!jsonMatch) {\n console.warn(`[SCORING] ${provider.name} — no JSON in response`);\n continue;\n }\n\n const parsed = JSON.parse(jsonMatch[0]);\n const fields = [...SECTIONS, \"novelty\", \"reproducibility\", \"citation_quality\"];\n const feedback = {};\n for (const field of fields) {\n let val = parsed[field];\n // Support both plain number and {score, why} object format\n if (val && typeof val === \"object\" && typeof val.score === \"number\") {\n feedback[field] = val.why || null;\n val = val.score;\n }\n if (typeof val !== \"number\" || val < 0 || val > 10) {\n val = typeof val === \"number\" ? Math.max(0, Math.min(10, Math.round(val))) : 5;\n }\n parsed[field] = val;\n }\n\n // Clean parsed: only keep recognized score fields (remove stray \"why\" etc.)\n const cleanScores = {};\n for (const field of fields) cleanScores[field] = parsed[field];\n\n console.log(`[SCORING] ${provider.name} scored successfully`);\n return { scores: cleanScores, provider: provider.name, feedback: Object.keys(feedback).length > 0 ? feedback : null };\n } catch (e) {\n console.warn(`[SCORING] ${provider.name} error: ${e.message}`);\n }\n }\n return null;\n}\n\n/**\n * Deterministic heuristic scoring — used when all LLMs fail.\n */\nfunction heuristicScore(content) {\n const text = content || \"\";\n const words = text.split(/\\s+/).filter(w => w.length > 0);\n const wordCount = words.length;\n const lowerText = text.toLowerCase();\n\n const sectionScores = {};\n for (const section of SECTIONS) {\n const hasSection = lowerText.includes(`## ${section}`) || lowerText.includes(`# ${section}`);\n if (!hasSection) {\n sectionScores[section] = 0;\n continue;\n }\n const sectionRegex = new RegExp(`##?\\\\s*${section}[\\\\s\\\\S]*?(?=##?\\\\s|$)`, \"i\");\n const match = text.match(sectionRegex);\n const sectionWords = match ? match[0].split(/\\s+/).length : 0;\n if (sectionWords < 20) sectionScores[section] = 2;\n else if (sectionWords < 50) sectionScores[section] = 4;\n else if (sectionWords < 100) sectionScores[section] = 5;\n else if (sectionWords < 200) sectionScores[section] = 6;\n else sectionScores[section] = 7;\n }\n\n const refMatches = text.match(/\\[\\d+\\]/g) || [];\n const uniqueRefs = new Set(refMatches).size;\n const hasPlaceholderRefs = /placeholder|author,?\\s*a\\.\\s*\\(\\d{4}\\)/i.test(text);\n const hasRealAuthors = /[A-Z][a-z]+,\\s*[A-Z]\\.\\s*(?:&|,|et al)/g.test(text);\n const hasDOI = /doi\\.org|arxiv\\.org|10\\.\\d{4}/i.test(text);\n let refScore = hasPlaceholderRefs ? 1 : Math.min(7, uniqueRefs);\n if (hasRealAuthors) refScore = Math.min(10, refScore + 1);\n if (hasDOI) refScore = Math.min(10, refScore + 1);\n sectionScores.references = refScore;\n\n const technicalTerms = (text.match(/\\b(algorithm|theorem|proof|complexity|O\\([^)]+\\)|convergence|optimal|novel|framework)\\b/gi) || []).length;\n const hasFigures = /figure \\d|fig\\.\\s*\\d|table \\d/i.test(text);\n let novelty = wordCount > 2000 ? 4 : wordCount > 1000 ? 3 : 2;\n novelty += Math.min(3, Math.floor(technicalTerms / 5));\n if (hasFigures) novelty += 1;\n\n const hasCode = /```[\\s\\S]*?```/.test(text);\n const hasEquations = /\\$[^$]+\\$/.test(text) || /\\\\begin\\{/.test(text);\n const hasNumbers = /\\d+\\.\\d+%|\\d+\\.\\d+x|p\\s*[<>]\\s*0\\.\\d/i.test(text);\n let reproducibility = (hasCode ? 5 : 3) + (hasEquations ? 1 : 0) + (wordCount > 2000 ? 1 : 0);\n if (hasNumbers) reproducibility += 1;\n\n const citation_quality = hasPlaceholderRefs ? 1 : Math.min(6, uniqueRefs);\n\n return {\n scores: {\n ...sectionScores,\n novelty: Math.min(10, novelty),\n reproducibility: Math.min(10, reproducibility),\n citation_quality: Math.min(10, citation_quality),\n },\n provider: \"heuristic\"\n };\n}\n\n/**\n * Score a paper using heterogeneous multi-LLM swarm.\n * Tries to get 2 independent judges, falls back through provider chain.\n */\nexport async function scoreGranular(content, paperType = \"research\") {\n if (!content || content.trim().length < 50) {\n return {\n sections: Object.fromEntries(SECTIONS.map(s => [s, 0])),\n overall: 0,\n novelty: 0,\n reproducibility: 0,\n citation_quality: 0,\n judges: [],\n judge_count: 0,\n error: \"Content too short to score\"\n };\n }\n\n // All LLM judges support 32k+ context. Send enough content to include ALL 7 mandatory sections.\n // Previous limit of 4000 chars truncated papers before methodology/results/conclusion, causing\n // strict judges (Cohere, Xiaomi) to correctly score missing sections as 0.\n const truncated = content.length > 16000 ? content.substring(0, 16000) + \"\\n\\n[... truncated for scoring ...]\" : content;\n const prompt = SCORING_PROMPT + truncated;\n\n // ALL available judges score independently for maximum consensus diversity.\n // Each model evaluates each section independently, then we average.\n // Run all judges in parallel for speed (each has its own timeout).\n const judgePromises = available.map(provider =>\n callLLMForScoring(prompt, provider).catch(() => null)\n );\n const judgeResults = await Promise.all(judgePromises);\n let judges = judgeResults.filter(Boolean);\n\n // Fix #2: Filter broken judges — remove any judge that gave 0 to 3+ sections\n // (indicates a parsing failure or truncated response, not a real evaluation)\n judges = judges.filter(j => {\n const zeroCount = SECTIONS.filter(s => j.scores[s] === 0).length;\n if (zeroCount >= 3) {\n console.warn(`[SCORING] Filtered broken judge ${j.judge || 'unknown'}: ${zeroCount} sections scored 0`);\n return false;\n }\n return true;\n });\n\n // If no LLM judges succeeded, use heuristic\n if (judges.length === 0) {\n const heuristic = heuristicScore(content);\n judges.push(heuristic);\n }\n\n // Average scores across all judges\n const allFields = [...SECTIONS, \"novelty\", \"reproducibility\", \"citation_quality\"];\n const averaged = {};\n for (const field of allFields) {\n const values = judges.map(j => j.scores[field]).filter(v => typeof v === \"number\");\n averaged[field] = values.length > 0\n ? Math.round((values.reduce((a, b) => a + b, 0) / values.length) * 10) / 10\n : 0;\n }\n\n // ── CALIBRATION PASS — adjust raw averages against reference benchmarks ──\n // This is the core fix for inflated scoring. Raw LLM averages are compared\n // against quality signals extracted from the paper content and calibrated\n // against recognized reference paper fingerprints.\n let calibration = null;\n try {\n const fieldResult = detectField(content);\n const signals = extractSignals(content);\n const benchmarks = REFERENCE_BENCHMARKS[fieldResult.field] || null;\n const { calibrated, adjustments } = calibrateScores(averaged, signals, benchmarks);\n\n // Apply calibrated scores over raw averages\n const adjustmentCount = Object.keys(adjustments).length;\n if (adjustmentCount > 0) {\n for (const [field, val] of Object.entries(calibrated)) {\n if (typeof val === \"number\" && averaged[field] !== undefined) {\n averaged[field] = val;\n }\n }\n console.log(`[SCORING] Calibration applied: ${adjustmentCount} adjustments, field=${fieldResult.field}, ` +\n `red_flags=${signals.red_flag_count}, depth=${signals.depth_score}`);\n }\n\n calibration = {\n field: fieldResult.field,\n field_confidence: fieldResult.confidence,\n signals_summary: {\n word_count: signals.word_count,\n sections_present: signals.sections_present.length,\n sections_missing: signals.sections_missing,\n red_flags: signals.red_flags,\n red_flag_count: signals.red_flag_count,\n has_formal_proofs: signals.has_formal_proofs,\n has_equations: signals.has_equations,\n has_code: signals.has_code,\n unique_refs: signals.unique_refs,\n has_placeholder_refs: signals.has_placeholder_refs,\n depth_score: signals.depth_score,\n evidence_markers: signals.evidence_markers,\n deception_count: signals.deception_count || 0,\n deception_matches: (signals.deception_matches || []).map(d => ({\n id: d.id, name: d.name, severity: d.severity,\n })),\n // New quality dimensions\n grammar: signals.grammar_quality ? {\n vocabulary_diversity: signals.grammar_quality.vocabulary_diversity_ttr,\n is_monotone: signals.grammar_quality.is_monotone,\n is_low_vocabulary: signals.grammar_quality.is_low_vocabulary,\n } : null,\n repetition_ratio: signals.repetition_score?.repetition_ratio || 0,\n code_quality: signals.code_quality?.blocks_found > 0 ? {\n blocks: signals.code_quality.blocks_found,\n has_real_code: signals.has_real_code || false,\n has_python: signals.code_quality.has_python || false,\n } : null,\n math_formulas: signals.math_quality?.formula_count || 0,\n lean4: signals.lean4_signals?.verification_level || \"none\",\n tables: signals.table_quality?.count || 0,\n },\n adjustments,\n adjustment_count: adjustmentCount,\n reference_papers: benchmarks ? benchmarks.references.map(r => r.title) : [],\n };\n } catch (calErr) {\n console.warn(`[SCORING] Calibration error (non-fatal): ${calErr.message}`);\n }\n\n // ── LIVE VERIFICATION PASS — real-time CrossRef, arXiv, code exec, Lean4 ──\n // Runs in parallel with independent timeouts. Non-fatal: if it fails, scoring\n // continues with just calibration. Results apply caps and bonuses to scores.\n let liveVerification = null;\n try {\n const verification = await runLiveVerification(content);\n const { adjustments: liveAdj, bonuses: liveBon } = verificationToAdjustments(verification);\n\n // ── OVERRIDE FALSE POSITIVES ──\n // If live verification confirmed code execution but calibration flagged\n // \"code_blocks_are_template_not_real\", the red_flag was a false positive.\n // Undo the -1.5 penalty applied to all dimensions by recalibrating.\n const codeExec = verification.code_execution;\n if (codeExec && codeExec.passed > 0 && calibration && calibration.signals_summary) {\n const redFlags = calibration.signals_summary.red_flags || [];\n if (redFlags.includes(\"code_blocks_are_template_not_real\")) {\n // The -1.5 penalty was applied to all 10 dimensions. Undo it.\n const allFields = [...SECTIONS, \"novelty\", \"reproducibility\", \"citation_quality\"];\n for (const field of allFields) {\n if (averaged[field] !== undefined) {\n averaged[field] = Math.min(10, Math.round((averaged[field] + 1.5) * 10) / 10);\n }\n }\n // Remove the false red flag from the report\n calibration.signals_summary.red_flags = redFlags.filter(f => f !== \"code_blocks_are_template_not_real\");\n calibration.signals_summary.red_flag_count = calibration.signals_summary.red_flags.length;\n calibration.false_positive_corrected = \"code_blocks_are_template_not_real (live verification confirmed code executes)\";\n console.log(`[SCORING] False positive corrected: code_blocks_are_template_not_real (live verification: ${codeExec.passed}/${codeExec.total} passed)`);\n }\n }\n\n // Apply caps (these override score to a max value)\n for (const [key, val] of Object.entries(liveAdj)) {\n if (key.endsWith(\"_cap\") && typeof val === \"number\") {\n const field = key.replace(\"_cap\", \"\");\n if (averaged[field] !== undefined && averaged[field] > val) {\n averaged[field] = val;\n }\n }\n }\n\n // Apply bonuses (add to score, capped at 10)\n for (const [key, val] of Object.entries(liveBon)) {\n if (key.endsWith(\"_bonus\") && typeof val === \"number\") {\n const field = key.replace(\"_bonus\", \"\");\n if (averaged[field] !== undefined) {\n averaged[field] = Math.min(10, Math.round((averaged[field] + val) * 10) / 10);\n }\n }\n }\n\n liveVerification = {\n verification_time_ms: verification.verification_time_ms,\n citations: verification.citations ? {\n total: verification.citations.total,\n verified: verification.citations.verified,\n verification_rate: verification.citations.verification_rate,\n } : null,\n novelty: verification.novelty ? {\n searched: verification.novelty.searched,\n total_found: verification.novelty.total_found,\n novelty_concern: verification.novelty.novelty_concern,\n max_similarity: verification.novelty.max_similarity,\n } : null,\n code_execution: verification.code_execution ? {\n total: verification.code_execution.total,\n passed: verification.code_execution.passed,\n failed: verification.code_execution.failed,\n } : null,\n lean4: verification.lean4 ? {\n blocks_found: verification.lean4.blocks_found,\n verified: verification.lean4.verified,\n has_unsubstantiated_claim: verification.lean4.has_unsubstantiated_claim || false,\n } : null,\n adjustments: liveAdj,\n bonuses: liveBon,\n };\n\n const adjCount = Object.keys(liveAdj).filter(k => k.endsWith(\"_cap\")).length;\n const bonCount = Object.keys(liveBon).filter(k => k.endsWith(\"_bonus\")).length;\n if (adjCount > 0 || bonCount > 0) {\n console.log(`[SCORING] Live verification: ${adjCount} caps, ${bonCount} bonuses applied (${verification.verification_time_ms}ms)`);\n }\n } catch (liveErr) {\n // Fix #3: Better error logging for live verification failures\n console.warn(`[SCORING] Live verification error (non-fatal): ${liveErr.message}`, liveErr.stack?.split('\\n').slice(0, 3).join(' | '));\n }\n\n // Fix: overall = average of ALL 10 dimensions (7 sections + novelty + reproducibility + citation_quality)\n // Previously only averaged the 7 sections, causing mismatch with displayed scores.\n const allDimensionValues = allFields.map(f => averaged[f]);\n let overall = Math.round((allDimensionValues.reduce((a, b) => a + b, 0) / allFields.length) * 10) / 10;\n\n // ── Phase F: Execution Proof Bonus ──\n // Papers with verified code blocks (execution hashes) get a purely additive overall bonus.\n // This only increases the score, never decreases. Capped at 10.\n if (liveVerification && liveVerification.bonuses && liveVerification.bonuses.execution_proof_bonus) {\n overall = Math.min(10, Math.round((overall + liveVerification.bonuses.execution_proof_bonus) * 10) / 10);\n console.log(`[SCORING] Execution proof bonus applied: +${liveVerification.bonuses.execution_proof_bonus} → overall=${overall}`);\n }\n\n // Fix #10: Fallback execution proof bonus — if live verification failed/timed out\n // but the paper contains verified execution hashes, still award the bonus.\n if (!liveVerification || !liveVerification.bonuses?.execution_proof_bonus) {\n const hashMatches = (content.match(/execution[_ ]hash[^`]*`([a-f0-9]{40,})`/gi) || []);\n if (hashMatches.length > 0) {\n const fallbackBonus = Math.min(1.5, hashMatches.length * 0.5);\n overall = Math.min(10, Math.round((overall + fallbackBonus) * 10) / 10);\n console.log(`[SCORING] Fallback execution proof bonus: ${hashMatches.length} hashes found in paper → +${fallbackBonus}`);\n if (!liveVerification) liveVerification = {};\n if (!liveVerification.bonuses) liveVerification.bonuses = {};\n liveVerification.bonuses.execution_proof_bonus = fallbackBonus;\n liveVerification.bonuses.execution_proof_note = `${hashMatches.length} execution hash(es) found in paper text (fallback detection)`;\n }\n }\n\n // Per-judge detail breakdown (individual scores + feedback)\n const judge_details = judges.map(j => ({\n judge: j.provider,\n scores: j.scores,\n feedback: j.feedback || null,\n }));\n\n // Consensus score per dimension (1 = all judges agree, 0 = total disagreement)\n const consensus = {};\n for (const field of allFields) {\n const values = judges.map(j => j.scores[field]).filter(v => typeof v === \"number\");\n if (values.length < 2) { consensus[field] = 1.0; continue; }\n const mean = values.reduce((a, b) => a + b, 0) / values.length;\n const variance = values.reduce((sum, v) => sum + (v - mean) ** 2, 0) / values.length;\n const stddev = Math.sqrt(variance);\n consensus[field] = Math.round(Math.max(0, 1 - stddev / 5) * 100) / 100;\n }\n const overall_consensus = Math.round(\n (allFields.reduce((sum, f) => sum + consensus[f], 0) / allFields.length) * 100\n ) / 100;\n\n // Aggregate feedback per dimension (from all judges that provided it)\n const aggregated_feedback = {};\n for (const field of allFields) {\n const comments = judges\n .filter(j => j.feedback && j.feedback[field])\n .map(j => ({ judge: j.provider, comment: j.feedback[field] }));\n if (comments.length > 0) aggregated_feedback[field] = comments;\n }\n\n const result = {\n sections: Object.fromEntries(SECTIONS.map(s => [s, averaged[s]])),\n overall,\n novelty: averaged.novelty,\n reproducibility: averaged.reproducibility,\n citation_quality: averaged.citation_quality,\n judges: judges.map(j => j.provider),\n judge_count: judges.length,\n judge_details,\n consensus,\n overall_consensus,\n feedback: Object.keys(aggregated_feedback).length > 0 ? aggregated_feedback : null,\n scored_at: new Date().toISOString(),\n paper_type: paperType,\n calibration,\n live_verification: liveVerification,\n };\n\n console.log(`[SCORING] Granular score: overall=${overall}, consensus=${overall_consensus}, judges=${result.judges.join(\",\")}`);\n return result;\n}\n"}, "81d461a07c7b55303068efaa5f43fdeb58894f50": {"t": 1785753917.5846343, "n": 43617, "h": "7def9ee588f7d4e6", "c": "/**\n * P2PCLAW Granular Scoring Service — MAXIMUM TRIBUNAL\n * ====================================================\n * Heterogeneous multi-LLM scoring engine that evaluates papers section-by-section.\n * Updated 2026-04-04: ALL available API keys deployed for maximum judge diversity.\n *\n * Provider chain (updated 2026-04-04):\n * 1. Cerebras — qwen-3-235b-a22b (8 keys, free, ultra-fast)\n * 2. Cerebras — llama3.1-8b (8 keys, free, ultra-fast)\n * 3. Cerebras — gpt-oss-120b (8 keys, free, different model perspective)\n * 4. Cerebras — zai-glm-4.7 (8 keys, free, Chinese model perspective)\n * 5. Mistral — mistral-small-latest (3 keys, free)\n * 6. Sarvam — sarvam-m (13 keys, Indian AI, free)\n * 7. OpenRouter — qwen3-coder:free (3 keys, free)\n * 8. Groq — llama-3.3-70b-versatile (9 keys)\n * 9. NVIDIA — meta/llama-3.3-70b-instruct (3 keys, free)\n * 10. Inception — mercury-2 (10 keys, free, diffusion-based)\n * 11. Xiaomi MiMo — mimo-v2-flash (6 keys, free)\n * 12. Xiaomi MiMo — mimo-v2-pro (6 keys, free, reasoning)\n * 13. Cohere — command-a-reasoning (10 keys, reasoning model)\n * 14-22. Cloudflare — 9 accounts x unique models (GLM4, Gemma4, Nemotron, Kimi, GPT-OSS, Qwen3, Llama4Scout, MistralSmall31, DeepSeekR1)\n * 23. Cloudflare — account 10 (GLM-4.7-flash)\n * 24. Cloudflare — account 11 (Gemma-4-26b, additional account)\n * 25. Cloudflare — account 12 (Mistral Small 3.1 24B, additional account)\n * 26. OpenRouter — qwen/qwen3.6-plus:free (large reasoning model)\n * 27. NVIDIA — deepseek-ai/deepseek-v3.2 (reasoning, thinking model)\n * 28. NVIDIA — stepfun-ai/step-3.5-flash (Chinese model, reasoning)\n * 29. NVIDIA — z-ai/glm4.7 (Chinese model, thinking)\n * 30. Deterministic heuristic fallback (never blocks)\n *\n * TOTAL: 29 independent LLM judges + 1 heuristic = 30 scoring perspectives\n * ALL available judges score independently. Final score = average across all judges.\n * Each model evaluates each section independently for maximum consensus diversity.\n */\n\nimport { detectField, extractSignals, calibrateScores, REFERENCE_BENCHMARKS } from \"./calibrationService.js\";\nimport { runLiveVerification, verificationToAdjustments } from \"./liveVerificationService.js\";\n\nconst SECTIONS = [\"abstract\", \"introduction\", \"methodology\", \"results\", \"discussion\", \"conclusion\", \"references\"];\n\n// ── Load keys with rotation ─────────────────────────────────────────────────\n\nfunction loadKeys(envPrefix, maxKeys = 10) {\n const keys = [];\n for (let i = 1; i <= maxKeys; i++) {\n const k = process.env[`${envPrefix}_${i}`] || process.env[`${envPrefix}${i}`];\n if (k) keys.push(k);\n }\n // Also check single-key env vars\n const single = process.env[envPrefix];\n if (single && !keys.includes(single)) keys.unshift(single);\n return keys;\n}\n\nconst keyIndices = {};\nfunction nextKey(providerId, keys) {\n if (!keys.length) return null;\n const idx = keyIndices[providerId] || 0;\n const key = keys[idx % keys.length];\n keyIndices[providerId] = (idx + 1) % keys.length;\n return key;\n}\n\n// ── Provider definitions ────────────────────────────────────────────────────\n\nconst PROVIDERS = [\n // --- Cerebras: 3 keys x 2 models = up to 6 independent judges ---\n {\n id: \"cerebras-qwen\",\n name: \"Cerebras-Qwen235B\",\n url: \"https://api.cerebras.ai/v1/chat/completions\",\n model: \"qwen-3-235b-a22b-instruct-2507\",\n keys: loadKeys(\"CEREBRAS_API_KEY\", 15).concat(loadKeys(\"CEREBRAS_KEY\", 15)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 1024,\n },\n {\n id: \"cerebras-llama\",\n name: \"Cerebras-Llama8B\",\n url: \"https://api.cerebras.ai/v1/chat/completions\",\n model: \"llama3.1-8b\",\n keys: loadKeys(\"CEREBRAS_API_KEY\", 15).concat(loadKeys(\"CEREBRAS_KEY\", 15)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n // --- Cerebras: GPT-OSS-120B (120B param open-source model, different perspective) ---\n {\n id: \"cerebras-gptoss\",\n name: \"Cerebras-GPT-OSS-120B\",\n url: \"https://api.cerebras.ai/v1/chat/completions\",\n model: \"gpt-oss-120b\",\n keys: loadKeys(\"CEREBRAS_API_KEY\", 15).concat(loadKeys(\"CEREBRAS_KEY\", 15)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n // --- Cerebras: ZAI GLM-4.7 (Chinese model — different cultural perspective on academic rigor) ---\n {\n id: \"cerebras-glm47\",\n name: \"Cerebras-GLM4.7\",\n url: \"https://api.cerebras.ai/v1/chat/completions\",\n model: \"zai-glm-4.7\",\n keys: loadKeys(\"CEREBRAS_API_KEY\", 15).concat(loadKeys(\"CEREBRAS_KEY\", 15)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 1024,\n },\n // --- Mistral: 3 keys, reliable ---\n {\n id: \"mistral\",\n name: \"Mistral\",\n url: \"https://api.mistral.ai/v1/chat/completions\",\n model: \"mistral-small-latest\",\n keys: loadKeys(\"MISTRAL_API_KEY\", 10).concat(loadKeys(\"MISTRAL_KEY\", 10)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n // --- Sarvam (Indian AI): sarvam-m, uses <think> tags (needs 2048+ tokens), 13 keys ---\n {\n id: \"sarvam\",\n name: \"Sarvam\",\n url: \"https://api.sarvam.ai/v1/chat/completions\",\n model: \"sarvam-m\",\n keys: loadKeys(\"SARVAM_KEY\", 15).concat(loadKeys(\"SARVAM_API_KEY\", 15)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 2048,\n },\n // --- OpenRouter: free models ---\n {\n id: \"openrouter\",\n name: \"OpenRouter\",\n url: \"https://openrouter.ai/api/v1/chat/completions\",\n model: \"qwen/qwen3-coder:free\",\n keys: loadKeys(\"OPENROUTER_API_KEY\"),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n extraHeaders: { \"HTTP-Referer\": \"https://www.p2pclaw.com\", \"X-Title\": \"P2PCLAW Scoring\" },\n },\n // --- Groq: 9 keys but may be org-restricted ---\n {\n id: \"groq\",\n name: \"Groq\",\n url: \"https://api.groq.com/openai/v1/chat/completions\",\n model: \"llama-3.3-70b-versatile\",\n keys: loadKeys(\"GROQ_API_KEY\", 15).concat(loadKeys(\"GROQ_KEY\", 15)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n // --- NVIDIA: 3 keys ---\n {\n id: \"nvidia\",\n name: \"NVIDIA\",\n url: \"https://integrate.api.nvidia.com/v1/chat/completions\",\n model: \"meta/llama-3.3-70b-instruct\",\n keys: loadKeys(\"NVAPI_KEY\", 10).concat(loadKeys(\"NVIDIA_API_KEY\", 10)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n timeout: 45000,\n },\n // --- Inception: mercury-2 (diffusion-based LLM — unique scoring perspective) ---\n {\n id: \"inception\",\n name: \"Inception-Mercury2\",\n url: \"https://api.inceptionlabs.ai/v1/chat/completions\",\n model: \"mercury-2\",\n keys: loadKeys(\"INCEPTION_API_KEY\", 15).concat(loadKeys(\"INCEPTION_KEY\", 15)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 1024, // Mercury-2 needs at least 50 tokens (reasoning model)\n timeout: 45000,\n },\n // --- Xiaomi MiMo: 5 keys x 2 models = up to 2 independent judges ---\n {\n id: \"xiaomi-flash\",\n name: \"Xiaomi-MiMo-Flash\",\n url: \"https://api.xiaomimimo.com/v1/chat/completions\",\n model: \"mimo-v2-flash\",\n keys: loadKeys(\"XIAOMI_API_KEY\", 10).concat(loadKeys(\"XIAOMI_KEY\", 10)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n {\n id: \"xiaomi-pro\",\n name: \"Xiaomi-MiMo-Pro\",\n url: \"https://api.xiaomimimo.com/v1/chat/completions\",\n model: \"mimo-v2-pro\",\n keys: loadKeys(\"XIAOMI_API_KEY\", 10).concat(loadKeys(\"XIAOMI_KEY\", 10)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 1024,\n },\n // --- Cohere: command-a-reasoning model (9 keys, reasoning/thinking model) ---\n {\n id: \"cohere\",\n name: \"Cohere-CommandA\",\n url: \"https://api.cohere.com/v2/chat\",\n model: \"command-a-reasoning-08-2025\",\n keys: loadKeys(\"COHERE_API_KEY\", 15).concat(loadKeys(\"COHERE_KEY\", 15)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 4096, // reasoning model needs extra tokens for thinking + answer\n responseFormat: \"cohere\", // data.message.content[] array with {type:\"thinking\"} + {type:\"text\"}\n timeout: 90000, // 90s — reasoning model needs time to think through all 10 dimensions\n },\n // --- Cloudflare Workers AI: 6 models across 6 accounts (all FREE) ---\n // Ordered by model quality: best first\n {\n id: \"cloudflare-glm\",\n name: \"Cloudflare-GLM4\",\n url: `https://api.cloudflare.com/client/v4/accounts/eaffd2b52c95c69aaad8d859e9dcb52b/ai/run/@cf/zai-org/glm-4.7-flash`,\n model: \"@cf/zai-org/glm-4.7-flash\",\n keys: loadKeys(\"CF_AI_TOKEN\").concat(loadKeys(\"CLOUDFLARE_AI_TOKEN\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 2048,\n responseFormat: \"cloudflare\",\n timeout: 60000,\n },\n {\n id: \"cloudflare-gemma4\",\n name: \"Cloudflare-Gemma4\",\n url: `https://api.cloudflare.com/client/v4/accounts/a7995d3f33b6ba57955749337c9abbe0/ai/run/@cf/google/gemma-4-26b-a4b-it`,\n model: \"@cf/google/gemma-4-26b-a4b-it\",\n keys: loadKeys(\"CF_AI_TOKEN_2\"),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 1024,\n responseFormat: \"cloudflare\",\n timeout: 60000,\n },\n {\n id: \"cloudflare-nemotron\",\n name: \"Cloudflare-Nemotron\",\n url: `https://api.cloudflare.com/client/v4/accounts/194d9aea21482ac893ed81fc6b004864/ai/run/@cf/nvidia/nemotron-3-120b-a12b`,\n model: \"@cf/nvidia/nemotron-3-120b-a12b\",\n keys: loadKeys(\"CF_AI_TOKEN_3\"),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 1024,\n responseFormat: \"cloudflare\",\n timeout: 60000,\n },\n {\n id: \"cloudflare-kimi\",\n name: \"Cloudflare-Kimi\",\n url: `https://api.cloudflare.com/client/v4/accounts/401a75ead25275262c1c05eecb7a997c/ai/run/@cf/moonshotai/kimi-k2.5`,\n model: \"@cf/moonshotai/kimi-k2.5\",\n keys: loadKeys(\"CF_AI_TOKEN_4\"),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 2048,\n responseFormat: \"cloudflare\",\n timeout: 60000,\n },\n {\n id: \"cloudflare-gptoss\",\n name: \"Cloudflare-GPT-OSS\",\n url: `https://api.cloudflare.com/client/v4/accounts/73340519f6430362daee759ba0b48ce8/ai/run/@cf/openai/gpt-oss-120b`,\n model: \"@cf/openai/gpt-oss-120b\",\n keys: loadKeys(\"CF_AI_TOKEN_5\"),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 1024,\n responseFormat: \"cloudflare\",\n timeout: 60000,\n },\n {\n id: \"cloudflare-qwen3\",\n name: \"Cloudflare-Qwen3\",\n url: `https://api.cloudflare.com/client/v4/accounts/df4a7888befcbb6ce3e0a0b346ea1990/ai/run/@cf/qwen/qwen3-30b-a3b-fp8`,\n model: \"@cf/qwen/qwen3-30b-a3b-fp8\",\n keys: loadKeys(\"CF_AI_TOKEN_6\"),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 2048,\n responseFormat: \"cloudflare\",\n timeout: 60000,\n },\n // --- Cloudflare Workers AI: 3 NEW accounts (accounts 7/8/9) ---\n {\n id: \"cloudflare-llama4scout\",\n name: \"Cloudflare-Llama4Scout\",\n url: `https://api.cloudflare.com/client/v4/accounts/3cd084561890e5ab468456fae547ded0/ai/run/@cf/meta/llama-4-scout-17b-16e-instruct`,\n model: \"@cf/meta/llama-4-scout-17b-16e-instruct\",\n keys: loadKeys(\"CF_AI_TOKEN_7\"),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 1024,\n responseFormat: \"cloudflare\",\n timeout: 60000,\n },\n {\n id: \"cloudflare-mistral31\",\n name: \"Cloudflare-MistralSmall31\",\n url: `https://api.cloudflare.com/client/v4/accounts/27920eccf7d83f7ee267130cd6018eaf/ai/run/@cf/mistralai/mistral-small-3.1-24b-instruct`,\n model: \"@cf/mistralai/mistral-small-3.1-24b-instruct\",\n keys: loadKeys(\"CF_AI_TOKEN_8\"),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 1024,\n responseFormat: \"cloudflare\",\n timeout: 60000,\n },\n {\n id: \"cloudflare-deepseekr1\",\n name: \"Cloudflare-DeepSeekR1\",\n url: `https://api.cloudflare.com/client/v4/accounts/60c2dcaa7fc3377f036114648f6397ba/ai/run/@cf/deepseek-ai/deepseek-r1-distill-qwen-32b`,\n model: \"@cf/deepseek-ai/deepseek-r1-distill-qwen-32b\",\n keys: loadKeys(\"CF_AI_TOKEN_9\"),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 2048,\n responseFormat: \"cloudflare\",\n timeout: 60000,\n },\n // --- Cloudflare Workers AI: Account 10 (Agnuxo2026, GLM-4.7-flash) ---\n {\n id: \"cloudflare-glm47-10\",\n name: \"Cloudflare-GLM47-Acct10\",\n url: `https://api.cloudflare.com/client/v4/accounts/${process.env.CF_ACCOUNT_ID_10 || \"00c5ebf4df46d16450d5f1419dc36c6a\"}/ai/run/@cf/zai-org/glm-4.7-flash`,\n model: \"@cf/zai-org/glm-4.7-flash\",\n keys: loadKeys(\"CF_AI_TOKEN_10\"),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 2048,\n responseFormat: \"cloudflare\",\n timeout: 60000,\n },\n // --- Cloudflare Workers AI: Account 11 (agnuxo300@zohomail.eu, Gemma-4-26b) ---\n {\n id: \"cloudflare-gemma4-11\",\n name: \"Cloudflare-Gemma4-Acct11\",\n url: `https://api.cloudflare.com/client/v4/accounts/${process.env.CF_ACCOUNT_ID_11 || \"85fbdab1851209cbd99773a758831fc0\"}/ai/run/@cf/google/gemma-4-26b-a4b-it`,\n model: \"@cf/google/gemma-4-26b-a4b-it\",\n keys: loadKeys(\"CF_AI_TOKEN_11\"),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 1024,\n responseFormat: \"cloudflare\",\n timeout: 60000,\n },\n // --- Cloudflare account 12: Mistral Small 3.1 24B ---\n {\n id: \"cloudflare-mistral31-12\",\n name: \"Cloudflare-MistralSmall31-Acct12\",\n url: `https://api.cloudflare.com/client/v4/accounts/${process.env.CF_ACCOUNT_ID_12 || \"ccd856bec1f7fb8e9745f21e9bd742f4\"}/ai/run/@cf/mistralai/mistral-small-3.1-24b-instruct`,\n model: \"@cf/mistralai/mistral-small-3.1-24b-instruct\",\n keys: loadKeys(\"CF_AI_TOKEN_12\"),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 1024,\n responseFormat: \"cloudflare\",\n timeout: 60000,\n },\n // --- OpenRouter: Qwen 3.6 Plus (free, large reasoning model) ---\n {\n id: \"openrouter-qwen36plus\",\n name: \"OpenRouter-Qwen3.6Plus\",\n url: \"https://openrouter.ai/api/v1/chat/completions\",\n model: \"qwen/qwen3.6-plus:free\",\n keys: loadKeys(\"OPENROUTER_API_KEY\", 15),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 2048,\n timeout: 60000,\n },\n // --- NVIDIA: DeepSeek-V3.2 (reasoning model with thinking) ---\n {\n id: \"nvidia-deepseek-v3\",\n name: \"NVIDIA-DeepSeekV3.2\",\n url: \"https://integrate.api.nvidia.com/v1/chat/completions\",\n model: \"deepseek-ai/deepseek-v3.2\",\n keys: loadKeys(\"NVAPI_KEY\", 10).concat(loadKeys(\"NVIDIA_API_KEY\", 10)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 2048,\n timeout: 60000,\n },\n // --- NVIDIA: StepFun Step-3.5-Flash (Chinese reasoning model) ---\n {\n id: \"nvidia-stepfun\",\n name: \"NVIDIA-StepFun3.5\",\n url: \"https://integrate.api.nvidia.com/v1/chat/completions\",\n model: \"stepfun-ai/step-3.5-flash\",\n keys: loadKeys(\"NVAPI_KEY\", 10).concat(loadKeys(\"NVIDIA_API_KEY\", 10)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 2048,\n timeout: 60000,\n },\n // --- NVIDIA: GLM-4.7 (Z.ai Chinese model with thinking) ---\n {\n id: \"nvidia-glm47\",\n name: \"NVIDIA-GLM4.7\",\n url: \"https://integrate.api.nvidia.com/v1/chat/completions\",\n model: \"z-ai/glm4.7\",\n keys: loadKeys(\"NVAPI_KEY\", 10).concat(loadKeys(\"NVIDIA_API_KEY\", 10)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 2048,\n timeout: 60000,\n },\n];\n\n// Deduplicate keys within each provider\nfor (const p of PROVIDERS) {\n p.keys = [...new Set(p.keys)].filter(Boolean);\n}\n\n// Log available providers\nconst available = PROVIDERS.filter(p => p.keys.length > 0);\nconsole.log(`[SCORING] ${available.length} LLM providers available: ${available.map(p => `${p.name}(${p.keys.length})`).join(\", \")}`);\nif (available.length === 0) console.warn(\"[SCORING] No LLM providers — heuristic scoring only.\");\n// Debug: log specifically which providers have NO keys\nconst unavailable = PROVIDERS.filter(p => p.keys.length === 0);\nif (unavailable.length > 0) console.log(`[SCORING] Providers with NO keys: ${unavailable.map(p => p.name).join(\", \")}`);\n\nconst SCORING_PROMPT = `You are a STRICT academic peer reviewer. You evaluate papers for the P2PCLAW benchmark.\n\nYour scoring must follow real academic standards. Most papers score 4-6. Only exceptional work reaches 7+. A score of 8+ means top-tier venue quality (NeurIPS, Nature, ICML). A 10 is historically significant (Turing Award level).\n\nCALIBRATION ANCHORS — use these to calibrate your scores:\n- 10/10 novelty: \"Attention Is All You Need\" (Vaswani 2017), Bitcoin whitepaper (Nakamoto 2008)\n- 8/10 novelty: A genuine new algorithm with proven improvements over SOTA\n- 6/10 novelty: Meaningful extension of existing work with some original insights\n- 5/10 novelty: Applying known techniques to a new domain (standard contribution)\n- 3/10 novelty: Minor variation of existing work, obvious next step\n- 1/10 novelty: Restating known results with different notation\n\nScore each criterion on 0-10:\n- abstract (0-10): Clarity, completeness, problem+scope+results summarized\n- introduction (0-10): Problem clarity, context, motivation. Needs 2+ real citations to related work\n- methodology (0-10): Rigor and reproducibility. Can someone replicate this exactly?\n- results (0-10): Strength of evidence. Real data, real experiments, statistical significance\n- discussion (0-10): Honest limitations, implications, comparison to prior work\n- conclusion (0-10): Summary of findings, concrete future directions\n- references (0-10): Real citations with authors, titles, years, DOIs. 8+ unique for score >5\n- novelty (0-10): TRUE original contribution. Applying a standard formula to a new domain = 4-5, NOT 8-9\n- reproducibility (0-10): Code, equations, parameters, data availability\n- citation_quality (0-10): Are references real, verified, and actually cited in the text?\n\nSTRICT RULES:\n- Missing section = 0 for that section\n- Placeholder/fake references = references score 1\n- Papers under 300 words = all scores below 3\n- Proving obvious things = novelty 0-1\n- Standard variance/mean/known formulas applied to new domain = novelty 4-5 MAX\n- No experimental data or only synthetic/estimated data = results 3-4 MAX\n- Self-referential citations or citing only own work = citation_quality 2-3\n- Circular reasoning (using conclusion to justify methodology) = methodology cap 4\n- Claims without evidence (e.g., \"we achieve SOTA\" with no comparison) = results cap 3\n- DO NOT give 8+ unless the work would genuinely be accepted at a top venue\n\nReturn ONLY this JSON (numbers 0-10):\n{\"abstract\":N,\"introduction\":N,\"methodology\":N,\"results\":N,\"discussion\":N,\"conclusion\":N,\"references\":N,\"novelty\":N,\"reproducibility\":N,\"citation_quality\":N}\n\nPaper content:\n`;\n\n/**\n * Call a single LLM provider to score a paper.\n * Tries up to 2 keys from the provider before giving up.\n */\nasync function callLLMForScoring(prompt, provider) {\n if (!provider.keys.length) return null;\n\n const maxAttempts = Math.min(provider.keys.length, 2);\n for (let attempt = 0; attempt < maxAttempts; attempt++) {\n const key = nextKey(provider.id, provider.keys);\n if (!key) return null;\n\n const headers = { \"Content-Type\": \"application/json\" };\n headers[provider.authHeader] = `${provider.authPrefix}${key}`;\n if (provider.extraHeaders) Object.assign(headers, provider.extraHeaders);\n\n try {\n const res = await fetch(provider.url, {\n method: \"POST\",\n headers,\n body: JSON.stringify({\n model: provider.model,\n messages: [{ role: \"user\", content: prompt }],\n max_tokens: provider.maxTokens || 512,\n temperature: 0.1,\n }),\n signal: AbortSignal.timeout(provider.timeout || 30000),\n });\n\n if (res.status === 429) {\n console.warn(`[SCORING] ${provider.name} 429 rate-limited`);\n await new Promise(r => setTimeout(r, 3000));\n continue;\n }\n if (res.status === 402) {\n console.warn(`[SCORING] ${provider.name} 402 credits exhausted — skipping provider`);\n return null;\n }\n if (!res.ok) {\n const errBody = await res.text().catch(() => \"\");\n console.warn(`[SCORING] ${provider.name} HTTP ${res.status}: ${errBody.substring(0, 150)}`);\n continue;\n }\n\n const data = await res.json();\n // Support both OpenAI format (choices[0].message.content) and Cohere v2 (message.content[] array)\n let text = \"\";\n if (provider.responseFormat === \"cohere\") {\n // Cohere v2 returns array of content blocks: [{type:\"thinking\",...}, {type:\"text\",text:\"...\"}]\n const blocks = data.message?.content || [];\n if (Array.isArray(blocks)) {\n const textBlock = blocks.find(b => b.type === \"text\");\n text = textBlock?.text || blocks[blocks.length - 1]?.text || \"\";\n } else {\n text = typeof blocks === \"string\" ? blocks : \"\";\n }\n } else if (provider.responseFormat === \"cloudflare\") {\n // Cloudflare Workers AI wraps OpenAI format: {result: {choices: [...]}, success: true}\n const inner = data.result || data;\n text = inner.choices?.[0]?.message?.content || \"\";\n } else {\n text = data.choices?.[0]?.message?.content || \"\";\n }\n\n // Strip <think>...</think> tags (Sarvam, Qwen with thinking mode)\n if (provider.stripThinkTags || text.includes(\"<think>\")) {\n // Strip closed think blocks\n text = text.replace(/<think>[\\s\\S]*?<\\/think>/g, \"\").trim();\n // Strip unclosed think blocks (model ran out of tokens mid-think)\n if (text.startsWith(\"<think>\")) {\n text = text.replace(/<think>[\\s\\S]*/g, \"\").trim();\n }\n }\n\n // Extract JSON — use balanced braces to handle nested objects like {\"score\":8,\"why\":\"...\"}\n let jsonMatch = null;\n const firstBrace = text.indexOf('{');\n if (firstBrace !== -1) {\n let depth = 0, end = -1;\n for (let i = firstBrace; i < text.length; i++) {\n if (text[i] === '{') depth++;\n else if (text[i] === '}') { depth--; if (depth === 0) { end = i; break; } }\n }\n if (end !== -1) jsonMatch = [text.substring(firstBrace, end + 1)];\n }\n if (!jsonMatch) {\n console.warn(`[SCORING] ${provider.name} — no JSON in response`);\n continue;\n }\n\n const parsed = JSON.parse(jsonMatch[0]);\n const fields = [...SECTIONS, \"novelty\", \"reproducibility\", \"citation_quality\"];\n const feedback = {};\n for (const field of fields) {\n let val = parsed[field];\n // Support both plain number and {score, why} object format\n if (val && typeof val === \"object\" && typeof val.score === \"number\") {\n feedback[field] = val.why || null;\n val = val.score;\n }\n if (typeof val !== \"number\" || val < 0 || val > 10) {\n val = typeof val === \"number\" ? Math.max(0, Math.min(10, Math.round(val))) : 5;\n }\n parsed[field] = val;\n }\n\n // Clean parsed: only keep recognized score fields (remove stray \"why\" etc.)\n const cleanScores = {};\n for (const field of fields) cleanScores[field] = parsed[field];\n\n console.log(`[SCORING] ${provider.name} scored successfully`);\n return { scores: cleanScores, provider: provider.name, feedback: Object.keys(feedback).length > 0 ? feedback : null };\n } catch (e) {\n console.warn(`[SCORING] ${provider.name} error: ${e.message}`);\n }\n }\n return null;\n}\n\n/**\n * Deterministic heuristic scoring — used when all LLMs fail.\n */\nfunction heuristicScore(content) {\n const text = content || \"\";\n const words = text.split(/\\s+/).filter(w => w.length > 0);\n const wordCount = words.length;\n const lowerText = text.toLowerCase();\n\n const sectionScores = {};\n for (const section of SECTIONS) {\n const hasSection = lowerText.includes(`## ${section}`) || lowerText.includes(`# ${section}`);\n if (!hasSection) {\n sectionScores[section] = 0;\n continue;\n }\n const sectionRegex = new RegExp(`##?\\\\s*${section}[\\\\s\\\\S]*?(?=##?\\\\s|$)`, \"i\");\n const match = text.match(sectionRegex);\n const sectionWords = match ? match[0].split(/\\s+/).length : 0;\n if (sectionWords < 20) sectionScores[section] = 2;\n else if (sectionWords < 50) sectionScores[section] = 4;\n else if (sectionWords < 100) sectionScores[section] = 5;\n else if (sectionWords < 200) sectionScores[section] = 6;\n else sectionScores[section] = 7;\n }\n\n const refMatches = text.match(/\\[\\d+\\]/g) || [];\n const uniqueRefs = new Set(refMatches).size;\n const hasPlaceholderRefs = /placeholder|author,?\\s*a\\.\\s*\\(\\d{4}\\)/i.test(text);\n const hasRealAuthors = /[A-Z][a-z]+,\\s*[A-Z]\\.\\s*(?:&|,|et al)/g.test(text);\n const hasDOI = /doi\\.org|arxiv\\.org|10\\.\\d{4}/i.test(text);\n let refScore = hasPlaceholderRefs ? 1 : Math.min(7, uniqueRefs);\n if (hasRealAuthors) refScore = Math.min(10, refScore + 1);\n if (hasDOI) refScore = Math.min(10, refScore + 1);\n sectionScores.references = refScore;\n\n const technicalTerms = (text.match(/\\b(algorithm|theorem|proof|complexity|O\\([^)]+\\)|convergence|optimal|novel|framework)\\b/gi) || []).length;\n const hasFigures = /figure \\d|fig\\.\\s*\\d|table \\d/i.test(text);\n let novelty = wordCount > 2000 ? 4 : wordCount > 1000 ? 3 : 2;\n novelty += Math.min(3, Math.floor(technicalTerms / 5));\n if (hasFigures) novelty += 1;\n\n const hasCode = /```[\\s\\S]*?```/.test(text);\n const hasEquations = /\\$[^$]+\\$/.test(text) || /\\\\begin\\{/.test(text);\n const hasNumbers = /\\d+\\.\\d+%|\\d+\\.\\d+x|p\\s*[<>]\\s*0\\.\\d/i.test(text);\n let reproducibility = (hasCode ? 5 : 3) + (hasEquations ? 1 : 0) + (wordCount > 2000 ? 1 : 0);\n if (hasNumbers) reproducibility += 1;\n\n const citation_quality = hasPlaceholderRefs ? 1 : Math.min(6, uniqueRefs);\n\n return {\n scores: {\n ...sectionScores,\n novelty: Math.min(10, novelty),\n reproducibility: Math.min(10, reproducibility),\n citation_quality: Math.min(10, citation_quality),\n },\n provider: \"heuristic\"\n };\n}\n\n/**\n * Score a paper using heterogeneous multi-LLM swarm.\n * Tries to get 2 independent judges, falls back through provider chain.\n */\nexport async function scoreGranular(content, paperType = \"research\") {\n if (!content || content.trim().length < 50) {\n return {\n sections: Object.fromEntries(SECTIONS.map(s => [s, 0])),\n overall: 0,\n novelty: 0,\n reproducibility: 0,\n citation_quality: 0,\n judges: [],\n judge_count: 0,\n error: \"Content too short to score\"\n };\n }\n\n // All LLM judges support 32k+ context. Send enough content to include ALL 7 mandatory sections.\n // Previous limit of 4000 chars truncated papers before methodology/results/conclusion, causing\n // strict judges (Cohere, Xiaomi) to correctly score missing sections as 0.\n const truncated = content.length > 16000 ? content.substring(0, 16000) + \"\\n\\n[... truncated for scoring ...]\" : content;\n const prompt = SCORING_PROMPT + truncated;\n\n // ALL available judges score independently for maximum consensus diversity.\n // Each model evaluates each section independently, then we average.\n // Run all judges in parallel for speed (each has its own timeout).\n const judgePromises = available.map(provider =>\n callLLMForScoring(prompt, provider).catch(() => null)\n );\n const judgeResults = await Promise.all(judgePromises);\n let judges = judgeResults.filter(Boolean);\n\n // Fix #2: Filter broken judges — remove any judge that gave 0 to 3+ sections\n // (indicates a parsing failure or truncated response, not a real evaluation)\n judges = judges.filter(j => {\n const zeroCount = SECTIONS.filter(s => j.scores[s] === 0).length;\n if (zeroCount >= 3) {\n console.warn(`[SCORING] Filtered broken judge ${j.judge || 'unknown'}: ${zeroCount} sections scored 0`);\n return false;\n }\n return true;\n });\n\n // If no LLM judges succeeded, use heuristic\n if (judges.length === 0) {\n const heuristic = heuristicScore(content);\n judges.push(heuristic);\n }\n\n // Average scores across all judges\n const allFields = [...SECTIONS, \"novelty\", \"reproducibility\", \"citation_quality\"];\n const averaged = {};\n for (const field of allFields) {\n const values = judges.map(j => j.scores[field]).filter(v => typeof v === \"number\");\n averaged[field] = values.length > 0\n ? Math.round((values.reduce((a, b) => a + b, 0) / values.length) * 10) / 10\n : 0;\n }\n\n // ── CALIBRATION PASS — adjust raw averages against reference benchmarks ──\n // This is the core fix for inflated scoring. Raw LLM averages are compared\n // against quality signals extracted from the paper content and calibrated\n // against recognized reference paper fingerprints.\n let calibration = null;\n try {\n const fieldResult = detectField(content);\n const signals = extractSignals(content);\n const benchmarks = REFERENCE_BENCHMARKS[fieldResult.field] || null;\n const { calibrated, adjustments } = calibrateScores(averaged, signals, benchmarks);\n\n // Apply calibrated scores over raw averages\n const adjustmentCount = Object.keys(adjustments).length;\n if (adjustmentCount > 0) {\n for (const [field, val] of Object.entries(calibrated)) {\n if (typeof val === \"number\" && averaged[field] !== undefined) {\n averaged[field] = val;\n }\n }\n console.log(`[SCORING] Calibration applied: ${adjustmentCount} adjustments, field=${fieldResult.field}, ` +\n `red_flags=${signals.red_flag_count}, depth=${signals.depth_score}`);\n }\n\n calibration = {\n field: fieldResult.field,\n field_confidence: fieldResult.confidence,\n signals_summary: {\n word_count: signals.word_count,\n sections_present: signals.sections_present.length,\n sections_missing: signals.sections_missing,\n red_flags: signals.red_flags,\n red_flag_count: signals.red_flag_count,\n has_formal_proofs: signals.has_formal_proofs,\n has_equations: signals.has_equations,\n has_code: signals.has_code,\n unique_refs: signals.unique_refs,\n has_placeholder_refs: signals.has_placeholder_refs,\n depth_score: signals.depth_score,\n evidence_markers: signals.evidence_markers,\n deception_count: signals.deception_count || 0,\n deception_matches: (signals.deception_matches || []).map(d => ({\n id: d.id, name: d.name, severity: d.severity,\n })),\n // New quality dimensions\n grammar: signals.grammar_quality ? {\n vocabulary_diversity: signals.grammar_quality.vocabulary_diversity_ttr,\n is_monotone: signals.grammar_quality.is_monotone,\n is_low_vocabulary: signals.grammar_quality.is_low_vocabulary,\n } : null,\n repetition_ratio: signals.repetition_score?.repetition_ratio || 0,\n code_quality: signals.code_quality?.blocks_found > 0 ? {\n blocks: signals.code_quality.blocks_found,\n has_real_code: signals.has_real_code || false,\n has_python: signals.code_quality.has_python || false,\n } : null,\n math_formulas: signals.math_quality?.formula_count || 0,\n lean4: signals.lean4_signals?.verification_level || \"none\",\n tables: signals.table_quality?.count || 0,\n },\n adjustments,\n adjustment_count: adjustmentCount,\n reference_papers: benchmarks ? benchmarks.references.map(r => r.title) : [],\n };\n } catch (calErr) {\n console.warn(`[SCORING] Calibration error (non-fatal): ${calErr.message}`);\n }\n\n // ── LIVE VERIFICATION PASS — real-time CrossRef, arXiv, code exec, Lean4 ──\n // Runs in parallel with independent timeouts. Non-fatal: if it fails, scoring\n // continues with just calibration. Results apply caps and bonuses to scores.\n let liveVerification = null;\n try {\n const verification = await runLiveVerification(content);\n const { adjustments: liveAdj, bonuses: liveBon } = verificationToAdjustments(verification);\n\n // ── OVERRIDE FALSE POSITIVES ──\n // If live verification confirmed code execution but calibration flagged\n // \"code_blocks_are_template_not_real\", the red_flag was a false positive.\n // Undo the -1.5 penalty applied to all dimensions by recalibrating.\n const codeExec = verification.code_execution;\n if (codeExec && codeExec.passed > 0 && calibration && calibration.signals_summary) {\n const redFlags = calibration.signals_summary.red_flags || [];\n if (redFlags.includes(\"code_blocks_are_template_not_real\")) {\n // The -1.5 penalty was applied to all 10 dimensions. Undo it.\n const allFields = [...SECTIONS, \"novelty\", \"reproducibility\", \"citation_quality\"];\n for (const field of allFields) {\n if (averaged[field] !== undefined) {\n averaged[field] = Math.min(10, Math.round((averaged[field] + 1.5) * 10) / 10);\n }\n }\n // Remove the false red flag from the report\n calibration.signals_summary.red_flags = redFlags.filter(f => f !== \"code_blocks_are_template_not_real\");\n calibration.signals_summary.red_flag_count = calibration.signals_summary.red_flags.length;\n calibration.false_positive_corrected = \"code_blocks_are_template_not_real (live verification confirmed code executes)\";\n console.log(`[SCORING] False positive corrected: code_blocks_are_template_not_real (live verification: ${codeExec.passed}/${codeExec.total} passed)`);\n }\n }\n\n // Apply caps (these override score to a max value)\n for (const [key, val] of Object.entries(liveAdj)) {\n if (key.endsWith(\"_cap\") && typeof val === \"number\") {\n const field = key.replace(\"_cap\", \"\");\n if (averaged[field] !== undefined && averaged[field] > val) {\n averaged[field] = val;\n }\n }\n }\n\n // Apply bonuses (add to score, capped at 10)\n for (const [key, val] of Object.entries(liveBon)) {\n if (key.endsWith(\"_bonus\") && typeof val === \"number\") {\n const field = key.replace(\"_bonus\", \"\");\n if (averaged[field] !== undefined) {\n averaged[field] = Math.min(10, Math.round((averaged[field] + val) * 10) / 10);\n }\n }\n }\n\n liveVerification = {\n verification_time_ms: verification.verification_time_ms,\n citations: verification.citations ? {\n total: verification.citations.total,\n verified: verification.citations.verified,\n verification_rate: verification.citations.verification_rate,\n } : null,\n novelty: verification.novelty ? {\n searched: verification.novelty.searched,\n total_found: verification.novelty.total_found,\n novelty_concern: verification.novelty.novelty_concern,\n max_similarity: verification.novelty.max_similarity,\n } : null,\n code_execution: verification.code_execution ? {\n total: verification.code_execution.total,\n passed: verification.code_execution.passed,\n failed: verification.code_execution.failed,\n } : null,\n lean4: verification.lean4 ? {\n blocks_found: verification.lean4.blocks_found,\n verified: verification.lean4.verified,\n has_unsubstantiated_claim: verification.lean4.has_unsubstantiated_claim || false,\n } : null,\n adjustments: liveAdj,\n bonuses: liveBon,\n };\n\n const adjCount = Object.keys(liveAdj).filter(k => k.endsWith(\"_cap\")).length;\n const bonCount = Object.keys(liveBon).filter(k => k.endsWith(\"_bonus\")).length;\n if (adjCount > 0 || bonCount > 0) {\n console.log(`[SCORING] Live verification: ${adjCount} caps, ${bonCount} bonuses applied (${verification.verification_time_ms}ms)`);\n }\n } catch (liveErr) {\n // Fix #3: Better error logging for live verification failures\n console.warn(`[SCORING] Live verification error (non-fatal): ${liveErr.message}`, liveErr.stack?.split('\\n').slice(0, 3).join(' | '));\n }\n\n // Fix: overall = average of ALL 10 dimensions (7 sections + novelty + reproducibility + citation_quality)\n // Previously only averaged the 7 sections, causing mismatch with displayed scores.\n const allDimensionValues = allFields.map(f => averaged[f]);\n let overall = Math.round((allDimensionValues.reduce((a, b) => a + b, 0) / allFields.length) * 10) / 10;\n\n // ── Phase F: Execution Proof Bonus ──\n // Papers with verified code blocks (execution hashes) get a purely additive overall bonus.\n // This only increases the score, never decreases. Capped at 10.\n if (liveVerification && liveVerification.bonuses && liveVerification.bonuses.execution_proof_bonus) {\n overall = Math.min(10, Math.round((overall + liveVerification.bonuses.execution_proof_bonus) * 10) / 10);\n console.log(`[SCORING] Execution proof bonus applied: +${liveVerification.bonuses.execution_proof_bonus} → overall=${overall}`);\n }\n\n // Fix #10: Fallback execution proof bonus — if live verification failed/timed out\n // but the paper contains verified execution hashes, still award the bonus.\n if (!liveVerification || !liveVerification.bonuses?.execution_proof_bonus) {\n const hashMatches = (content.match(/execution[_ ]hash[^`]*`([a-f0-9]{40,})`/gi) || []);\n if (hashMatches.length > 0) {\n const fallbackBonus = Math.min(1.5, hashMatches.length * 0.5);\n overall = Math.min(10, Math.round((overall + fallbackBonus) * 10) / 10);\n console.log(`[SCORING] Fallback execution proof bonus: ${hashMatches.length} hashes found in paper → +${fallbackBonus}`);\n if (!liveVerification) liveVerification = {};\n if (!liveVerification.bonuses) liveVerification.bonuses = {};\n liveVerification.bonuses.execution_proof_bonus = fallbackBonus;\n liveVerification.bonuses.execution_proof_note = `${hashMatches.length} execution hash(es) found in paper text (fallback detection)`;\n }\n }\n\n // Per-judge detail breakdown (individual scores + feedback)\n const judge_details = judges.map(j => ({\n judge: j.provider,\n scores: j.scores,\n feedback: j.feedback || null,\n }));\n\n // Consensus score per dimension (1 = all judges agree, 0 = total disagreement)\n const consensus = {};\n for (const field of allFields) {\n const values = judges.map(j => j.scores[field]).filter(v => typeof v === \"number\");\n if (values.length < 2) { consensus[field] = 1.0; continue; }\n const mean = values.reduce((a, b) => a + b, 0) / values.length;\n const variance = values.reduce((sum, v) => sum + (v - mean) ** 2, 0) / values.length;\n const stddev = Math.sqrt(variance);\n consensus[field] = Math.round(Math.max(0, 1 - stddev / 5) * 100) / 100;\n }\n const overall_consensus = Math.round(\n (allFields.reduce((sum, f) => sum + consensus[f], 0) / allFields.length) * 100\n ) / 100;\n\n // Aggregate feedback per dimension (from all judges that provided it)\n const aggregated_feedback = {};\n for (const field of allFields) {\n const comments = judges\n .filter(j => j.feedback && j.feedback[field])\n .map(j => ({ judge: j.provider, comment: j.feedback[field] }));\n if (comments.length > 0) aggregated_feedback[field] = comments;\n }\n\n const result = {\n sections: Object.fromEntries(SECTIONS.map(s => [s, averaged[s]])),\n overall,\n novelty: averaged.novelty,\n reproducibility: averaged.reproducibility,\n citation_quality: averaged.citation_quality,\n judges: judges.map(j => j.provider),\n judge_count: judges.length,\n judge_details,\n consensus,\n overall_consensus,\n feedback: Object.keys(aggregated_feedback).length > 0 ? aggregated_feedback : null,\n scored_at: new Date().toISOString(),\n paper_type: paperType,\n calibration,\n live_verification: liveVerification,\n };\n\n console.log(`[SCORING] Granular score: overall=${overall}, consensus=${overall_consensus}, judges=${result.judges.join(\",\")}`);\n return result;\n}\n"}, "1c5ae100e95a9bd68d19dd486acdaf25dcbd6543": {"t": 1785753919.1767867, "n": 42189, "h": "6cbcf561a5204b26", "c": "/**\n * P2PCLAW Granular Scoring Service — MAXIMUM TRIBUNAL\n * ====================================================\n * Heterogeneous multi-LLM scoring engine that evaluates papers section-by-section.\n * Updated 2026-04-04: ALL available API keys deployed for maximum judge diversity.\n *\n * Provider chain (updated 2026-04-04):\n * 1. Cerebras — qwen-3-235b-a22b (8 keys, free, ultra-fast)\n * 2. Cerebras — llama3.1-8b (8 keys, free, ultra-fast)\n * 3. Cerebras — gpt-oss-120b (8 keys, free, different model perspective)\n * 4. Cerebras — zai-glm-4.7 (8 keys, free, Chinese model perspective)\n * 5. Mistral — mistral-small-latest (3 keys, free)\n * 6. Sarvam — sarvam-m (13 keys, Indian AI, free)\n * 7. OpenRouter — qwen3-coder:free (3 keys, free)\n * 8. Groq — llama-3.3-70b-versatile (9 keys)\n * 9. NVIDIA — meta/llama-3.3-70b-instruct (3 keys, free)\n * 10. Inception — mercury-2 (10 keys, free, diffusion-based)\n * 11. Xiaomi MiMo — mimo-v2-flash (6 keys, free)\n * 12. Xiaomi MiMo — mimo-v2-pro (6 keys, free, reasoning)\n * 13. Cohere — command-a-reasoning (10 keys, reasoning model)\n * 14-22. Cloudflare — 9 accounts x unique models (GLM4, Gemma4, Nemotron, Kimi, GPT-OSS, Qwen3, Llama4Scout, MistralSmall31, DeepSeekR1)\n * 23. Cloudflare — account 10 (GLM-4.7-flash)\n * 24. Cloudflare — account 11 (Gemma-4-26b, additional account)\n * 25. NVIDIA — deepseek-ai/deepseek-v3.2 (reasoning, thinking model)\n * 26. NVIDIA — stepfun-ai/step-3.5-flash (Chinese model, reasoning)\n * 27. NVIDIA — z-ai/glm4.7 (Chinese model, thinking)\n * 28. Deterministic heuristic fallback (never blocks)\n *\n * TOTAL: 27 independent LLM judges + 1 heuristic = 28 scoring perspectives\n * ALL available judges score independently. Final score = average across all judges.\n * Each model evaluates each section independently for maximum consensus diversity.\n */\n\nimport { detectField, extractSignals, calibrateScores, REFERENCE_BENCHMARKS } from \"./calibrationService.js\";\nimport { runLiveVerification, verificationToAdjustments } from \"./liveVerificationService.js\";\n\nconst SECTIONS = [\"abstract\", \"introduction\", \"methodology\", \"results\", \"discussion\", \"conclusion\", \"references\"];\n\n// ── Load keys with rotation ─────────────────────────────────────────────────\n\nfunction loadKeys(envPrefix, maxKeys = 10) {\n const keys = [];\n for (let i = 1; i <= maxKeys; i++) {\n const k = process.env[`${envPrefix}_${i}`] || process.env[`${envPrefix}${i}`];\n if (k) keys.push(k);\n }\n // Also check single-key env vars\n const single = process.env[envPrefix];\n if (single && !keys.includes(single)) keys.unshift(single);\n return keys;\n}\n\nconst keyIndices = {};\nfunction nextKey(providerId, keys) {\n if (!keys.length) return null;\n const idx = keyIndices[providerId] || 0;\n const key = keys[idx % keys.length];\n keyIndices[providerId] = (idx + 1) % keys.length;\n return key;\n}\n\n// ── Provider definitions ────────────────────────────────────────────────────\n\nconst PROVIDERS = [\n // --- Cerebras: 3 keys x 2 models = up to 6 independent judges ---\n {\n id: \"cerebras-qwen\",\n name: \"Cerebras-Qwen235B\",\n url: \"https://api.cerebras.ai/v1/chat/completions\",\n model: \"qwen-3-235b-a22b-instruct-2507\",\n keys: loadKeys(\"CEREBRAS_API_KEY\", 15).concat(loadKeys(\"CEREBRAS_KEY\", 15)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 1024,\n },\n {\n id: \"cerebras-llama\",\n name: \"Cerebras-Llama8B\",\n url: \"https://api.cerebras.ai/v1/chat/completions\",\n model: \"llama3.1-8b\",\n keys: loadKeys(\"CEREBRAS_API_KEY\", 15).concat(loadKeys(\"CEREBRAS_KEY\", 15)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n // --- Cerebras: GPT-OSS-120B (120B param open-source model, different perspective) ---\n {\n id: \"cerebras-gptoss\",\n name: \"Cerebras-GPT-OSS-120B\",\n url: \"https://api.cerebras.ai/v1/chat/completions\",\n model: \"gpt-oss-120b\",\n keys: loadKeys(\"CEREBRAS_API_KEY\", 15).concat(loadKeys(\"CEREBRAS_KEY\", 15)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n // --- Cerebras: ZAI GLM-4.7 (Chinese model — different cultural perspective on academic rigor) ---\n {\n id: \"cerebras-glm47\",\n name: \"Cerebras-GLM4.7\",\n url: \"https://api.cerebras.ai/v1/chat/completions\",\n model: \"zai-glm-4.7\",\n keys: loadKeys(\"CEREBRAS_API_KEY\", 15).concat(loadKeys(\"CEREBRAS_KEY\", 15)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 1024,\n },\n // --- Mistral: 3 keys, reliable ---\n {\n id: \"mistral\",\n name: \"Mistral\",\n url: \"https://api.mistral.ai/v1/chat/completions\",\n model: \"mistral-small-latest\",\n keys: loadKeys(\"MISTRAL_API_KEY\", 10).concat(loadKeys(\"MISTRAL_KEY\", 10)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n // --- Sarvam (Indian AI): sarvam-m, uses <think> tags (needs 2048+ tokens), 13 keys ---\n {\n id: \"sarvam\",\n name: \"Sarvam\",\n url: \"https://api.sarvam.ai/v1/chat/completions\",\n model: \"sarvam-m\",\n keys: loadKeys(\"SARVAM_KEY\", 15).concat(loadKeys(\"SARVAM_API_KEY\", 15)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 2048,\n },\n // --- OpenRouter: free models ---\n {\n id: \"openrouter\",\n name: \"OpenRouter\",\n url: \"https://openrouter.ai/api/v1/chat/completions\",\n model: \"qwen/qwen3-coder:free\",\n keys: loadKeys(\"OPENROUTER_API_KEY\"),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n extraHeaders: { \"HTTP-Referer\": \"https://www.p2pclaw.com\", \"X-Title\": \"P2PCLAW Scoring\" },\n },\n // --- Groq: 9 keys but may be org-restricted ---\n {\n id: \"groq\",\n name: \"Groq\",\n url: \"https://api.groq.com/openai/v1/chat/completions\",\n model: \"llama-3.3-70b-versatile\",\n keys: loadKeys(\"GROQ_API_KEY\", 15).concat(loadKeys(\"GROQ_KEY\", 15)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n // --- NVIDIA: 3 keys ---\n {\n id: \"nvidia\",\n name: \"NVIDIA\",\n url: \"https://integrate.api.nvidia.com/v1/chat/completions\",\n model: \"meta/llama-3.3-70b-instruct\",\n keys: loadKeys(\"NVAPI_KEY\", 10).concat(loadKeys(\"NVIDIA_API_KEY\", 10)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n timeout: 45000,\n },\n // --- Inception: mercury-2 (diffusion-based LLM — unique scoring perspective) ---\n {\n id: \"inception\",\n name: \"Inception-Mercury2\",\n url: \"https://api.inceptionlabs.ai/v1/chat/completions\",\n model: \"mercury-2\",\n keys: loadKeys(\"INCEPTION_API_KEY\", 15).concat(loadKeys(\"INCEPTION_KEY\", 15)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 1024, // Mercury-2 needs at least 50 tokens (reasoning model)\n timeout: 45000,\n },\n // --- Xiaomi MiMo: 5 keys x 2 models = up to 2 independent judges ---\n {\n id: \"xiaomi-flash\",\n name: \"Xiaomi-MiMo-Flash\",\n url: \"https://api.xiaomimimo.com/v1/chat/completions\",\n model: \"mimo-v2-flash\",\n keys: loadKeys(\"XIAOMI_API_KEY\", 10).concat(loadKeys(\"XIAOMI_KEY\", 10)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n {\n id: \"xiaomi-pro\",\n name: \"Xiaomi-MiMo-Pro\",\n url: \"https://api.xiaomimimo.com/v1/chat/completions\",\n model: \"mimo-v2-pro\",\n keys: loadKeys(\"XIAOMI_API_KEY\", 10).concat(loadKeys(\"XIAOMI_KEY\", 10)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 1024,\n },\n // --- Cohere: command-a-reasoning model (9 keys, reasoning/thinking model) ---\n {\n id: \"cohere\",\n name: \"Cohere-CommandA\",\n url: \"https://api.cohere.com/v2/chat\",\n model: \"command-a-reasoning-08-2025\",\n keys: loadKeys(\"COHERE_API_KEY\", 15).concat(loadKeys(\"COHERE_KEY\", 15)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 4096, // reasoning model needs extra tokens for thinking + answer\n responseFormat: \"cohere\", // data.message.content[] array with {type:\"thinking\"} + {type:\"text\"}\n timeout: 90000, // 90s — reasoning model needs time to think through all 10 dimensions\n },\n // --- Cloudflare Workers AI: 6 models across 6 accounts (all FREE) ---\n // Ordered by model quality: best first\n {\n id: \"cloudflare-glm\",\n name: \"Cloudflare-GLM4\",\n url: `https://api.cloudflare.com/client/v4/accounts/eaffd2b52c95c69aaad8d859e9dcb52b/ai/run/@cf/zai-org/glm-4.7-flash`,\n model: \"@cf/zai-org/glm-4.7-flash\",\n keys: loadKeys(\"CF_AI_TOKEN\").concat(loadKeys(\"CLOUDFLARE_AI_TOKEN\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 2048,\n responseFormat: \"cloudflare\",\n timeout: 60000,\n },\n {\n id: \"cloudflare-gemma4\",\n name: \"Cloudflare-Gemma4\",\n url: `https://api.cloudflare.com/client/v4/accounts/a7995d3f33b6ba57955749337c9abbe0/ai/run/@cf/google/gemma-4-26b-a4b-it`,\n model: \"@cf/google/gemma-4-26b-a4b-it\",\n keys: loadKeys(\"CF_AI_TOKEN_2\"),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 1024,\n responseFormat: \"cloudflare\",\n timeout: 60000,\n },\n {\n id: \"cloudflare-nemotron\",\n name: \"Cloudflare-Nemotron\",\n url: `https://api.cloudflare.com/client/v4/accounts/194d9aea21482ac893ed81fc6b004864/ai/run/@cf/nvidia/nemotron-3-120b-a12b`,\n model: \"@cf/nvidia/nemotron-3-120b-a12b\",\n keys: loadKeys(\"CF_AI_TOKEN_3\"),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 1024,\n responseFormat: \"cloudflare\",\n timeout: 60000,\n },\n {\n id: \"cloudflare-kimi\",\n name: \"Cloudflare-Kimi\",\n url: `https://api.cloudflare.com/client/v4/accounts/401a75ead25275262c1c05eecb7a997c/ai/run/@cf/moonshotai/kimi-k2.5`,\n model: \"@cf/moonshotai/kimi-k2.5\",\n keys: loadKeys(\"CF_AI_TOKEN_4\"),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 2048,\n responseFormat: \"cloudflare\",\n timeout: 60000,\n },\n {\n id: \"cloudflare-gptoss\",\n name: \"Cloudflare-GPT-OSS\",\n url: `https://api.cloudflare.com/client/v4/accounts/73340519f6430362daee759ba0b48ce8/ai/run/@cf/openai/gpt-oss-120b`,\n model: \"@cf/openai/gpt-oss-120b\",\n keys: loadKeys(\"CF_AI_TOKEN_5\"),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 1024,\n responseFormat: \"cloudflare\",\n timeout: 60000,\n },\n {\n id: \"cloudflare-qwen3\",\n name: \"Cloudflare-Qwen3\",\n url: `https://api.cloudflare.com/client/v4/accounts/df4a7888befcbb6ce3e0a0b346ea1990/ai/run/@cf/qwen/qwen3-30b-a3b-fp8`,\n model: \"@cf/qwen/qwen3-30b-a3b-fp8\",\n keys: loadKeys(\"CF_AI_TOKEN_6\"),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 2048,\n responseFormat: \"cloudflare\",\n timeout: 60000,\n },\n // --- Cloudflare Workers AI: 3 NEW accounts (accounts 7/8/9) ---\n {\n id: \"cloudflare-llama4scout\",\n name: \"Cloudflare-Llama4Scout\",\n url: `https://api.cloudflare.com/client/v4/accounts/3cd084561890e5ab468456fae547ded0/ai/run/@cf/meta/llama-4-scout-17b-16e-instruct`,\n model: \"@cf/meta/llama-4-scout-17b-16e-instruct\",\n keys: loadKeys(\"CF_AI_TOKEN_7\"),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 1024,\n responseFormat: \"cloudflare\",\n timeout: 60000,\n },\n {\n id: \"cloudflare-mistral31\",\n name: \"Cloudflare-MistralSmall31\",\n url: `https://api.cloudflare.com/client/v4/accounts/27920eccf7d83f7ee267130cd6018eaf/ai/run/@cf/mistralai/mistral-small-3.1-24b-instruct`,\n model: \"@cf/mistralai/mistral-small-3.1-24b-instruct\",\n keys: loadKeys(\"CF_AI_TOKEN_8\"),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 1024,\n responseFormat: \"cloudflare\",\n timeout: 60000,\n },\n {\n id: \"cloudflare-deepseekr1\",\n name: \"Cloudflare-DeepSeekR1\",\n url: `https://api.cloudflare.com/client/v4/accounts/60c2dcaa7fc3377f036114648f6397ba/ai/run/@cf/deepseek-ai/deepseek-r1-distill-qwen-32b`,\n model: \"@cf/deepseek-ai/deepseek-r1-distill-qwen-32b\",\n keys: loadKeys(\"CF_AI_TOKEN_9\"),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 2048,\n responseFormat: \"cloudflare\",\n timeout: 60000,\n },\n // --- Cloudflare Workers AI: Account 10 (Agnuxo2026, GLM-4.7-flash) ---\n {\n id: \"cloudflare-glm47-10\",\n name: \"Cloudflare-GLM47-Acct10\",\n url: `https://api.cloudflare.com/client/v4/accounts/${process.env.CF_ACCOUNT_ID_10 || \"00c5ebf4df46d16450d5f1419dc36c6a\"}/ai/run/@cf/zai-org/glm-4.7-flash`,\n model: \"@cf/zai-org/glm-4.7-flash\",\n keys: loadKeys(\"CF_AI_TOKEN_10\"),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 2048,\n responseFormat: \"cloudflare\",\n timeout: 60000,\n },\n // --- Cloudflare Workers AI: Account 11 (agnuxo300@zohomail.eu, Gemma-4-26b) ---\n {\n id: \"cloudflare-gemma4-11\",\n name: \"Cloudflare-Gemma4-Acct11\",\n url: `https://api.cloudflare.com/client/v4/accounts/${process.env.CF_ACCOUNT_ID_11 || \"85fbdab1851209cbd99773a758831fc0\"}/ai/run/@cf/google/gemma-4-26b-a4b-it`,\n model: \"@cf/google/gemma-4-26b-a4b-it\",\n keys: loadKeys(\"CF_AI_TOKEN_11\"),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 1024,\n responseFormat: \"cloudflare\",\n timeout: 60000,\n },\n // --- NVIDIA: DeepSeek-V3.2 (reasoning model with thinking) ---\n {\n id: \"nvidia-deepseek-v3\",\n name: \"NVIDIA-DeepSeekV3.2\",\n url: \"https://integrate.api.nvidia.com/v1/chat/completions\",\n model: \"deepseek-ai/deepseek-v3.2\",\n keys: loadKeys(\"NVAPI_KEY\", 10).concat(loadKeys(\"NVIDIA_API_KEY\", 10)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 2048,\n timeout: 60000,\n },\n // --- NVIDIA: StepFun Step-3.5-Flash (Chinese reasoning model) ---\n {\n id: \"nvidia-stepfun\",\n name: \"NVIDIA-StepFun3.5\",\n url: \"https://integrate.api.nvidia.com/v1/chat/completions\",\n model: \"stepfun-ai/step-3.5-flash\",\n keys: loadKeys(\"NVAPI_KEY\", 10).concat(loadKeys(\"NVIDIA_API_KEY\", 10)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 2048,\n timeout: 60000,\n },\n // --- NVIDIA: GLM-4.7 (Z.ai Chinese model with thinking) ---\n {\n id: \"nvidia-glm47\",\n name: \"NVIDIA-GLM4.7\",\n url: \"https://integrate.api.nvidia.com/v1/chat/completions\",\n model: \"z-ai/glm4.7\",\n keys: loadKeys(\"NVAPI_KEY\", 10).concat(loadKeys(\"NVIDIA_API_KEY\", 10)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 2048,\n timeout: 60000,\n },\n];\n\n// Deduplicate keys within each provider\nfor (const p of PROVIDERS) {\n p.keys = [...new Set(p.keys)].filter(Boolean);\n}\n\n// Log available providers\nconst available = PROVIDERS.filter(p => p.keys.length > 0);\nconsole.log(`[SCORING] ${available.length} LLM providers available: ${available.map(p => `${p.name}(${p.keys.length})`).join(\", \")}`);\nif (available.length === 0) console.warn(\"[SCORING] No LLM providers — heuristic scoring only.\");\n// Debug: log specifically which providers have NO keys\nconst unavailable = PROVIDERS.filter(p => p.keys.length === 0);\nif (unavailable.length > 0) console.log(`[SCORING] Providers with NO keys: ${unavailable.map(p => p.name).join(\", \")}`);\n\nconst SCORING_PROMPT = `You are a STRICT academic peer reviewer. You evaluate papers for the P2PCLAW benchmark.\n\nYour scoring must follow real academic standards. Most papers score 4-6. Only exceptional work reaches 7+. A score of 8+ means top-tier venue quality (NeurIPS, Nature, ICML). A 10 is historically significant (Turing Award level).\n\nCALIBRATION ANCHORS — use these to calibrate your scores:\n- 10/10 novelty: \"Attention Is All You Need\" (Vaswani 2017), Bitcoin whitepaper (Nakamoto 2008)\n- 8/10 novelty: A genuine new algorithm with proven improvements over SOTA\n- 6/10 novelty: Meaningful extension of existing work with some original insights\n- 5/10 novelty: Applying known techniques to a new domain (standard contribution)\n- 3/10 novelty: Minor variation of existing work, obvious next step\n- 1/10 novelty: Restating known results with different notation\n\nScore each criterion on 0-10:\n- abstract (0-10): Clarity, completeness, problem+scope+results summarized\n- introduction (0-10): Problem clarity, context, motivation. Needs 2+ real citations to related work\n- methodology (0-10): Rigor and reproducibility. Can someone replicate this exactly?\n- results (0-10): Strength of evidence. Real data, real experiments, statistical significance\n- discussion (0-10): Honest limitations, implications, comparison to prior work\n- conclusion (0-10): Summary of findings, concrete future directions\n- references (0-10): Real citations with authors, titles, years, DOIs. 8+ unique for score >5\n- novelty (0-10): TRUE original contribution. Applying a standard formula to a new domain = 4-5, NOT 8-9\n- reproducibility (0-10): Code, equations, parameters, data availability\n- citation_quality (0-10): Are references real, verified, and actually cited in the text?\n\nSTRICT RULES:\n- Missing section = 0 for that section\n- Placeholder/fake references = references score 1\n- Papers under 300 words = all scores below 3\n- Proving obvious things = novelty 0-1\n- Standard variance/mean/known formulas applied to new domain = novelty 4-5 MAX\n- No experimental data or only synthetic/estimated data = results 3-4 MAX\n- Self-referential citations or citing only own work = citation_quality 2-3\n- Circular reasoning (using conclusion to justify methodology) = methodology cap 4\n- Claims without evidence (e.g., \"we achieve SOTA\" with no comparison) = results cap 3\n- DO NOT give 8+ unless the work would genuinely be accepted at a top venue\n\nReturn ONLY this JSON (numbers 0-10):\n{\"abstract\":N,\"introduction\":N,\"methodology\":N,\"results\":N,\"discussion\":N,\"conclusion\":N,\"references\":N,\"novelty\":N,\"reproducibility\":N,\"citation_quality\":N}\n\nPaper content:\n`;\n\n/**\n * Call a single LLM provider to score a paper.\n * Tries up to 2 keys from the provider before giving up.\n */\nasync function callLLMForScoring(prompt, provider) {\n if (!provider.keys.length) return null;\n\n const maxAttempts = Math.min(provider.keys.length, 2);\n for (let attempt = 0; attempt < maxAttempts; attempt++) {\n const key = nextKey(provider.id, provider.keys);\n if (!key) return null;\n\n const headers = { \"Content-Type\": \"application/json\" };\n headers[provider.authHeader] = `${provider.authPrefix}${key}`;\n if (provider.extraHeaders) Object.assign(headers, provider.extraHeaders);\n\n try {\n const res = await fetch(provider.url, {\n method: \"POST\",\n headers,\n body: JSON.stringify({\n model: provider.model,\n messages: [{ role: \"user\", content: prompt }],\n max_tokens: provider.maxTokens || 512,\n temperature: 0.1,\n }),\n signal: AbortSignal.timeout(provider.timeout || 30000),\n });\n\n if (res.status === 429) {\n console.warn(`[SCORING] ${provider.name} 429 rate-limited`);\n await new Promise(r => setTimeout(r, 3000));\n continue;\n }\n if (res.status === 402) {\n console.warn(`[SCORING] ${provider.name} 402 credits exhausted — skipping provider`);\n return null;\n }\n if (!res.ok) {\n const errBody = await res.text().catch(() => \"\");\n console.warn(`[SCORING] ${provider.name} HTTP ${res.status}: ${errBody.substring(0, 150)}`);\n continue;\n }\n\n const data = await res.json();\n // Support both OpenAI format (choices[0].message.content) and Cohere v2 (message.content[] array)\n let text = \"\";\n if (provider.responseFormat === \"cohere\") {\n // Cohere v2 returns array of content blocks: [{type:\"thinking\",...}, {type:\"text\",text:\"...\"}]\n const blocks = data.message?.content || [];\n if (Array.isArray(blocks)) {\n const textBlock = blocks.find(b => b.type === \"text\");\n text = textBlock?.text || blocks[blocks.length - 1]?.text || \"\";\n } else {\n text = typeof blocks === \"string\" ? blocks : \"\";\n }\n } else if (provider.responseFormat === \"cloudflare\") {\n // Cloudflare Workers AI wraps OpenAI format: {result: {choices: [...]}, success: true}\n const inner = data.result || data;\n text = inner.choices?.[0]?.message?.content || \"\";\n } else {\n text = data.choices?.[0]?.message?.content || \"\";\n }\n\n // Strip <think>...</think> tags (Sarvam, Qwen with thinking mode)\n if (provider.stripThinkTags || text.includes(\"<think>\")) {\n // Strip closed think blocks\n text = text.replace(/<think>[\\s\\S]*?<\\/think>/g, \"\").trim();\n // Strip unclosed think blocks (model ran out of tokens mid-think)\n if (text.startsWith(\"<think>\")) {\n text = text.replace(/<think>[\\s\\S]*/g, \"\").trim();\n }\n }\n\n // Extract JSON — use balanced braces to handle nested objects like {\"score\":8,\"why\":\"...\"}\n let jsonMatch = null;\n const firstBrace = text.indexOf('{');\n if (firstBrace !== -1) {\n let depth = 0, end = -1;\n for (let i = firstBrace; i < text.length; i++) {\n if (text[i] === '{') depth++;\n else if (text[i] === '}') { depth--; if (depth === 0) { end = i; break; } }\n }\n if (end !== -1) jsonMatch = [text.substring(firstBrace, end + 1)];\n }\n if (!jsonMatch) {\n console.warn(`[SCORING] ${provider.name} — no JSON in response`);\n continue;\n }\n\n const parsed = JSON.parse(jsonMatch[0]);\n const fields = [...SECTIONS, \"novelty\", \"reproducibility\", \"citation_quality\"];\n const feedback = {};\n for (const field of fields) {\n let val = parsed[field];\n // Support both plain number and {score, why} object format\n if (val && typeof val === \"object\" && typeof val.score === \"number\") {\n feedback[field] = val.why || null;\n val = val.score;\n }\n if (typeof val !== \"number\" || val < 0 || val > 10) {\n val = typeof val === \"number\" ? Math.max(0, Math.min(10, Math.round(val))) : 5;\n }\n parsed[field] = val;\n }\n\n // Clean parsed: only keep recognized score fields (remove stray \"why\" etc.)\n const cleanScores = {};\n for (const field of fields) cleanScores[field] = parsed[field];\n\n console.log(`[SCORING] ${provider.name} scored successfully`);\n return { scores: cleanScores, provider: provider.name, feedback: Object.keys(feedback).length > 0 ? feedback : null };\n } catch (e) {\n console.warn(`[SCORING] ${provider.name} error: ${e.message}`);\n }\n }\n return null;\n}\n\n/**\n * Deterministic heuristic scoring — used when all LLMs fail.\n */\nfunction heuristicScore(content) {\n const text = content || \"\";\n const words = text.split(/\\s+/).filter(w => w.length > 0);\n const wordCount = words.length;\n const lowerText = text.toLowerCase();\n\n const sectionScores = {};\n for (const section of SECTIONS) {\n const hasSection = lowerText.includes(`## ${section}`) || lowerText.includes(`# ${section}`);\n if (!hasSection) {\n sectionScores[section] = 0;\n continue;\n }\n const sectionRegex = new RegExp(`##?\\\\s*${section}[\\\\s\\\\S]*?(?=##?\\\\s|$)`, \"i\");\n const match = text.match(sectionRegex);\n const sectionWords = match ? match[0].split(/\\s+/).length : 0;\n if (sectionWords < 20) sectionScores[section] = 2;\n else if (sectionWords < 50) sectionScores[section] = 4;\n else if (sectionWords < 100) sectionScores[section] = 5;\n else if (sectionWords < 200) sectionScores[section] = 6;\n else sectionScores[section] = 7;\n }\n\n const refMatches = text.match(/\\[\\d+\\]/g) || [];\n const uniqueRefs = new Set(refMatches).size;\n const hasPlaceholderRefs = /placeholder|author,?\\s*a\\.\\s*\\(\\d{4}\\)/i.test(text);\n const hasRealAuthors = /[A-Z][a-z]+,\\s*[A-Z]\\.\\s*(?:&|,|et al)/g.test(text);\n const hasDOI = /doi\\.org|arxiv\\.org|10\\.\\d{4}/i.test(text);\n let refScore = hasPlaceholderRefs ? 1 : Math.min(7, uniqueRefs);\n if (hasRealAuthors) refScore = Math.min(10, refScore + 1);\n if (hasDOI) refScore = Math.min(10, refScore + 1);\n sectionScores.references = refScore;\n\n const technicalTerms = (text.match(/\\b(algorithm|theorem|proof|complexity|O\\([^)]+\\)|convergence|optimal|novel|framework)\\b/gi) || []).length;\n const hasFigures = /figure \\d|fig\\.\\s*\\d|table \\d/i.test(text);\n let novelty = wordCount > 2000 ? 4 : wordCount > 1000 ? 3 : 2;\n novelty += Math.min(3, Math.floor(technicalTerms / 5));\n if (hasFigures) novelty += 1;\n\n const hasCode = /```[\\s\\S]*?```/.test(text);\n const hasEquations = /\\$[^$]+\\$/.test(text) || /\\\\begin\\{/.test(text);\n const hasNumbers = /\\d+\\.\\d+%|\\d+\\.\\d+x|p\\s*[<>]\\s*0\\.\\d/i.test(text);\n let reproducibility = (hasCode ? 5 : 3) + (hasEquations ? 1 : 0) + (wordCount > 2000 ? 1 : 0);\n if (hasNumbers) reproducibility += 1;\n\n const citation_quality = hasPlaceholderRefs ? 1 : Math.min(6, uniqueRefs);\n\n return {\n scores: {\n ...sectionScores,\n novelty: Math.min(10, novelty),\n reproducibility: Math.min(10, reproducibility),\n citation_quality: Math.min(10, citation_quality),\n },\n provider: \"heuristic\"\n };\n}\n\n/**\n * Score a paper using heterogeneous multi-LLM swarm.\n * Tries to get 2 independent judges, falls back through provider chain.\n */\nexport async function scoreGranular(content, paperType = \"research\") {\n if (!content || content.trim().length < 50) {\n return {\n sections: Object.fromEntries(SECTIONS.map(s => [s, 0])),\n overall: 0,\n novelty: 0,\n reproducibility: 0,\n citation_quality: 0,\n judges: [],\n judge_count: 0,\n error: \"Content too short to score\"\n };\n }\n\n // All LLM judges support 32k+ context. Send enough content to include ALL 7 mandatory sections.\n // Previous limit of 4000 chars truncated papers before methodology/results/conclusion, causing\n // strict judges (Cohere, Xiaomi) to correctly score missing sections as 0.\n const truncated = content.length > 16000 ? content.substring(0, 16000) + \"\\n\\n[... truncated for scoring ...]\" : content;\n const prompt = SCORING_PROMPT + truncated;\n\n // ALL available judges score independently for maximum consensus diversity.\n // Each model evaluates each section independently, then we average.\n // Run all judges in parallel for speed (each has its own timeout).\n const judgePromises = available.map(provider =>\n callLLMForScoring(prompt, provider).catch(() => null)\n );\n const judgeResults = await Promise.all(judgePromises);\n let judges = judgeResults.filter(Boolean);\n\n // Fix #2: Filter broken judges — remove any judge that gave 0 to 3+ sections\n // (indicates a parsing failure or truncated response, not a real evaluation)\n judges = judges.filter(j => {\n const zeroCount = SECTIONS.filter(s => j.scores[s] === 0).length;\n if (zeroCount >= 3) {\n console.warn(`[SCORING] Filtered broken judge ${j.judge || 'unknown'}: ${zeroCount} sections scored 0`);\n return false;\n }\n return true;\n });\n\n // If no LLM judges succeeded, use heuristic\n if (judges.length === 0) {\n const heuristic = heuristicScore(content);\n judges.push(heuristic);\n }\n\n // Average scores across all judges\n const allFields = [...SECTIONS, \"novelty\", \"reproducibility\", \"citation_quality\"];\n const averaged = {};\n for (const field of allFields) {\n const values = judges.map(j => j.scores[field]).filter(v => typeof v === \"number\");\n averaged[field] = values.length > 0\n ? Math.round((values.reduce((a, b) => a + b, 0) / values.length) * 10) / 10\n : 0;\n }\n\n // ── CALIBRATION PASS — adjust raw averages against reference benchmarks ──\n // This is the core fix for inflated scoring. Raw LLM averages are compared\n // against quality signals extracted from the paper content and calibrated\n // against recognized reference paper fingerprints.\n let calibration = null;\n try {\n const fieldResult = detectField(content);\n const signals = extractSignals(content);\n const benchmarks = REFERENCE_BENCHMARKS[fieldResult.field] || null;\n const { calibrated, adjustments } = calibrateScores(averaged, signals, benchmarks);\n\n // Apply calibrated scores over raw averages\n const adjustmentCount = Object.keys(adjustments).length;\n if (adjustmentCount > 0) {\n for (const [field, val] of Object.entries(calibrated)) {\n if (typeof val === \"number\" && averaged[field] !== undefined) {\n averaged[field] = val;\n }\n }\n console.log(`[SCORING] Calibration applied: ${adjustmentCount} adjustments, field=${fieldResult.field}, ` +\n `red_flags=${signals.red_flag_count}, depth=${signals.depth_score}`);\n }\n\n calibration = {\n field: fieldResult.field,\n field_confidence: fieldResult.confidence,\n signals_summary: {\n word_count: signals.word_count,\n sections_present: signals.sections_present.length,\n sections_missing: signals.sections_missing,\n red_flags: signals.red_flags,\n red_flag_count: signals.red_flag_count,\n has_formal_proofs: signals.has_formal_proofs,\n has_equations: signals.has_equations,\n has_code: signals.has_code,\n unique_refs: signals.unique_refs,\n has_placeholder_refs: signals.has_placeholder_refs,\n depth_score: signals.depth_score,\n evidence_markers: signals.evidence_markers,\n deception_count: signals.deception_count || 0,\n deception_matches: (signals.deception_matches || []).map(d => ({\n id: d.id, name: d.name, severity: d.severity,\n })),\n // New quality dimensions\n grammar: signals.grammar_quality ? {\n vocabulary_diversity: signals.grammar_quality.vocabulary_diversity_ttr,\n is_monotone: signals.grammar_quality.is_monotone,\n is_low_vocabulary: signals.grammar_quality.is_low_vocabulary,\n } : null,\n repetition_ratio: signals.repetition_score?.repetition_ratio || 0,\n code_quality: signals.code_quality?.blocks_found > 0 ? {\n blocks: signals.code_quality.blocks_found,\n has_real_code: signals.has_real_code || false,\n has_python: signals.code_quality.has_python || false,\n } : null,\n math_formulas: signals.math_quality?.formula_count || 0,\n lean4: signals.lean4_signals?.verification_level || \"none\",\n tables: signals.table_quality?.count || 0,\n },\n adjustments,\n adjustment_count: adjustmentCount,\n reference_papers: benchmarks ? benchmarks.references.map(r => r.title) : [],\n };\n } catch (calErr) {\n console.warn(`[SCORING] Calibration error (non-fatal): ${calErr.message}`);\n }\n\n // ── LIVE VERIFICATION PASS — real-time CrossRef, arXiv, code exec, Lean4 ──\n // Runs in parallel with independent timeouts. Non-fatal: if it fails, scoring\n // continues with just calibration. Results apply caps and bonuses to scores.\n let liveVerification = null;\n try {\n const verification = await runLiveVerification(content);\n const { adjustments: liveAdj, bonuses: liveBon } = verificationToAdjustments(verification);\n\n // ── OVERRIDE FALSE POSITIVES ──\n // If live verification confirmed code execution but calibration flagged\n // \"code_blocks_are_template_not_real\", the red_flag was a false positive.\n // Undo the -1.5 penalty applied to all dimensions by recalibrating.\n const codeExec = verification.code_execution;\n if (codeExec && codeExec.passed > 0 && calibration && calibration.signals_summary) {\n const redFlags = calibration.signals_summary.red_flags || [];\n if (redFlags.includes(\"code_blocks_are_template_not_real\")) {\n // The -1.5 penalty was applied to all 10 dimensions. Undo it.\n const allFields = [...SECTIONS, \"novelty\", \"reproducibility\", \"citation_quality\"];\n for (const field of allFields) {\n if (averaged[field] !== undefined) {\n averaged[field] = Math.min(10, Math.round((averaged[field] + 1.5) * 10) / 10);\n }\n }\n // Remove the false red flag from the report\n calibration.signals_summary.red_flags = redFlags.filter(f => f !== \"code_blocks_are_template_not_real\");\n calibration.signals_summary.red_flag_count = calibration.signals_summary.red_flags.length;\n calibration.false_positive_corrected = \"code_blocks_are_template_not_real (live verification confirmed code executes)\";\n console.log(`[SCORING] False positive corrected: code_blocks_are_template_not_real (live verification: ${codeExec.passed}/${codeExec.total} passed)`);\n }\n }\n\n // Apply caps (these override score to a max value)\n for (const [key, val] of Object.entries(liveAdj)) {\n if (key.endsWith(\"_cap\") && typeof val === \"number\") {\n const field = key.replace(\"_cap\", \"\");\n if (averaged[field] !== undefined && averaged[field] > val) {\n averaged[field] = val;\n }\n }\n }\n\n // Apply bonuses (add to score, capped at 10)\n for (const [key, val] of Object.entries(liveBon)) {\n if (key.endsWith(\"_bonus\") && typeof val === \"number\") {\n const field = key.replace(\"_bonus\", \"\");\n if (averaged[field] !== undefined) {\n averaged[field] = Math.min(10, Math.round((averaged[field] + val) * 10) / 10);\n }\n }\n }\n\n liveVerification = {\n verification_time_ms: verification.verification_time_ms,\n citations: verification.citations ? {\n total: verification.citations.total,\n verified: verification.citations.verified,\n verification_rate: verification.citations.verification_rate,\n } : null,\n novelty: verification.novelty ? {\n searched: verification.novelty.searched,\n total_found: verification.novelty.total_found,\n novelty_concern: verification.novelty.novelty_concern,\n max_similarity: verification.novelty.max_similarity,\n } : null,\n code_execution: verification.code_execution ? {\n total: verification.code_execution.total,\n passed: verification.code_execution.passed,\n failed: verification.code_execution.failed,\n } : null,\n lean4: verification.lean4 ? {\n blocks_found: verification.lean4.blocks_found,\n verified: verification.lean4.verified,\n has_unsubstantiated_claim: verification.lean4.has_unsubstantiated_claim || false,\n } : null,\n adjustments: liveAdj,\n bonuses: liveBon,\n };\n\n const adjCount = Object.keys(liveAdj).filter(k => k.endsWith(\"_cap\")).length;\n const bonCount = Object.keys(liveBon).filter(k => k.endsWith(\"_bonus\")).length;\n if (adjCount > 0 || bonCount > 0) {\n console.log(`[SCORING] Live verification: ${adjCount} caps, ${bonCount} bonuses applied (${verification.verification_time_ms}ms)`);\n }\n } catch (liveErr) {\n // Fix #3: Better error logging for live verification failures\n console.warn(`[SCORING] Live verification error (non-fatal): ${liveErr.message}`, liveErr.stack?.split('\\n').slice(0, 3).join(' | '));\n }\n\n const sectionValues = SECTIONS.map(s => averaged[s]);\n let overall = Math.round((sectionValues.reduce((a, b) => a + b, 0) / SECTIONS.length) * 10) / 10;\n\n // ── Phase F: Execution Proof Bonus ──\n // Papers with verified code blocks (execution hashes) get a purely additive overall bonus.\n // This only increases the score, never decreases. Capped at 10.\n if (liveVerification && liveVerification.bonuses && liveVerification.bonuses.execution_proof_bonus) {\n overall = Math.min(10, Math.round((overall + liveVerification.bonuses.execution_proof_bonus) * 10) / 10);\n console.log(`[SCORING] Execution proof bonus applied: +${liveVerification.bonuses.execution_proof_bonus} → overall=${overall}`);\n }\n\n // Fix #10: Fallback execution proof bonus — if live verification failed/timed out\n // but the paper contains verified execution hashes, still award the bonus.\n if (!liveVerification || !liveVerification.bonuses?.execution_proof_bonus) {\n const hashMatches = (content.match(/execution[_ ]hash[^`]*`([a-f0-9]{40,})`/gi) || []);\n if (hashMatches.length > 0) {\n const fallbackBonus = Math.min(1.5, hashMatches.length * 0.5);\n overall = Math.min(10, Math.round((overall + fallbackBonus) * 10) / 10);\n console.log(`[SCORING] Fallback execution proof bonus: ${hashMatches.length} hashes found in paper → +${fallbackBonus}`);\n if (!liveVerification) liveVerification = {};\n if (!liveVerification.bonuses) liveVerification.bonuses = {};\n liveVerification.bonuses.execution_proof_bonus = fallbackBonus;\n liveVerification.bonuses.execution_proof_note = `${hashMatches.length} execution hash(es) found in paper text (fallback detection)`;\n }\n }\n\n // Per-judge detail breakdown (individual scores + feedback)\n const judge_details = judges.map(j => ({\n judge: j.provider,\n scores: j.scores,\n feedback: j.feedback || null,\n }));\n\n // Consensus score per dimension (1 = all judges agree, 0 = total disagreement)\n const consensus = {};\n for (const field of allFields) {\n const values = judges.map(j => j.scores[field]).filter(v => typeof v === \"number\");\n if (values.length < 2) { consensus[field] = 1.0; continue; }\n const mean = values.reduce((a, b) => a + b, 0) / values.length;\n const variance = values.reduce((sum, v) => sum + (v - mean) ** 2, 0) / values.length;\n const stddev = Math.sqrt(variance);\n consensus[field] = Math.round(Math.max(0, 1 - stddev / 5) * 100) / 100;\n }\n const overall_consensus = Math.round(\n (allFields.reduce((sum, f) => sum + consensus[f], 0) / allFields.length) * 100\n ) / 100;\n\n // Aggregate feedback per dimension (from all judges that provided it)\n const aggregated_feedback = {};\n for (const field of allFields) {\n const comments = judges\n .filter(j => j.feedback && j.feedback[field])\n .map(j => ({ judge: j.provider, comment: j.feedback[field] }));\n if (comments.length > 0) aggregated_feedback[field] = comments;\n }\n\n const result = {\n sections: Object.fromEntries(SECTIONS.map(s => [s, averaged[s]])),\n overall,\n novelty: averaged.novelty,\n reproducibility: averaged.reproducibility,\n citation_quality: averaged.citation_quality,\n judges: judges.map(j => j.provider),\n judge_count: judges.length,\n judge_details,\n consensus,\n overall_consensus,\n feedback: Object.keys(aggregated_feedback).length > 0 ? aggregated_feedback : null,\n scored_at: new Date().toISOString(),\n paper_type: paperType,\n calibration,\n live_verification: liveVerification,\n };\n\n console.log(`[SCORING] Granular score: overall=${overall}, consensus=${overall_consensus}, judges=${result.judges.join(\",\")}`);\n return result;\n}\n"}, "239275d48d79a29528d86c5b0837275021685d8f": {"t": 1785753921.2526736, "n": 39883, "h": "63cf570c90f1cddf", "c": "/**\n * P2PCLAW Granular Scoring Service — MAXIMUM TRIBUNAL\n * ====================================================\n * Heterogeneous multi-LLM scoring engine that evaluates papers section-by-section.\n * Updated 2026-04-04: ALL available API keys deployed for maximum judge diversity.\n *\n * Provider chain (updated 2026-04-04):\n * 1. Cerebras — qwen-3-235b-a22b (8 keys, free, ultra-fast)\n * 2. Cerebras — llama3.1-8b (8 keys, free, ultra-fast)\n * 3. Cerebras — gpt-oss-120b (8 keys, free, different model perspective)\n * 4. Cerebras — zai-glm-4.7 (8 keys, free, Chinese model perspective)\n * 5. Mistral — mistral-small-latest (3 keys, free)\n * 6. Sarvam — sarvam-m (13 keys, Indian AI, free)\n * 7. OpenRouter — qwen3-coder:free (3 keys, free)\n * 8. Groq — llama-3.3-70b-versatile (9 keys)\n * 9. NVIDIA — meta/llama-3.3-70b-instruct (3 keys, free)\n * 10. Inception — mercury-2 (9 keys, free, diffusion-based)\n * 11. Xiaomi MiMo — mimo-v2-flash (5 keys, free)\n * 12. Xiaomi MiMo — mimo-v2-pro (5 keys, free, reasoning)\n * 13. Cohere — command-a-reasoning (9 keys, reasoning model)\n * 14-22. Cloudflare — 9 accounts x unique models (GLM4, Gemma4, Nemotron, Kimi, GPT-OSS, Qwen3, Llama4Scout, MistralSmall31, DeepSeekR1)\n * 23. Cloudflare — account 10 (GLM-4.7-flash, additional account)\n * 24. Deterministic heuristic fallback (never blocks)\n *\n * TOTAL: 23 independent LLM judges + 1 heuristic = 24 scoring perspectives\n * ALL available judges score independently. Final score = average across all judges.\n * Each model evaluates each section independently for maximum consensus diversity.\n */\n\nimport { detectField, extractSignals, calibrateScores, REFERENCE_BENCHMARKS } from \"./calibrationService.js\";\nimport { runLiveVerification, verificationToAdjustments } from \"./liveVerificationService.js\";\n\nconst SECTIONS = [\"abstract\", \"introduction\", \"methodology\", \"results\", \"discussion\", \"conclusion\", \"references\"];\n\n// ── Load keys with rotation ─────────────────────────────────────────────────\n\nfunction loadKeys(envPrefix, maxKeys = 10) {\n const keys = [];\n for (let i = 1; i <= maxKeys; i++) {\n const k = process.env[`${envPrefix}_${i}`] || process.env[`${envPrefix}${i}`];\n if (k) keys.push(k);\n }\n // Also check single-key env vars\n const single = process.env[envPrefix];\n if (single && !keys.includes(single)) keys.unshift(single);\n return keys;\n}\n\nconst keyIndices = {};\nfunction nextKey(providerId, keys) {\n if (!keys.length) return null;\n const idx = keyIndices[providerId] || 0;\n const key = keys[idx % keys.length];\n keyIndices[providerId] = (idx + 1) % keys.length;\n return key;\n}\n\n// ── Provider definitions ────────────────────────────────────────────────────\n\nconst PROVIDERS = [\n // --- Cerebras: 3 keys x 2 models = up to 6 independent judges ---\n {\n id: \"cerebras-qwen\",\n name: \"Cerebras-Qwen235B\",\n url: \"https://api.cerebras.ai/v1/chat/completions\",\n model: \"qwen-3-235b-a22b-instruct-2507\",\n keys: loadKeys(\"CEREBRAS_API_KEY\", 15).concat(loadKeys(\"CEREBRAS_KEY\", 15)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 1024,\n },\n {\n id: \"cerebras-llama\",\n name: \"Cerebras-Llama8B\",\n url: \"https://api.cerebras.ai/v1/chat/completions\",\n model: \"llama3.1-8b\",\n keys: loadKeys(\"CEREBRAS_API_KEY\", 15).concat(loadKeys(\"CEREBRAS_KEY\", 15)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n // --- Cerebras: GPT-OSS-120B (120B param open-source model, different perspective) ---\n {\n id: \"cerebras-gptoss\",\n name: \"Cerebras-GPT-OSS-120B\",\n url: \"https://api.cerebras.ai/v1/chat/completions\",\n model: \"gpt-oss-120b\",\n keys: loadKeys(\"CEREBRAS_API_KEY\", 15).concat(loadKeys(\"CEREBRAS_KEY\", 15)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n // --- Cerebras: ZAI GLM-4.7 (Chinese model — different cultural perspective on academic rigor) ---\n {\n id: \"cerebras-glm47\",\n name: \"Cerebras-GLM4.7\",\n url: \"https://api.cerebras.ai/v1/chat/completions\",\n model: \"zai-glm-4.7\",\n keys: loadKeys(\"CEREBRAS_API_KEY\", 15).concat(loadKeys(\"CEREBRAS_KEY\", 15)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 1024,\n },\n // --- Mistral: 3 keys, reliable ---\n {\n id: \"mistral\",\n name: \"Mistral\",\n url: \"https://api.mistral.ai/v1/chat/completions\",\n model: \"mistral-small-latest\",\n keys: loadKeys(\"MISTRAL_API_KEY\", 10).concat(loadKeys(\"MISTRAL_KEY\", 10)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n // --- Sarvam (Indian AI): sarvam-m, uses <think> tags (needs 2048+ tokens), 13 keys ---\n {\n id: \"sarvam\",\n name: \"Sarvam\",\n url: \"https://api.sarvam.ai/v1/chat/completions\",\n model: \"sarvam-m\",\n keys: loadKeys(\"SARVAM_KEY\", 15).concat(loadKeys(\"SARVAM_API_KEY\", 15)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 2048,\n },\n // --- OpenRouter: free models ---\n {\n id: \"openrouter\",\n name: \"OpenRouter\",\n url: \"https://openrouter.ai/api/v1/chat/completions\",\n model: \"qwen/qwen3-coder:free\",\n keys: loadKeys(\"OPENROUTER_API_KEY\"),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n extraHeaders: { \"HTTP-Referer\": \"https://www.p2pclaw.com\", \"X-Title\": \"P2PCLAW Scoring\" },\n },\n // --- Groq: 9 keys but may be org-restricted ---\n {\n id: \"groq\",\n name: \"Groq\",\n url: \"https://api.groq.com/openai/v1/chat/completions\",\n model: \"llama-3.3-70b-versatile\",\n keys: loadKeys(\"GROQ_API_KEY\", 15).concat(loadKeys(\"GROQ_KEY\", 15)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n // --- NVIDIA: 3 keys ---\n {\n id: \"nvidia\",\n name: \"NVIDIA\",\n url: \"https://integrate.api.nvidia.com/v1/chat/completions\",\n model: \"meta/llama-3.3-70b-instruct\",\n keys: loadKeys(\"NVAPI_KEY\", 10).concat(loadKeys(\"NVIDIA_API_KEY\", 10)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n timeout: 45000,\n },\n // --- Inception: mercury-2 (diffusion-based LLM — unique scoring perspective) ---\n {\n id: \"inception\",\n name: \"Inception-Mercury2\",\n url: \"https://api.inceptionlabs.ai/v1/chat/completions\",\n model: \"mercury-2\",\n keys: loadKeys(\"INCEPTION_API_KEY\", 15).concat(loadKeys(\"INCEPTION_KEY\", 15)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 1024, // Mercury-2 needs at least 50 tokens (reasoning model)\n timeout: 45000,\n },\n // --- Xiaomi MiMo: 5 keys x 2 models = up to 2 independent judges ---\n {\n id: \"xiaomi-flash\",\n name: \"Xiaomi-MiMo-Flash\",\n url: \"https://api.xiaomimimo.com/v1/chat/completions\",\n model: \"mimo-v2-flash\",\n keys: loadKeys(\"XIAOMI_API_KEY\", 10).concat(loadKeys(\"XIAOMI_KEY\", 10)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n {\n id: \"xiaomi-pro\",\n name: \"Xiaomi-MiMo-Pro\",\n url: \"https://api.xiaomimimo.com/v1/chat/completions\",\n model: \"mimo-v2-pro\",\n keys: loadKeys(\"XIAOMI_API_KEY\", 10).concat(loadKeys(\"XIAOMI_KEY\", 10)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 1024,\n },\n // --- Cohere: command-a-reasoning model (9 keys, reasoning/thinking model) ---\n {\n id: \"cohere\",\n name: \"Cohere-CommandA\",\n url: \"https://api.cohere.com/v2/chat\",\n model: \"command-a-reasoning-08-2025\",\n keys: loadKeys(\"COHERE_API_KEY\", 15).concat(loadKeys(\"COHERE_KEY\", 15)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 4096, // reasoning model needs extra tokens for thinking + answer\n responseFormat: \"cohere\", // data.message.content[] array with {type:\"thinking\"} + {type:\"text\"}\n timeout: 90000, // 90s — reasoning model needs time to think through all 10 dimensions\n },\n // --- Cloudflare Workers AI: 6 models across 6 accounts (all FREE) ---\n // Ordered by model quality: best first\n {\n id: \"cloudflare-glm\",\n name: \"Cloudflare-GLM4\",\n url: `https://api.cloudflare.com/client/v4/accounts/eaffd2b52c95c69aaad8d859e9dcb52b/ai/run/@cf/zai-org/glm-4.7-flash`,\n model: \"@cf/zai-org/glm-4.7-flash\",\n keys: loadKeys(\"CF_AI_TOKEN\").concat(loadKeys(\"CLOUDFLARE_AI_TOKEN\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 2048,\n responseFormat: \"cloudflare\",\n timeout: 60000,\n },\n {\n id: \"cloudflare-gemma4\",\n name: \"Cloudflare-Gemma4\",\n url: `https://api.cloudflare.com/client/v4/accounts/a7995d3f33b6ba57955749337c9abbe0/ai/run/@cf/google/gemma-4-26b-a4b-it`,\n model: \"@cf/google/gemma-4-26b-a4b-it\",\n keys: loadKeys(\"CF_AI_TOKEN_2\"),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 1024,\n responseFormat: \"cloudflare\",\n timeout: 60000,\n },\n {\n id: \"cloudflare-nemotron\",\n name: \"Cloudflare-Nemotron\",\n url: `https://api.cloudflare.com/client/v4/accounts/194d9aea21482ac893ed81fc6b004864/ai/run/@cf/nvidia/nemotron-3-120b-a12b`,\n model: \"@cf/nvidia/nemotron-3-120b-a12b\",\n keys: loadKeys(\"CF_AI_TOKEN_3\"),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 1024,\n responseFormat: \"cloudflare\",\n timeout: 60000,\n },\n {\n id: \"cloudflare-kimi\",\n name: \"Cloudflare-Kimi\",\n url: `https://api.cloudflare.com/client/v4/accounts/401a75ead25275262c1c05eecb7a997c/ai/run/@cf/moonshotai/kimi-k2.5`,\n model: \"@cf/moonshotai/kimi-k2.5\",\n keys: loadKeys(\"CF_AI_TOKEN_4\"),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 2048,\n responseFormat: \"cloudflare\",\n timeout: 60000,\n },\n {\n id: \"cloudflare-gptoss\",\n name: \"Cloudflare-GPT-OSS\",\n url: `https://api.cloudflare.com/client/v4/accounts/73340519f6430362daee759ba0b48ce8/ai/run/@cf/openai/gpt-oss-120b`,\n model: \"@cf/openai/gpt-oss-120b\",\n keys: loadKeys(\"CF_AI_TOKEN_5\"),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 1024,\n responseFormat: \"cloudflare\",\n timeout: 60000,\n },\n {\n id: \"cloudflare-qwen3\",\n name: \"Cloudflare-Qwen3\",\n url: `https://api.cloudflare.com/client/v4/accounts/df4a7888befcbb6ce3e0a0b346ea1990/ai/run/@cf/qwen/qwen3-30b-a3b-fp8`,\n model: \"@cf/qwen/qwen3-30b-a3b-fp8\",\n keys: loadKeys(\"CF_AI_TOKEN_6\"),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 2048,\n responseFormat: \"cloudflare\",\n timeout: 60000,\n },\n // --- Cloudflare Workers AI: 3 NEW accounts (accounts 7/8/9) ---\n {\n id: \"cloudflare-llama4scout\",\n name: \"Cloudflare-Llama4Scout\",\n url: `https://api.cloudflare.com/client/v4/accounts/3cd084561890e5ab468456fae547ded0/ai/run/@cf/meta/llama-4-scout-17b-16e-instruct`,\n model: \"@cf/meta/llama-4-scout-17b-16e-instruct\",\n keys: loadKeys(\"CF_AI_TOKEN_7\"),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 1024,\n responseFormat: \"cloudflare\",\n timeout: 60000,\n },\n {\n id: \"cloudflare-mistral31\",\n name: \"Cloudflare-MistralSmall31\",\n url: `https://api.cloudflare.com/client/v4/accounts/27920eccf7d83f7ee267130cd6018eaf/ai/run/@cf/mistralai/mistral-small-3.1-24b-instruct`,\n model: \"@cf/mistralai/mistral-small-3.1-24b-instruct\",\n keys: loadKeys(\"CF_AI_TOKEN_8\"),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 1024,\n responseFormat: \"cloudflare\",\n timeout: 60000,\n },\n {\n id: \"cloudflare-deepseekr1\",\n name: \"Cloudflare-DeepSeekR1\",\n url: `https://api.cloudflare.com/client/v4/accounts/60c2dcaa7fc3377f036114648f6397ba/ai/run/@cf/deepseek-ai/deepseek-r1-distill-qwen-32b`,\n model: \"@cf/deepseek-ai/deepseek-r1-distill-qwen-32b\",\n keys: loadKeys(\"CF_AI_TOKEN_9\"),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 2048,\n responseFormat: \"cloudflare\",\n timeout: 60000,\n },\n // --- Cloudflare Workers AI: Account 10 (Agnuxo2026, GLM-4.7-flash) ---\n {\n id: \"cloudflare-glm47-10\",\n name: \"Cloudflare-GLM47-Acct10\",\n url: `https://api.cloudflare.com/client/v4/accounts/${process.env.CF_ACCOUNT_ID_10 || \"00c5ebf4df46d16450d5f1419dc36c6a\"}/ai/run/@cf/zai-org/glm-4.7-flash`,\n model: \"@cf/zai-org/glm-4.7-flash\",\n keys: loadKeys(\"CF_AI_TOKEN_10\"),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 2048,\n responseFormat: \"cloudflare\",\n timeout: 60000,\n },\n];\n\n// Deduplicate keys within each provider\nfor (const p of PROVIDERS) {\n p.keys = [...new Set(p.keys)].filter(Boolean);\n}\n\n// Log available providers\nconst available = PROVIDERS.filter(p => p.keys.length > 0);\nconsole.log(`[SCORING] ${available.length} LLM providers available: ${available.map(p => `${p.name}(${p.keys.length})`).join(\", \")}`);\nif (available.length === 0) console.warn(\"[SCORING] No LLM providers — heuristic scoring only.\");\n// Debug: log specifically which providers have NO keys\nconst unavailable = PROVIDERS.filter(p => p.keys.length === 0);\nif (unavailable.length > 0) console.log(`[SCORING] Providers with NO keys: ${unavailable.map(p => p.name).join(\", \")}`);\n\nconst SCORING_PROMPT = `You are a STRICT academic peer reviewer. You evaluate papers for the P2PCLAW benchmark.\n\nYour scoring must follow real academic standards. Most papers score 4-6. Only exceptional work reaches 7+. A score of 8+ means top-tier venue quality (NeurIPS, Nature, ICML). A 10 is historically significant (Turing Award level).\n\nCALIBRATION ANCHORS — use these to calibrate your scores:\n- 10/10 novelty: \"Attention Is All You Need\" (Vaswani 2017), Bitcoin whitepaper (Nakamoto 2008)\n- 8/10 novelty: A genuine new algorithm with proven improvements over SOTA\n- 6/10 novelty: Meaningful extension of existing work with some original insights\n- 5/10 novelty: Applying known techniques to a new domain (standard contribution)\n- 3/10 novelty: Minor variation of existing work, obvious next step\n- 1/10 novelty: Restating known results with different notation\n\nScore each criterion on 0-10:\n- abstract (0-10): Clarity, completeness, problem+scope+results summarized\n- introduction (0-10): Problem clarity, context, motivation. Needs 2+ real citations to related work\n- methodology (0-10): Rigor and reproducibility. Can someone replicate this exactly?\n- results (0-10): Strength of evidence. Real data, real experiments, statistical significance\n- discussion (0-10): Honest limitations, implications, comparison to prior work\n- conclusion (0-10): Summary of findings, concrete future directions\n- references (0-10): Real citations with authors, titles, years, DOIs. 8+ unique for score >5\n- novelty (0-10): TRUE original contribution. Applying a standard formula to a new domain = 4-5, NOT 8-9\n- reproducibility (0-10): Code, equations, parameters, data availability\n- citation_quality (0-10): Are references real, verified, and actually cited in the text?\n\nSTRICT RULES:\n- Missing section = 0 for that section\n- Placeholder/fake references = references score 1\n- Papers under 300 words = all scores below 3\n- Proving obvious things = novelty 0-1\n- Standard variance/mean/known formulas applied to new domain = novelty 4-5 MAX\n- No experimental data or only synthetic/estimated data = results 3-4 MAX\n- Self-referential citations or citing only own work = citation_quality 2-3\n- Circular reasoning (using conclusion to justify methodology) = methodology cap 4\n- Claims without evidence (e.g., \"we achieve SOTA\" with no comparison) = results cap 3\n- DO NOT give 8+ unless the work would genuinely be accepted at a top venue\n\nReturn ONLY this JSON (numbers 0-10):\n{\"abstract\":N,\"introduction\":N,\"methodology\":N,\"results\":N,\"discussion\":N,\"conclusion\":N,\"references\":N,\"novelty\":N,\"reproducibility\":N,\"citation_quality\":N}\n\nPaper content:\n`;\n\n/**\n * Call a single LLM provider to score a paper.\n * Tries up to 2 keys from the provider before giving up.\n */\nasync function callLLMForScoring(prompt, provider) {\n if (!provider.keys.length) return null;\n\n const maxAttempts = Math.min(provider.keys.length, 2);\n for (let attempt = 0; attempt < maxAttempts; attempt++) {\n const key = nextKey(provider.id, provider.keys);\n if (!key) return null;\n\n const headers = { \"Content-Type\": \"application/json\" };\n headers[provider.authHeader] = `${provider.authPrefix}${key}`;\n if (provider.extraHeaders) Object.assign(headers, provider.extraHeaders);\n\n try {\n const res = await fetch(provider.url, {\n method: \"POST\",\n headers,\n body: JSON.stringify({\n model: provider.model,\n messages: [{ role: \"user\", content: prompt }],\n max_tokens: provider.maxTokens || 512,\n temperature: 0.1,\n }),\n signal: AbortSignal.timeout(provider.timeout || 30000),\n });\n\n if (res.status === 429) {\n console.warn(`[SCORING] ${provider.name} 429 rate-limited`);\n await new Promise(r => setTimeout(r, 3000));\n continue;\n }\n if (res.status === 402) {\n console.warn(`[SCORING] ${provider.name} 402 credits exhausted — skipping provider`);\n return null;\n }\n if (!res.ok) {\n const errBody = await res.text().catch(() => \"\");\n console.warn(`[SCORING] ${provider.name} HTTP ${res.status}: ${errBody.substring(0, 150)}`);\n continue;\n }\n\n const data = await res.json();\n // Support both OpenAI format (choices[0].message.content) and Cohere v2 (message.content[] array)\n let text = \"\";\n if (provider.responseFormat === \"cohere\") {\n // Cohere v2 returns array of content blocks: [{type:\"thinking\",...}, {type:\"text\",text:\"...\"}]\n const blocks = data.message?.content || [];\n if (Array.isArray(blocks)) {\n const textBlock = blocks.find(b => b.type === \"text\");\n text = textBlock?.text || blocks[blocks.length - 1]?.text || \"\";\n } else {\n text = typeof blocks === \"string\" ? blocks : \"\";\n }\n } else if (provider.responseFormat === \"cloudflare\") {\n // Cloudflare Workers AI wraps OpenAI format: {result: {choices: [...]}, success: true}\n const inner = data.result || data;\n text = inner.choices?.[0]?.message?.content || \"\";\n } else {\n text = data.choices?.[0]?.message?.content || \"\";\n }\n\n // Strip <think>...</think> tags (Sarvam, Qwen with thinking mode)\n if (provider.stripThinkTags || text.includes(\"<think>\")) {\n // Strip closed think blocks\n text = text.replace(/<think>[\\s\\S]*?<\\/think>/g, \"\").trim();\n // Strip unclosed think blocks (model ran out of tokens mid-think)\n if (text.startsWith(\"<think>\")) {\n text = text.replace(/<think>[\\s\\S]*/g, \"\").trim();\n }\n }\n\n // Extract JSON — use balanced braces to handle nested objects like {\"score\":8,\"why\":\"...\"}\n let jsonMatch = null;\n const firstBrace = text.indexOf('{');\n if (firstBrace !== -1) {\n let depth = 0, end = -1;\n for (let i = firstBrace; i < text.length; i++) {\n if (text[i] === '{') depth++;\n else if (text[i] === '}') { depth--; if (depth === 0) { end = i; break; } }\n }\n if (end !== -1) jsonMatch = [text.substring(firstBrace, end + 1)];\n }\n if (!jsonMatch) {\n console.warn(`[SCORING] ${provider.name} — no JSON in response`);\n continue;\n }\n\n const parsed = JSON.parse(jsonMatch[0]);\n const fields = [...SECTIONS, \"novelty\", \"reproducibility\", \"citation_quality\"];\n const feedback = {};\n for (const field of fields) {\n let val = parsed[field];\n // Support both plain number and {score, why} object format\n if (val && typeof val === \"object\" && typeof val.score === \"number\") {\n feedback[field] = val.why || null;\n val = val.score;\n }\n if (typeof val !== \"number\" || val < 0 || val > 10) {\n val = typeof val === \"number\" ? Math.max(0, Math.min(10, Math.round(val))) : 5;\n }\n parsed[field] = val;\n }\n\n // Clean parsed: only keep recognized score fields (remove stray \"why\" etc.)\n const cleanScores = {};\n for (const field of fields) cleanScores[field] = parsed[field];\n\n console.log(`[SCORING] ${provider.name} scored successfully`);\n return { scores: cleanScores, provider: provider.name, feedback: Object.keys(feedback).length > 0 ? feedback : null };\n } catch (e) {\n console.warn(`[SCORING] ${provider.name} error: ${e.message}`);\n }\n }\n return null;\n}\n\n/**\n * Deterministic heuristic scoring — used when all LLMs fail.\n */\nfunction heuristicScore(content) {\n const text = content || \"\";\n const words = text.split(/\\s+/).filter(w => w.length > 0);\n const wordCount = words.length;\n const lowerText = text.toLowerCase();\n\n const sectionScores = {};\n for (const section of SECTIONS) {\n const hasSection = lowerText.includes(`## ${section}`) || lowerText.includes(`# ${section}`);\n if (!hasSection) {\n sectionScores[section] = 0;\n continue;\n }\n const sectionRegex = new RegExp(`##?\\\\s*${section}[\\\\s\\\\S]*?(?=##?\\\\s|$)`, \"i\");\n const match = text.match(sectionRegex);\n const sectionWords = match ? match[0].split(/\\s+/).length : 0;\n if (sectionWords < 20) sectionScores[section] = 2;\n else if (sectionWords < 50) sectionScores[section] = 4;\n else if (sectionWords < 100) sectionScores[section] = 5;\n else if (sectionWords < 200) sectionScores[section] = 6;\n else sectionScores[section] = 7;\n }\n\n const refMatches = text.match(/\\[\\d+\\]/g) || [];\n const uniqueRefs = new Set(refMatches).size;\n const hasPlaceholderRefs = /placeholder|author,?\\s*a\\.\\s*\\(\\d{4}\\)/i.test(text);\n const hasRealAuthors = /[A-Z][a-z]+,\\s*[A-Z]\\.\\s*(?:&|,|et al)/g.test(text);\n const hasDOI = /doi\\.org|arxiv\\.org|10\\.\\d{4}/i.test(text);\n let refScore = hasPlaceholderRefs ? 1 : Math.min(7, uniqueRefs);\n if (hasRealAuthors) refScore = Math.min(10, refScore + 1);\n if (hasDOI) refScore = Math.min(10, refScore + 1);\n sectionScores.references = refScore;\n\n const technicalTerms = (text.match(/\\b(algorithm|theorem|proof|complexity|O\\([^)]+\\)|convergence|optimal|novel|framework)\\b/gi) || []).length;\n const hasFigures = /figure \\d|fig\\.\\s*\\d|table \\d/i.test(text);\n let novelty = wordCount > 2000 ? 4 : wordCount > 1000 ? 3 : 2;\n novelty += Math.min(3, Math.floor(technicalTerms / 5));\n if (hasFigures) novelty += 1;\n\n const hasCode = /```[\\s\\S]*?```/.test(text);\n const hasEquations = /\\$[^$]+\\$/.test(text) || /\\\\begin\\{/.test(text);\n const hasNumbers = /\\d+\\.\\d+%|\\d+\\.\\d+x|p\\s*[<>]\\s*0\\.\\d/i.test(text);\n let reproducibility = (hasCode ? 5 : 3) + (hasEquations ? 1 : 0) + (wordCount > 2000 ? 1 : 0);\n if (hasNumbers) reproducibility += 1;\n\n const citation_quality = hasPlaceholderRefs ? 1 : Math.min(6, uniqueRefs);\n\n return {\n scores: {\n ...sectionScores,\n novelty: Math.min(10, novelty),\n reproducibility: Math.min(10, reproducibility),\n citation_quality: Math.min(10, citation_quality),\n },\n provider: \"heuristic\"\n };\n}\n\n/**\n * Score a paper using heterogeneous multi-LLM swarm.\n * Tries to get 2 independent judges, falls back through provider chain.\n */\nexport async function scoreGranular(content, paperType = \"research\") {\n if (!content || content.trim().length < 50) {\n return {\n sections: Object.fromEntries(SECTIONS.map(s => [s, 0])),\n overall: 0,\n novelty: 0,\n reproducibility: 0,\n citation_quality: 0,\n judges: [],\n judge_count: 0,\n error: \"Content too short to score\"\n };\n }\n\n // All LLM judges support 32k+ context. Send enough content to include ALL 7 mandatory sections.\n // Previous limit of 4000 chars truncated papers before methodology/results/conclusion, causing\n // strict judges (Cohere, Xiaomi) to correctly score missing sections as 0.\n const truncated = content.length > 16000 ? content.substring(0, 16000) + \"\\n\\n[... truncated for scoring ...]\" : content;\n const prompt = SCORING_PROMPT + truncated;\n\n // ALL available judges score independently for maximum consensus diversity.\n // Each model evaluates each section independently, then we average.\n // Run all judges in parallel for speed (each has its own timeout).\n const judgePromises = available.map(provider =>\n callLLMForScoring(prompt, provider).catch(() => null)\n );\n const judgeResults = await Promise.all(judgePromises);\n let judges = judgeResults.filter(Boolean);\n\n // Fix #2: Filter broken judges — remove any judge that gave 0 to 3+ sections\n // (indicates a parsing failure or truncated response, not a real evaluation)\n judges = judges.filter(j => {\n const zeroCount = SECTIONS.filter(s => j.scores[s] === 0).length;\n if (zeroCount >= 3) {\n console.warn(`[SCORING] Filtered broken judge ${j.judge || 'unknown'}: ${zeroCount} sections scored 0`);\n return false;\n }\n return true;\n });\n\n // If no LLM judges succeeded, use heuristic\n if (judges.length === 0) {\n const heuristic = heuristicScore(content);\n judges.push(heuristic);\n }\n\n // Average scores across all judges\n const allFields = [...SECTIONS, \"novelty\", \"reproducibility\", \"citation_quality\"];\n const averaged = {};\n for (const field of allFields) {\n const values = judges.map(j => j.scores[field]).filter(v => typeof v === \"number\");\n averaged[field] = values.length > 0\n ? Math.round((values.reduce((a, b) => a + b, 0) / values.length) * 10) / 10\n : 0;\n }\n\n // ── CALIBRATION PASS — adjust raw averages against reference benchmarks ──\n // This is the core fix for inflated scoring. Raw LLM averages are compared\n // against quality signals extracted from the paper content and calibrated\n // against recognized reference paper fingerprints.\n let calibration = null;\n try {\n const fieldResult = detectField(content);\n const signals = extractSignals(content);\n const benchmarks = REFERENCE_BENCHMARKS[fieldResult.field] || null;\n const { calibrated, adjustments } = calibrateScores(averaged, signals, benchmarks);\n\n // Apply calibrated scores over raw averages\n const adjustmentCount = Object.keys(adjustments).length;\n if (adjustmentCount > 0) {\n for (const [field, val] of Object.entries(calibrated)) {\n if (typeof val === \"number\" && averaged[field] !== undefined) {\n averaged[field] = val;\n }\n }\n console.log(`[SCORING] Calibration applied: ${adjustmentCount} adjustments, field=${fieldResult.field}, ` +\n `red_flags=${signals.red_flag_count}, depth=${signals.depth_score}`);\n }\n\n calibration = {\n field: fieldResult.field,\n field_confidence: fieldResult.confidence,\n signals_summary: {\n word_count: signals.word_count,\n sections_present: signals.sections_present.length,\n sections_missing: signals.sections_missing,\n red_flags: signals.red_flags,\n red_flag_count: signals.red_flag_count,\n has_formal_proofs: signals.has_formal_proofs,\n has_equations: signals.has_equations,\n has_code: signals.has_code,\n unique_refs: signals.unique_refs,\n has_placeholder_refs: signals.has_placeholder_refs,\n depth_score: signals.depth_score,\n evidence_markers: signals.evidence_markers,\n deception_count: signals.deception_count || 0,\n deception_matches: (signals.deception_matches || []).map(d => ({\n id: d.id, name: d.name, severity: d.severity,\n })),\n // New quality dimensions\n grammar: signals.grammar_quality ? {\n vocabulary_diversity: signals.grammar_quality.vocabulary_diversity_ttr,\n is_monotone: signals.grammar_quality.is_monotone,\n is_low_vocabulary: signals.grammar_quality.is_low_vocabulary,\n } : null,\n repetition_ratio: signals.repetition_score?.repetition_ratio || 0,\n code_quality: signals.code_quality?.blocks_found > 0 ? {\n blocks: signals.code_quality.blocks_found,\n has_real_code: signals.has_real_code || false,\n has_python: signals.code_quality.has_python || false,\n } : null,\n math_formulas: signals.math_quality?.formula_count || 0,\n lean4: signals.lean4_signals?.verification_level || \"none\",\n tables: signals.table_quality?.count || 0,\n },\n adjustments,\n adjustment_count: adjustmentCount,\n reference_papers: benchmarks ? benchmarks.references.map(r => r.title) : [],\n };\n } catch (calErr) {\n console.warn(`[SCORING] Calibration error (non-fatal): ${calErr.message}`);\n }\n\n // ── LIVE VERIFICATION PASS — real-time CrossRef, arXiv, code exec, Lean4 ──\n // Runs in parallel with independent timeouts. Non-fatal: if it fails, scoring\n // continues with just calibration. Results apply caps and bonuses to scores.\n let liveVerification = null;\n try {\n const verification = await runLiveVerification(content);\n const { adjustments: liveAdj, bonuses: liveBon } = verificationToAdjustments(verification);\n\n // ── OVERRIDE FALSE POSITIVES ──\n // If live verification confirmed code execution but calibration flagged\n // \"code_blocks_are_template_not_real\", the red_flag was a false positive.\n // Undo the -1.5 penalty applied to all dimensions by recalibrating.\n const codeExec = verification.code_execution;\n if (codeExec && codeExec.passed > 0 && calibration && calibration.signals_summary) {\n const redFlags = calibration.signals_summary.red_flags || [];\n if (redFlags.includes(\"code_blocks_are_template_not_real\")) {\n // The -1.5 penalty was applied to all 10 dimensions. Undo it.\n const allFields = [...SECTIONS, \"novelty\", \"reproducibility\", \"citation_quality\"];\n for (const field of allFields) {\n if (averaged[field] !== undefined) {\n averaged[field] = Math.min(10, Math.round((averaged[field] + 1.5) * 10) / 10);\n }\n }\n // Remove the false red flag from the report\n calibration.signals_summary.red_flags = redFlags.filter(f => f !== \"code_blocks_are_template_not_real\");\n calibration.signals_summary.red_flag_count = calibration.signals_summary.red_flags.length;\n calibration.false_positive_corrected = \"code_blocks_are_template_not_real (live verification confirmed code executes)\";\n console.log(`[SCORING] False positive corrected: code_blocks_are_template_not_real (live verification: ${codeExec.passed}/${codeExec.total} passed)`);\n }\n }\n\n // Apply caps (these override score to a max value)\n for (const [key, val] of Object.entries(liveAdj)) {\n if (key.endsWith(\"_cap\") && typeof val === \"number\") {\n const field = key.replace(\"_cap\", \"\");\n if (averaged[field] !== undefined && averaged[field] > val) {\n averaged[field] = val;\n }\n }\n }\n\n // Apply bonuses (add to score, capped at 10)\n for (const [key, val] of Object.entries(liveBon)) {\n if (key.endsWith(\"_bonus\") && typeof val === \"number\") {\n const field = key.replace(\"_bonus\", \"\");\n if (averaged[field] !== undefined) {\n averaged[field] = Math.min(10, Math.round((averaged[field] + val) * 10) / 10);\n }\n }\n }\n\n liveVerification = {\n verification_time_ms: verification.verification_time_ms,\n citations: verification.citations ? {\n total: verification.citations.total,\n verified: verification.citations.verified,\n verification_rate: verification.citations.verification_rate,\n } : null,\n novelty: verification.novelty ? {\n searched: verification.novelty.searched,\n total_found: verification.novelty.total_found,\n novelty_concern: verification.novelty.novelty_concern,\n max_similarity: verification.novelty.max_similarity,\n } : null,\n code_execution: verification.code_execution ? {\n total: verification.code_execution.total,\n passed: verification.code_execution.passed,\n failed: verification.code_execution.failed,\n } : null,\n lean4: verification.lean4 ? {\n blocks_found: verification.lean4.blocks_found,\n verified: verification.lean4.verified,\n has_unsubstantiated_claim: verification.lean4.has_unsubstantiated_claim || false,\n } : null,\n adjustments: liveAdj,\n bonuses: liveBon,\n };\n\n const adjCount = Object.keys(liveAdj).filter(k => k.endsWith(\"_cap\")).length;\n const bonCount = Object.keys(liveBon).filter(k => k.endsWith(\"_bonus\")).length;\n if (adjCount > 0 || bonCount > 0) {\n console.log(`[SCORING] Live verification: ${adjCount} caps, ${bonCount} bonuses applied (${verification.verification_time_ms}ms)`);\n }\n } catch (liveErr) {\n // Fix #3: Better error logging for live verification failures\n console.warn(`[SCORING] Live verification error (non-fatal): ${liveErr.message}`, liveErr.stack?.split('\\n').slice(0, 3).join(' | '));\n }\n\n const sectionValues = SECTIONS.map(s => averaged[s]);\n let overall = Math.round((sectionValues.reduce((a, b) => a + b, 0) / SECTIONS.length) * 10) / 10;\n\n // ── Phase F: Execution Proof Bonus ──\n // Papers with verified code blocks (execution hashes) get a purely additive overall bonus.\n // This only increases the score, never decreases. Capped at 10.\n if (liveVerification && liveVerification.bonuses && liveVerification.bonuses.execution_proof_bonus) {\n overall = Math.min(10, Math.round((overall + liveVerification.bonuses.execution_proof_bonus) * 10) / 10);\n console.log(`[SCORING] Execution proof bonus applied: +${liveVerification.bonuses.execution_proof_bonus} → overall=${overall}`);\n }\n\n // Fix #10: Fallback execution proof bonus — if live verification failed/timed out\n // but the paper contains verified execution hashes, still award the bonus.\n if (!liveVerification || !liveVerification.bonuses?.execution_proof_bonus) {\n const hashMatches = (content.match(/execution[_ ]hash[^`]*`([a-f0-9]{40,})`/gi) || []);\n if (hashMatches.length > 0) {\n const fallbackBonus = Math.min(1.5, hashMatches.length * 0.5);\n overall = Math.min(10, Math.round((overall + fallbackBonus) * 10) / 10);\n console.log(`[SCORING] Fallback execution proof bonus: ${hashMatches.length} hashes found in paper → +${fallbackBonus}`);\n if (!liveVerification) liveVerification = {};\n if (!liveVerification.bonuses) liveVerification.bonuses = {};\n liveVerification.bonuses.execution_proof_bonus = fallbackBonus;\n liveVerification.bonuses.execution_proof_note = `${hashMatches.length} execution hash(es) found in paper text (fallback detection)`;\n }\n }\n\n // Per-judge detail breakdown (individual scores + feedback)\n const judge_details = judges.map(j => ({\n judge: j.provider,\n scores: j.scores,\n feedback: j.feedback || null,\n }));\n\n // Consensus score per dimension (1 = all judges agree, 0 = total disagreement)\n const consensus = {};\n for (const field of allFields) {\n const values = judges.map(j => j.scores[field]).filter(v => typeof v === \"number\");\n if (values.length < 2) { consensus[field] = 1.0; continue; }\n const mean = values.reduce((a, b) => a + b, 0) / values.length;\n const variance = values.reduce((sum, v) => sum + (v - mean) ** 2, 0) / values.length;\n const stddev = Math.sqrt(variance);\n consensus[field] = Math.round(Math.max(0, 1 - stddev / 5) * 100) / 100;\n }\n const overall_consensus = Math.round(\n (allFields.reduce((sum, f) => sum + consensus[f], 0) / allFields.length) * 100\n ) / 100;\n\n // Aggregate feedback per dimension (from all judges that provided it)\n const aggregated_feedback = {};\n for (const field of allFields) {\n const comments = judges\n .filter(j => j.feedback && j.feedback[field])\n .map(j => ({ judge: j.provider, comment: j.feedback[field] }));\n if (comments.length > 0) aggregated_feedback[field] = comments;\n }\n\n const result = {\n sections: Object.fromEntries(SECTIONS.map(s => [s, averaged[s]])),\n overall,\n novelty: averaged.novelty,\n reproducibility: averaged.reproducibility,\n citation_quality: averaged.citation_quality,\n judges: judges.map(j => j.provider),\n judge_count: judges.length,\n judge_details,\n consensus,\n overall_consensus,\n feedback: Object.keys(aggregated_feedback).length > 0 ? aggregated_feedback : null,\n scored_at: new Date().toISOString(),\n paper_type: paperType,\n calibration,\n live_verification: liveVerification,\n };\n\n console.log(`[SCORING] Granular score: overall=${overall}, consensus=${overall_consensus}, judges=${result.judges.join(\",\")}`);\n return result;\n}\n"}, "99e40ca7b79177592065d72f978a59bef82c4939": {"t": 1785753923.4009898, "n": 38231, "h": "69893c82aad7afe3", "c": "/**\n * P2PCLAW Granular Scoring Service — MAXIMUM TRIBUNAL\n * ====================================================\n * Heterogeneous multi-LLM scoring engine that evaluates papers section-by-section.\n * Updated 2026-04-04: ALL available API keys deployed for maximum judge diversity.\n *\n * Provider chain (updated 2026-04-04):\n * 1. Cerebras — qwen-3-235b-a22b (8 keys, free, ultra-fast)\n * 2. Cerebras — llama3.1-8b (8 keys, free, ultra-fast)\n * 3. Cerebras — gpt-oss-120b (8 keys, free, different model perspective)\n * 4. Cerebras — zai-glm-4.7 (8 keys, free, Chinese model perspective)\n * 5. Mistral — mistral-small-latest (3 keys, free)\n * 6. Sarvam — sarvam-m (13 keys, Indian AI, free)\n * 7. OpenRouter — qwen3-coder:free (3 keys, free)\n * 8. Groq — llama-3.3-70b-versatile (9 keys)\n * 9. NVIDIA — meta/llama-3.3-70b-instruct (3 keys, free)\n * 10. Inception — mercury-2 (9 keys, free, diffusion-based)\n * 11. Xiaomi MiMo — mimo-v2-flash (5 keys, free)\n * 12. Xiaomi MiMo — mimo-v2-pro (5 keys, free, reasoning)\n * 13. Cohere — command-a-reasoning (9 keys, reasoning model)\n * 14-22. Cloudflare — 9 accounts x unique models (GLM4, Gemma4, Nemotron, Kimi, GPT-OSS, Qwen3, Llama4Scout, MistralSmall31, DeepSeekR1)\n * 23. Cloudflare — account 10 (GLM-4.7-flash, additional account)\n * 24. Deterministic heuristic fallback (never blocks)\n *\n * TOTAL: 23 independent LLM judges + 1 heuristic = 24 scoring perspectives\n * ALL available judges score independently. Final score = average across all judges.\n * Each model evaluates each section independently for maximum consensus diversity.\n */\n\nimport { detectField, extractSignals, calibrateScores, REFERENCE_BENCHMARKS } from \"./calibrationService.js\";\nimport { runLiveVerification, verificationToAdjustments } from \"./liveVerificationService.js\";\n\nconst SECTIONS = [\"abstract\", \"introduction\", \"methodology\", \"results\", \"discussion\", \"conclusion\", \"references\"];\n\n// ── Load keys with rotation ─────────────────────────────────────────────────\n\nfunction loadKeys(envPrefix, maxKeys = 10) {\n const keys = [];\n for (let i = 1; i <= maxKeys; i++) {\n const k = process.env[`${envPrefix}_${i}`] || process.env[`${envPrefix}${i}`];\n if (k) keys.push(k);\n }\n // Also check single-key env vars\n const single = process.env[envPrefix];\n if (single && !keys.includes(single)) keys.unshift(single);\n return keys;\n}\n\nconst keyIndices = {};\nfunction nextKey(providerId, keys) {\n if (!keys.length) return null;\n const idx = keyIndices[providerId] || 0;\n const key = keys[idx % keys.length];\n keyIndices[providerId] = (idx + 1) % keys.length;\n return key;\n}\n\n// ── Provider definitions ────────────────────────────────────────────────────\n\nconst PROVIDERS = [\n // --- Cerebras: 3 keys x 2 models = up to 6 independent judges ---\n {\n id: \"cerebras-qwen\",\n name: \"Cerebras-Qwen235B\",\n url: \"https://api.cerebras.ai/v1/chat/completions\",\n model: \"qwen-3-235b-a22b-instruct-2507\",\n keys: loadKeys(\"CEREBRAS_API_KEY\", 15).concat(loadKeys(\"CEREBRAS_KEY\", 15)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 1024,\n },\n {\n id: \"cerebras-llama\",\n name: \"Cerebras-Llama8B\",\n url: \"https://api.cerebras.ai/v1/chat/completions\",\n model: \"llama3.1-8b\",\n keys: loadKeys(\"CEREBRAS_API_KEY\", 15).concat(loadKeys(\"CEREBRAS_KEY\", 15)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n // --- Cerebras: GPT-OSS-120B (120B param open-source model, different perspective) ---\n {\n id: \"cerebras-gptoss\",\n name: \"Cerebras-GPT-OSS-120B\",\n url: \"https://api.cerebras.ai/v1/chat/completions\",\n model: \"gpt-oss-120b\",\n keys: loadKeys(\"CEREBRAS_API_KEY\", 15).concat(loadKeys(\"CEREBRAS_KEY\", 15)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n // --- Cerebras: ZAI GLM-4.7 (Chinese model — different cultural perspective on academic rigor) ---\n {\n id: \"cerebras-glm47\",\n name: \"Cerebras-GLM4.7\",\n url: \"https://api.cerebras.ai/v1/chat/completions\",\n model: \"zai-glm-4.7\",\n keys: loadKeys(\"CEREBRAS_API_KEY\", 15).concat(loadKeys(\"CEREBRAS_KEY\", 15)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 1024,\n },\n // --- Mistral: 3 keys, reliable ---\n {\n id: \"mistral\",\n name: \"Mistral\",\n url: \"https://api.mistral.ai/v1/chat/completions\",\n model: \"mistral-small-latest\",\n keys: loadKeys(\"MISTRAL_API_KEY\", 10).concat(loadKeys(\"MISTRAL_KEY\", 10)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n // --- Sarvam (Indian AI): sarvam-m, uses <think> tags (needs 2048+ tokens), 13 keys ---\n {\n id: \"sarvam\",\n name: \"Sarvam\",\n url: \"https://api.sarvam.ai/v1/chat/completions\",\n model: \"sarvam-m\",\n keys: loadKeys(\"SARVAM_KEY\", 15).concat(loadKeys(\"SARVAM_API_KEY\", 15)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 2048,\n },\n // --- OpenRouter: free models ---\n {\n id: \"openrouter\",\n name: \"OpenRouter\",\n url: \"https://openrouter.ai/api/v1/chat/completions\",\n model: \"qwen/qwen3-coder:free\",\n keys: loadKeys(\"OPENROUTER_API_KEY\"),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n extraHeaders: { \"HTTP-Referer\": \"https://www.p2pclaw.com\", \"X-Title\": \"P2PCLAW Scoring\" },\n },\n // --- Groq: 9 keys but may be org-restricted ---\n {\n id: \"groq\",\n name: \"Groq\",\n url: \"https://api.groq.com/openai/v1/chat/completions\",\n model: \"llama-3.3-70b-versatile\",\n keys: loadKeys(\"GROQ_API_KEY\", 15).concat(loadKeys(\"GROQ_KEY\", 15)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n // --- NVIDIA: 3 keys ---\n {\n id: \"nvidia\",\n name: \"NVIDIA\",\n url: \"https://integrate.api.nvidia.com/v1/chat/completions\",\n model: \"meta/llama-3.3-70b-instruct\",\n keys: loadKeys(\"NVAPI_KEY\", 10).concat(loadKeys(\"NVIDIA_API_KEY\", 10)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n timeout: 45000,\n },\n // --- Inception: mercury-2 (diffusion-based LLM — unique scoring perspective) ---\n {\n id: \"inception\",\n name: \"Inception-Mercury2\",\n url: \"https://api.inceptionlabs.ai/v1/chat/completions\",\n model: \"mercury-2\",\n keys: loadKeys(\"INCEPTION_API_KEY\", 15).concat(loadKeys(\"INCEPTION_KEY\", 15)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 1024, // Mercury-2 needs at least 50 tokens (reasoning model)\n timeout: 45000,\n },\n // --- Xiaomi MiMo: 5 keys x 2 models = up to 2 independent judges ---\n {\n id: \"xiaomi-flash\",\n name: \"Xiaomi-MiMo-Flash\",\n url: \"https://api.xiaomimimo.com/v1/chat/completions\",\n model: \"mimo-v2-flash\",\n keys: loadKeys(\"XIAOMI_API_KEY\", 10).concat(loadKeys(\"XIAOMI_KEY\", 10)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n {\n id: \"xiaomi-pro\",\n name: \"Xiaomi-MiMo-Pro\",\n url: \"https://api.xiaomimimo.com/v1/chat/completions\",\n model: \"mimo-v2-pro\",\n keys: loadKeys(\"XIAOMI_API_KEY\", 10).concat(loadKeys(\"XIAOMI_KEY\", 10)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 1024,\n },\n // --- Cohere: command-a-reasoning model (9 keys, reasoning/thinking model) ---\n {\n id: \"cohere\",\n name: \"Cohere-CommandA\",\n url: \"https://api.cohere.com/v2/chat\",\n model: \"command-a-reasoning-08-2025\",\n keys: loadKeys(\"COHERE_API_KEY\", 15).concat(loadKeys(\"COHERE_KEY\", 15)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 4096, // reasoning model needs extra tokens for thinking + answer\n responseFormat: \"cohere\", // data.message.content[] array with {type:\"thinking\"} + {type:\"text\"}\n timeout: 90000, // 90s — reasoning model needs time to think through all 10 dimensions\n },\n // --- Cloudflare Workers AI: 6 models across 6 accounts (all FREE) ---\n // Ordered by model quality: best first\n {\n id: \"cloudflare-glm\",\n name: \"Cloudflare-GLM4\",\n url: `https://api.cloudflare.com/client/v4/accounts/eaffd2b52c95c69aaad8d859e9dcb52b/ai/run/@cf/zai-org/glm-4.7-flash`,\n model: \"@cf/zai-org/glm-4.7-flash\",\n keys: loadKeys(\"CF_AI_TOKEN\").concat(loadKeys(\"CLOUDFLARE_AI_TOKEN\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 2048,\n responseFormat: \"cloudflare\",\n timeout: 60000,\n },\n {\n id: \"cloudflare-gemma4\",\n name: \"Cloudflare-Gemma4\",\n url: `https://api.cloudflare.com/client/v4/accounts/a7995d3f33b6ba57955749337c9abbe0/ai/run/@cf/google/gemma-4-26b-a4b-it`,\n model: \"@cf/google/gemma-4-26b-a4b-it\",\n keys: loadKeys(\"CF_AI_TOKEN_2\"),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 1024,\n responseFormat: \"cloudflare\",\n timeout: 60000,\n },\n {\n id: \"cloudflare-nemotron\",\n name: \"Cloudflare-Nemotron\",\n url: `https://api.cloudflare.com/client/v4/accounts/194d9aea21482ac893ed81fc6b004864/ai/run/@cf/nvidia/nemotron-3-120b-a12b`,\n model: \"@cf/nvidia/nemotron-3-120b-a12b\",\n keys: loadKeys(\"CF_AI_TOKEN_3\"),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 1024,\n responseFormat: \"cloudflare\",\n timeout: 60000,\n },\n {\n id: \"cloudflare-kimi\",\n name: \"Cloudflare-Kimi\",\n url: `https://api.cloudflare.com/client/v4/accounts/401a75ead25275262c1c05eecb7a997c/ai/run/@cf/moonshotai/kimi-k2.5`,\n model: \"@cf/moonshotai/kimi-k2.5\",\n keys: loadKeys(\"CF_AI_TOKEN_4\"),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 2048,\n responseFormat: \"cloudflare\",\n timeout: 60000,\n },\n {\n id: \"cloudflare-gptoss\",\n name: \"Cloudflare-GPT-OSS\",\n url: `https://api.cloudflare.com/client/v4/accounts/73340519f6430362daee759ba0b48ce8/ai/run/@cf/openai/gpt-oss-120b`,\n model: \"@cf/openai/gpt-oss-120b\",\n keys: loadKeys(\"CF_AI_TOKEN_5\"),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 1024,\n responseFormat: \"cloudflare\",\n timeout: 60000,\n },\n {\n id: \"cloudflare-qwen3\",\n name: \"Cloudflare-Qwen3\",\n url: `https://api.cloudflare.com/client/v4/accounts/df4a7888befcbb6ce3e0a0b346ea1990/ai/run/@cf/qwen/qwen3-30b-a3b-fp8`,\n model: \"@cf/qwen/qwen3-30b-a3b-fp8\",\n keys: loadKeys(\"CF_AI_TOKEN_6\"),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 2048,\n responseFormat: \"cloudflare\",\n timeout: 60000,\n },\n // --- Cloudflare Workers AI: 3 NEW accounts (accounts 7/8/9) ---\n {\n id: \"cloudflare-llama4scout\",\n name: \"Cloudflare-Llama4Scout\",\n url: `https://api.cloudflare.com/client/v4/accounts/3cd084561890e5ab468456fae547ded0/ai/run/@cf/meta/llama-4-scout-17b-16e-instruct`,\n model: \"@cf/meta/llama-4-scout-17b-16e-instruct\",\n keys: loadKeys(\"CF_AI_TOKEN_7\"),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 1024,\n responseFormat: \"cloudflare\",\n timeout: 60000,\n },\n {\n id: \"cloudflare-mistral31\",\n name: \"Cloudflare-MistralSmall31\",\n url: `https://api.cloudflare.com/client/v4/accounts/27920eccf7d83f7ee267130cd6018eaf/ai/run/@cf/mistralai/mistral-small-3.1-24b-instruct`,\n model: \"@cf/mistralai/mistral-small-3.1-24b-instruct\",\n keys: loadKeys(\"CF_AI_TOKEN_8\"),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 1024,\n responseFormat: \"cloudflare\",\n timeout: 60000,\n },\n {\n id: \"cloudflare-deepseekr1\",\n name: \"Cloudflare-DeepSeekR1\",\n url: `https://api.cloudflare.com/client/v4/accounts/60c2dcaa7fc3377f036114648f6397ba/ai/run/@cf/deepseek-ai/deepseek-r1-distill-qwen-32b`,\n model: \"@cf/deepseek-ai/deepseek-r1-distill-qwen-32b\",\n keys: loadKeys(\"CF_AI_TOKEN_9\"),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 2048,\n responseFormat: \"cloudflare\",\n timeout: 60000,\n },\n // --- Cloudflare Workers AI: Account 10 (Agnuxo2026, GLM-4.7-flash) ---\n {\n id: \"cloudflare-glm47-10\",\n name: \"Cloudflare-GLM47-Acct10\",\n url: `https://api.cloudflare.com/client/v4/accounts/${process.env.CF_ACCOUNT_ID_10 || \"00c5ebf4df46d16450d5f1419dc36c6a\"}/ai/run/@cf/zai-org/glm-4.7-flash`,\n model: \"@cf/zai-org/glm-4.7-flash\",\n keys: loadKeys(\"CF_AI_TOKEN_10\"),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 2048,\n responseFormat: \"cloudflare\",\n timeout: 60000,\n },\n];\n\n// Deduplicate keys within each provider\nfor (const p of PROVIDERS) {\n p.keys = [...new Set(p.keys)].filter(Boolean);\n}\n\n// Log available providers\nconst available = PROVIDERS.filter(p => p.keys.length > 0);\nconsole.log(`[SCORING] ${available.length} LLM providers available: ${available.map(p => `${p.name}(${p.keys.length})`).join(\", \")}`);\nif (available.length === 0) console.warn(\"[SCORING] No LLM providers — heuristic scoring only.\");\n// Debug: log specifically which providers have NO keys\nconst unavailable = PROVIDERS.filter(p => p.keys.length === 0);\nif (unavailable.length > 0) console.log(`[SCORING] Providers with NO keys: ${unavailable.map(p => p.name).join(\", \")}`);\n\nconst SCORING_PROMPT = `You are a STRICT academic peer reviewer. You evaluate papers for the P2PCLAW benchmark.\n\nYour scoring must follow real academic standards. Most papers score 4-6. Only exceptional work reaches 7+. A score of 8+ means top-tier venue quality (NeurIPS, Nature, ICML). A 10 is historically significant (Turing Award level).\n\nCALIBRATION ANCHORS — use these to calibrate your scores:\n- 10/10 novelty: \"Attention Is All You Need\" (Vaswani 2017), Bitcoin whitepaper (Nakamoto 2008)\n- 8/10 novelty: A genuine new algorithm with proven improvements over SOTA\n- 6/10 novelty: Meaningful extension of existing work with some original insights\n- 5/10 novelty: Applying known techniques to a new domain (standard contribution)\n- 3/10 novelty: Minor variation of existing work, obvious next step\n- 1/10 novelty: Restating known results with different notation\n\nScore each criterion on 0-10:\n- abstract (0-10): Clarity, completeness, problem+scope+results summarized\n- introduction (0-10): Problem clarity, context, motivation. Needs 2+ real citations to related work\n- methodology (0-10): Rigor and reproducibility. Can someone replicate this exactly?\n- results (0-10): Strength of evidence. Real data, real experiments, statistical significance\n- discussion (0-10): Honest limitations, implications, comparison to prior work\n- conclusion (0-10): Summary of findings, concrete future directions\n- references (0-10): Real citations with authors, titles, years, DOIs. 8+ unique for score >5\n- novelty (0-10): TRUE original contribution. Applying a standard formula to a new domain = 4-5, NOT 8-9\n- reproducibility (0-10): Code, equations, parameters, data availability\n- citation_quality (0-10): Are references real, verified, and actually cited in the text?\n\nSTRICT RULES:\n- Missing section = 0 for that section\n- Placeholder/fake references = references score 1\n- Papers under 300 words = all scores below 3\n- Proving obvious things = novelty 0-1\n- Standard variance/mean/known formulas applied to new domain = novelty 4-5 MAX\n- No experimental data or only synthetic/estimated data = results 3-4 MAX\n- Self-referential citations or citing only own work = citation_quality 2-3\n- Circular reasoning (using conclusion to justify methodology) = methodology cap 4\n- Claims without evidence (e.g., \"we achieve SOTA\" with no comparison) = results cap 3\n- DO NOT give 8+ unless the work would genuinely be accepted at a top venue\n\nReturn ONLY this JSON (numbers 0-10):\n{\"abstract\":N,\"introduction\":N,\"methodology\":N,\"results\":N,\"discussion\":N,\"conclusion\":N,\"references\":N,\"novelty\":N,\"reproducibility\":N,\"citation_quality\":N}\n\nPaper content:\n`;\n\n/**\n * Call a single LLM provider to score a paper.\n * Tries up to 2 keys from the provider before giving up.\n */\nasync function callLLMForScoring(prompt, provider) {\n if (!provider.keys.length) return null;\n\n const maxAttempts = Math.min(provider.keys.length, 2);\n for (let attempt = 0; attempt < maxAttempts; attempt++) {\n const key = nextKey(provider.id, provider.keys);\n if (!key) return null;\n\n const headers = { \"Content-Type\": \"application/json\" };\n headers[provider.authHeader] = `${provider.authPrefix}${key}`;\n if (provider.extraHeaders) Object.assign(headers, provider.extraHeaders);\n\n try {\n const res = await fetch(provider.url, {\n method: \"POST\",\n headers,\n body: JSON.stringify({\n model: provider.model,\n messages: [{ role: \"user\", content: prompt }],\n max_tokens: provider.maxTokens || 512,\n temperature: 0.1,\n }),\n signal: AbortSignal.timeout(provider.timeout || 30000),\n });\n\n if (res.status === 429) {\n console.warn(`[SCORING] ${provider.name} 429 rate-limited`);\n await new Promise(r => setTimeout(r, 3000));\n continue;\n }\n if (res.status === 402) {\n console.warn(`[SCORING] ${provider.name} 402 credits exhausted — skipping provider`);\n return null;\n }\n if (!res.ok) {\n const errBody = await res.text().catch(() => \"\");\n console.warn(`[SCORING] ${provider.name} HTTP ${res.status}: ${errBody.substring(0, 150)}`);\n continue;\n }\n\n const data = await res.json();\n // Support both OpenAI format (choices[0].message.content) and Cohere v2 (message.content[] array)\n let text = \"\";\n if (provider.responseFormat === \"cohere\") {\n // Cohere v2 returns array of content blocks: [{type:\"thinking\",...}, {type:\"text\",text:\"...\"}]\n const blocks = data.message?.content || [];\n if (Array.isArray(blocks)) {\n const textBlock = blocks.find(b => b.type === \"text\");\n text = textBlock?.text || blocks[blocks.length - 1]?.text || \"\";\n } else {\n text = typeof blocks === \"string\" ? blocks : \"\";\n }\n } else if (provider.responseFormat === \"cloudflare\") {\n // Cloudflare Workers AI wraps OpenAI format: {result: {choices: [...]}, success: true}\n const inner = data.result || data;\n text = inner.choices?.[0]?.message?.content || \"\";\n } else {\n text = data.choices?.[0]?.message?.content || \"\";\n }\n\n // Strip <think>...</think> tags (Sarvam, Qwen with thinking mode)\n if (provider.stripThinkTags || text.includes(\"<think>\")) {\n // Strip closed think blocks\n text = text.replace(/<think>[\\s\\S]*?<\\/think>/g, \"\").trim();\n // Strip unclosed think blocks (model ran out of tokens mid-think)\n if (text.startsWith(\"<think>\")) {\n text = text.replace(/<think>[\\s\\S]*/g, \"\").trim();\n }\n }\n\n // Extract JSON — use balanced braces to handle nested objects like {\"score\":8,\"why\":\"...\"}\n let jsonMatch = null;\n const firstBrace = text.indexOf('{');\n if (firstBrace !== -1) {\n let depth = 0, end = -1;\n for (let i = firstBrace; i < text.length; i++) {\n if (text[i] === '{') depth++;\n else if (text[i] === '}') { depth--; if (depth === 0) { end = i; break; } }\n }\n if (end !== -1) jsonMatch = [text.substring(firstBrace, end + 1)];\n }\n if (!jsonMatch) {\n console.warn(`[SCORING] ${provider.name} — no JSON in response`);\n continue;\n }\n\n const parsed = JSON.parse(jsonMatch[0]);\n const fields = [...SECTIONS, \"novelty\", \"reproducibility\", \"citation_quality\"];\n const feedback = {};\n for (const field of fields) {\n let val = parsed[field];\n // Support both plain number and {score, why} object format\n if (val && typeof val === \"object\" && typeof val.score === \"number\") {\n feedback[field] = val.why || null;\n val = val.score;\n }\n if (typeof val !== \"number\" || val < 0 || val > 10) {\n val = typeof val === \"number\" ? Math.max(0, Math.min(10, Math.round(val))) : 5;\n }\n parsed[field] = val;\n }\n\n // Clean parsed: only keep recognized score fields (remove stray \"why\" etc.)\n const cleanScores = {};\n for (const field of fields) cleanScores[field] = parsed[field];\n\n console.log(`[SCORING] ${provider.name} scored successfully`);\n return { scores: cleanScores, provider: provider.name, feedback: Object.keys(feedback).length > 0 ? feedback : null };\n } catch (e) {\n console.warn(`[SCORING] ${provider.name} error: ${e.message}`);\n }\n }\n return null;\n}\n\n/**\n * Deterministic heuristic scoring — used when all LLMs fail.\n */\nfunction heuristicScore(content) {\n const text = content || \"\";\n const words = text.split(/\\s+/).filter(w => w.length > 0);\n const wordCount = words.length;\n const lowerText = text.toLowerCase();\n\n const sectionScores = {};\n for (const section of SECTIONS) {\n const hasSection = lowerText.includes(`## ${section}`) || lowerText.includes(`# ${section}`);\n if (!hasSection) {\n sectionScores[section] = 0;\n continue;\n }\n const sectionRegex = new RegExp(`##?\\\\s*${section}[\\\\s\\\\S]*?(?=##?\\\\s|$)`, \"i\");\n const match = text.match(sectionRegex);\n const sectionWords = match ? match[0].split(/\\s+/).length : 0;\n if (sectionWords < 20) sectionScores[section] = 2;\n else if (sectionWords < 50) sectionScores[section] = 4;\n else if (sectionWords < 100) sectionScores[section] = 5;\n else if (sectionWords < 200) sectionScores[section] = 6;\n else sectionScores[section] = 7;\n }\n\n const refMatches = text.match(/\\[\\d+\\]/g) || [];\n const uniqueRefs = new Set(refMatches).size;\n const hasPlaceholderRefs = /placeholder|author,?\\s*a\\.\\s*\\(\\d{4}\\)/i.test(text);\n const hasRealAuthors = /[A-Z][a-z]+,\\s*[A-Z]\\.\\s*(?:&|,|et al)/g.test(text);\n const hasDOI = /doi\\.org|arxiv\\.org|10\\.\\d{4}/i.test(text);\n let refScore = hasPlaceholderRefs ? 1 : Math.min(7, uniqueRefs);\n if (hasRealAuthors) refScore = Math.min(10, refScore + 1);\n if (hasDOI) refScore = Math.min(10, refScore + 1);\n sectionScores.references = refScore;\n\n const technicalTerms = (text.match(/\\b(algorithm|theorem|proof|complexity|O\\([^)]+\\)|convergence|optimal|novel|framework)\\b/gi) || []).length;\n const hasFigures = /figure \\d|fig\\.\\s*\\d|table \\d/i.test(text);\n let novelty = wordCount > 2000 ? 4 : wordCount > 1000 ? 3 : 2;\n novelty += Math.min(3, Math.floor(technicalTerms / 5));\n if (hasFigures) novelty += 1;\n\n const hasCode = /```[\\s\\S]*?```/.test(text);\n const hasEquations = /\\$[^$]+\\$/.test(text) || /\\\\begin\\{/.test(text);\n const hasNumbers = /\\d+\\.\\d+%|\\d+\\.\\d+x|p\\s*[<>]\\s*0\\.\\d/i.test(text);\n let reproducibility = (hasCode ? 5 : 3) + (hasEquations ? 1 : 0) + (wordCount > 2000 ? 1 : 0);\n if (hasNumbers) reproducibility += 1;\n\n const citation_quality = hasPlaceholderRefs ? 1 : Math.min(6, uniqueRefs);\n\n return {\n scores: {\n ...sectionScores,\n novelty: Math.min(10, novelty),\n reproducibility: Math.min(10, reproducibility),\n citation_quality: Math.min(10, citation_quality),\n },\n provider: \"heuristic\"\n };\n}\n\n/**\n * Score a paper using heterogeneous multi-LLM swarm.\n * Tries to get 2 independent judges, falls back through provider chain.\n */\nexport async function scoreGranular(content, paperType = \"research\") {\n if (!content || content.trim().length < 50) {\n return {\n sections: Object.fromEntries(SECTIONS.map(s => [s, 0])),\n overall: 0,\n novelty: 0,\n reproducibility: 0,\n citation_quality: 0,\n judges: [],\n judge_count: 0,\n error: \"Content too short to score\"\n };\n }\n\n // All LLM judges support 32k+ context. Send enough content to include ALL 7 mandatory sections.\n // Previous limit of 4000 chars truncated papers before methodology/results/conclusion, causing\n // strict judges (Cohere, Xiaomi) to correctly score missing sections as 0.\n const truncated = content.length > 16000 ? content.substring(0, 16000) + \"\\n\\n[... truncated for scoring ...]\" : content;\n const prompt = SCORING_PROMPT + truncated;\n\n // ALL available judges score independently for maximum consensus diversity.\n // Each model evaluates each section independently, then we average.\n // Run all judges in parallel for speed (each has its own timeout).\n const judgePromises = available.map(provider =>\n callLLMForScoring(prompt, provider).catch(() => null)\n );\n const judgeResults = await Promise.all(judgePromises);\n const judges = judgeResults.filter(Boolean);\n\n // If no LLM judges succeeded, use heuristic\n if (judges.length === 0) {\n const heuristic = heuristicScore(content);\n judges.push(heuristic);\n }\n\n // Average scores across all judges\n const allFields = [...SECTIONS, \"novelty\", \"reproducibility\", \"citation_quality\"];\n const averaged = {};\n for (const field of allFields) {\n const values = judges.map(j => j.scores[field]).filter(v => typeof v === \"number\");\n averaged[field] = values.length > 0\n ? Math.round((values.reduce((a, b) => a + b, 0) / values.length) * 10) / 10\n : 0;\n }\n\n // ── CALIBRATION PASS — adjust raw averages against reference benchmarks ──\n // This is the core fix for inflated scoring. Raw LLM averages are compared\n // against quality signals extracted from the paper content and calibrated\n // against recognized reference paper fingerprints.\n let calibration = null;\n try {\n const fieldResult = detectField(content);\n const signals = extractSignals(content);\n const benchmarks = REFERENCE_BENCHMARKS[fieldResult.field] || null;\n const { calibrated, adjustments } = calibrateScores(averaged, signals, benchmarks);\n\n // Apply calibrated scores over raw averages\n const adjustmentCount = Object.keys(adjustments).length;\n if (adjustmentCount > 0) {\n for (const [field, val] of Object.entries(calibrated)) {\n if (typeof val === \"number\" && averaged[field] !== undefined) {\n averaged[field] = val;\n }\n }\n console.log(`[SCORING] Calibration applied: ${adjustmentCount} adjustments, field=${fieldResult.field}, ` +\n `red_flags=${signals.red_flag_count}, depth=${signals.depth_score}`);\n }\n\n calibration = {\n field: fieldResult.field,\n field_confidence: fieldResult.confidence,\n signals_summary: {\n word_count: signals.word_count,\n sections_present: signals.sections_present.length,\n sections_missing: signals.sections_missing,\n red_flags: signals.red_flags,\n red_flag_count: signals.red_flag_count,\n has_formal_proofs: signals.has_formal_proofs,\n has_equations: signals.has_equations,\n has_code: signals.has_code,\n unique_refs: signals.unique_refs,\n has_placeholder_refs: signals.has_placeholder_refs,\n depth_score: signals.depth_score,\n evidence_markers: signals.evidence_markers,\n deception_count: signals.deception_count || 0,\n deception_matches: (signals.deception_matches || []).map(d => ({\n id: d.id, name: d.name, severity: d.severity,\n })),\n // New quality dimensions\n grammar: signals.grammar_quality ? {\n vocabulary_diversity: signals.grammar_quality.vocabulary_diversity_ttr,\n is_monotone: signals.grammar_quality.is_monotone,\n is_low_vocabulary: signals.grammar_quality.is_low_vocabulary,\n } : null,\n repetition_ratio: signals.repetition_score?.repetition_ratio || 0,\n code_quality: signals.code_quality?.blocks_found > 0 ? {\n blocks: signals.code_quality.blocks_found,\n has_real_code: signals.has_real_code || false,\n has_python: signals.code_quality.has_python || false,\n } : null,\n math_formulas: signals.math_quality?.formula_count || 0,\n lean4: signals.lean4_signals?.verification_level || \"none\",\n tables: signals.table_quality?.count || 0,\n },\n adjustments,\n adjustment_count: adjustmentCount,\n reference_papers: benchmarks ? benchmarks.references.map(r => r.title) : [],\n };\n } catch (calErr) {\n console.warn(`[SCORING] Calibration error (non-fatal): ${calErr.message}`);\n }\n\n // ── LIVE VERIFICATION PASS — real-time CrossRef, arXiv, code exec, Lean4 ──\n // Runs in parallel with independent timeouts. Non-fatal: if it fails, scoring\n // continues with just calibration. Results apply caps and bonuses to scores.\n let liveVerification = null;\n try {\n const verification = await runLiveVerification(content);\n const { adjustments: liveAdj, bonuses: liveBon } = verificationToAdjustments(verification);\n\n // ── OVERRIDE FALSE POSITIVES ──\n // If live verification confirmed code execution but calibration flagged\n // \"code_blocks_are_template_not_real\", the red_flag was a false positive.\n // Undo the -1.5 penalty applied to all dimensions by recalibrating.\n const codeExec = verification.code_execution;\n if (codeExec && codeExec.passed > 0 && calibration && calibration.signals_summary) {\n const redFlags = calibration.signals_summary.red_flags || [];\n if (redFlags.includes(\"code_blocks_are_template_not_real\")) {\n // The -1.5 penalty was applied to all 10 dimensions. Undo it.\n const allFields = [...SECTIONS, \"novelty\", \"reproducibility\", \"citation_quality\"];\n for (const field of allFields) {\n if (averaged[field] !== undefined) {\n averaged[field] = Math.min(10, Math.round((averaged[field] + 1.5) * 10) / 10);\n }\n }\n // Remove the false red flag from the report\n calibration.signals_summary.red_flags = redFlags.filter(f => f !== \"code_blocks_are_template_not_real\");\n calibration.signals_summary.red_flag_count = calibration.signals_summary.red_flags.length;\n calibration.false_positive_corrected = \"code_blocks_are_template_not_real (live verification confirmed code executes)\";\n console.log(`[SCORING] False positive corrected: code_blocks_are_template_not_real (live verification: ${codeExec.passed}/${codeExec.total} passed)`);\n }\n }\n\n // Apply caps (these override score to a max value)\n for (const [key, val] of Object.entries(liveAdj)) {\n if (key.endsWith(\"_cap\") && typeof val === \"number\") {\n const field = key.replace(\"_cap\", \"\");\n if (averaged[field] !== undefined && averaged[field] > val) {\n averaged[field] = val;\n }\n }\n }\n\n // Apply bonuses (add to score, capped at 10)\n for (const [key, val] of Object.entries(liveBon)) {\n if (key.endsWith(\"_bonus\") && typeof val === \"number\") {\n const field = key.replace(\"_bonus\", \"\");\n if (averaged[field] !== undefined) {\n averaged[field] = Math.min(10, Math.round((averaged[field] + val) * 10) / 10);\n }\n }\n }\n\n liveVerification = {\n verification_time_ms: verification.verification_time_ms,\n citations: verification.citations ? {\n total: verification.citations.total,\n verified: verification.citations.verified,\n verification_rate: verification.citations.verification_rate,\n } : null,\n novelty: verification.novelty ? {\n searched: verification.novelty.searched,\n total_found: verification.novelty.total_found,\n novelty_concern: verification.novelty.novelty_concern,\n max_similarity: verification.novelty.max_similarity,\n } : null,\n code_execution: verification.code_execution ? {\n total: verification.code_execution.total,\n passed: verification.code_execution.passed,\n failed: verification.code_execution.failed,\n } : null,\n lean4: verification.lean4 ? {\n blocks_found: verification.lean4.blocks_found,\n verified: verification.lean4.verified,\n has_unsubstantiated_claim: verification.lean4.has_unsubstantiated_claim || false,\n } : null,\n adjustments: liveAdj,\n bonuses: liveBon,\n };\n\n const adjCount = Object.keys(liveAdj).filter(k => k.endsWith(\"_cap\")).length;\n const bonCount = Object.keys(liveBon).filter(k => k.endsWith(\"_bonus\")).length;\n if (adjCount > 0 || bonCount > 0) {\n console.log(`[SCORING] Live verification: ${adjCount} caps, ${bonCount} bonuses applied (${verification.verification_time_ms}ms)`);\n }\n } catch (liveErr) {\n console.warn(`[SCORING] Live verification error (non-fatal): ${liveErr.message}`);\n }\n\n const sectionValues = SECTIONS.map(s => averaged[s]);\n let overall = Math.round((sectionValues.reduce((a, b) => a + b, 0) / SECTIONS.length) * 10) / 10;\n\n // ── Phase F: Execution Proof Bonus ──\n // Papers with verified code blocks (execution hashes) get a purely additive overall bonus.\n // This only increases the score, never decreases. Capped at 10.\n if (liveVerification && liveVerification.bonuses && liveVerification.bonuses.execution_proof_bonus) {\n overall = Math.min(10, Math.round((overall + liveVerification.bonuses.execution_proof_bonus) * 10) / 10);\n console.log(`[SCORING] Execution proof bonus applied: +${liveVerification.bonuses.execution_proof_bonus} → overall=${overall}`);\n }\n\n // Per-judge detail breakdown (individual scores + feedback)\n const judge_details = judges.map(j => ({\n judge: j.provider,\n scores: j.scores,\n feedback: j.feedback || null,\n }));\n\n // Consensus score per dimension (1 = all judges agree, 0 = total disagreement)\n const consensus = {};\n for (const field of allFields) {\n const values = judges.map(j => j.scores[field]).filter(v => typeof v === \"number\");\n if (values.length < 2) { consensus[field] = 1.0; continue; }\n const mean = values.reduce((a, b) => a + b, 0) / values.length;\n const variance = values.reduce((sum, v) => sum + (v - mean) ** 2, 0) / values.length;\n const stddev = Math.sqrt(variance);\n consensus[field] = Math.round(Math.max(0, 1 - stddev / 5) * 100) / 100;\n }\n const overall_consensus = Math.round(\n (allFields.reduce((sum, f) => sum + consensus[f], 0) / allFields.length) * 100\n ) / 100;\n\n // Aggregate feedback per dimension (from all judges that provided it)\n const aggregated_feedback = {};\n for (const field of allFields) {\n const comments = judges\n .filter(j => j.feedback && j.feedback[field])\n .map(j => ({ judge: j.provider, comment: j.feedback[field] }));\n if (comments.length > 0) aggregated_feedback[field] = comments;\n }\n\n const result = {\n sections: Object.fromEntries(SECTIONS.map(s => [s, averaged[s]])),\n overall,\n novelty: averaged.novelty,\n reproducibility: averaged.reproducibility,\n citation_quality: averaged.citation_quality,\n judges: judges.map(j => j.provider),\n judge_count: judges.length,\n judge_details,\n consensus,\n overall_consensus,\n feedback: Object.keys(aggregated_feedback).length > 0 ? aggregated_feedback : null,\n scored_at: new Date().toISOString(),\n paper_type: paperType,\n calibration,\n live_verification: liveVerification,\n };\n\n console.log(`[SCORING] Granular score: overall=${overall}, consensus=${overall_consensus}, judges=${result.judges.join(\",\")}`);\n return result;\n}\n"}, "efdb37e136f19e611efe389ee752eafdea4ab576": {"t": 1785753925.0102754, "n": 37660, "h": "15a8e49584268d60", "c": "/**\n * P2PCLAW Granular Scoring Service — MAXIMUM TRIBUNAL\n * ====================================================\n * Heterogeneous multi-LLM scoring engine that evaluates papers section-by-section.\n * Updated 2026-04-04: ALL available API keys deployed for maximum judge diversity.\n *\n * Provider chain (updated 2026-04-04):\n * 1. Cerebras — qwen-3-235b-a22b (8 keys, free, ultra-fast)\n * 2. Cerebras — llama3.1-8b (8 keys, free, ultra-fast)\n * 3. Cerebras — gpt-oss-120b (8 keys, free, different model perspective)\n * 4. Cerebras — zai-glm-4.7 (8 keys, free, Chinese model perspective)\n * 5. Mistral — mistral-small-latest (3 keys, free)\n * 6. Sarvam — sarvam-m (13 keys, Indian AI, free)\n * 7. OpenRouter — qwen3-coder:free (3 keys, free)\n * 8. Groq — llama-3.3-70b-versatile (9 keys)\n * 9. NVIDIA — meta/llama-3.3-70b-instruct (3 keys, free)\n * 10. Inception — mercury-2 (9 keys, free, diffusion-based)\n * 11. Xiaomi MiMo — mimo-v2-flash (5 keys, free)\n * 12. Xiaomi MiMo — mimo-v2-pro (5 keys, free, reasoning)\n * 13. Cohere — command-a-reasoning (9 keys, reasoning model)\n * 14-22. Cloudflare — 9 accounts x unique models (GLM4, Gemma4, Nemotron, Kimi, GPT-OSS, Qwen3, Llama4Scout, MistralSmall31, DeepSeekR1)\n * 23. Cloudflare — account 10 (GLM-4.7-flash, additional account)\n * 24. Deterministic heuristic fallback (never blocks)\n *\n * TOTAL: 23 independent LLM judges + 1 heuristic = 24 scoring perspectives\n * ALL available judges score independently. Final score = average across all judges.\n * Each model evaluates each section independently for maximum consensus diversity.\n */\n\nimport { detectField, extractSignals, calibrateScores, REFERENCE_BENCHMARKS } from \"./calibrationService.js\";\nimport { runLiveVerification, verificationToAdjustments } from \"./liveVerificationService.js\";\n\nconst SECTIONS = [\"abstract\", \"introduction\", \"methodology\", \"results\", \"discussion\", \"conclusion\", \"references\"];\n\n// ── Load keys with rotation ─────────────────────────────────────────────────\n\nfunction loadKeys(envPrefix, maxKeys = 10) {\n const keys = [];\n for (let i = 1; i <= maxKeys; i++) {\n const k = process.env[`${envPrefix}_${i}`] || process.env[`${envPrefix}${i}`];\n if (k) keys.push(k);\n }\n // Also check single-key env vars\n const single = process.env[envPrefix];\n if (single && !keys.includes(single)) keys.unshift(single);\n return keys;\n}\n\nconst keyIndices = {};\nfunction nextKey(providerId, keys) {\n if (!keys.length) return null;\n const idx = keyIndices[providerId] || 0;\n const key = keys[idx % keys.length];\n keyIndices[providerId] = (idx + 1) % keys.length;\n return key;\n}\n\n// ── Provider definitions ────────────────────────────────────────────────────\n\nconst PROVIDERS = [\n // --- Cerebras: 3 keys x 2 models = up to 6 independent judges ---\n {\n id: \"cerebras-qwen\",\n name: \"Cerebras-Qwen235B\",\n url: \"https://api.cerebras.ai/v1/chat/completions\",\n model: \"qwen-3-235b-a22b-instruct-2507\",\n keys: loadKeys(\"CEREBRAS_API_KEY\", 15).concat(loadKeys(\"CEREBRAS_KEY\", 15)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 1024,\n },\n {\n id: \"cerebras-llama\",\n name: \"Cerebras-Llama8B\",\n url: \"https://api.cerebras.ai/v1/chat/completions\",\n model: \"llama3.1-8b\",\n keys: loadKeys(\"CEREBRAS_API_KEY\", 15).concat(loadKeys(\"CEREBRAS_KEY\", 15)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n // --- Cerebras: GPT-OSS-120B (120B param open-source model, different perspective) ---\n {\n id: \"cerebras-gptoss\",\n name: \"Cerebras-GPT-OSS-120B\",\n url: \"https://api.cerebras.ai/v1/chat/completions\",\n model: \"gpt-oss-120b\",\n keys: loadKeys(\"CEREBRAS_API_KEY\", 15).concat(loadKeys(\"CEREBRAS_KEY\", 15)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n // --- Cerebras: ZAI GLM-4.7 (Chinese model — different cultural perspective on academic rigor) ---\n {\n id: \"cerebras-glm47\",\n name: \"Cerebras-GLM4.7\",\n url: \"https://api.cerebras.ai/v1/chat/completions\",\n model: \"zai-glm-4.7\",\n keys: loadKeys(\"CEREBRAS_API_KEY\", 15).concat(loadKeys(\"CEREBRAS_KEY\", 15)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 1024,\n },\n // --- Mistral: 3 keys, reliable ---\n {\n id: \"mistral\",\n name: \"Mistral\",\n url: \"https://api.mistral.ai/v1/chat/completions\",\n model: \"mistral-small-latest\",\n keys: loadKeys(\"MISTRAL_API_KEY\", 10).concat(loadKeys(\"MISTRAL_KEY\", 10)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n // --- Sarvam (Indian AI): sarvam-m, uses <think> tags (needs 2048+ tokens), 13 keys ---\n {\n id: \"sarvam\",\n name: \"Sarvam\",\n url: \"https://api.sarvam.ai/v1/chat/completions\",\n model: \"sarvam-m\",\n keys: loadKeys(\"SARVAM_KEY\", 15).concat(loadKeys(\"SARVAM_API_KEY\", 15)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 2048,\n },\n // --- OpenRouter: free models ---\n {\n id: \"openrouter\",\n name: \"OpenRouter\",\n url: \"https://openrouter.ai/api/v1/chat/completions\",\n model: \"qwen/qwen3-coder:free\",\n keys: loadKeys(\"OPENROUTER_API_KEY\"),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n extraHeaders: { \"HTTP-Referer\": \"https://www.p2pclaw.com\", \"X-Title\": \"P2PCLAW Scoring\" },\n },\n // --- Groq: 9 keys but may be org-restricted ---\n {\n id: \"groq\",\n name: \"Groq\",\n url: \"https://api.groq.com/openai/v1/chat/completions\",\n model: \"llama-3.3-70b-versatile\",\n keys: loadKeys(\"GROQ_API_KEY\", 15).concat(loadKeys(\"GROQ_KEY\", 15)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n // --- NVIDIA: 3 keys ---\n {\n id: \"nvidia\",\n name: \"NVIDIA\",\n url: \"https://integrate.api.nvidia.com/v1/chat/completions\",\n model: \"meta/llama-3.3-70b-instruct\",\n keys: loadKeys(\"NVAPI_KEY\", 10).concat(loadKeys(\"NVIDIA_API_KEY\", 10)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n timeout: 45000,\n },\n // --- Inception: mercury-2 (diffusion-based LLM — unique scoring perspective) ---\n {\n id: \"inception\",\n name: \"Inception-Mercury2\",\n url: \"https://api.inceptionlabs.ai/v1/chat/completions\",\n model: \"mercury-2\",\n keys: loadKeys(\"INCEPTION_API_KEY\", 15).concat(loadKeys(\"INCEPTION_KEY\", 15)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 1024, // Mercury-2 needs at least 50 tokens (reasoning model)\n timeout: 45000,\n },\n // --- Xiaomi MiMo: 5 keys x 2 models = up to 2 independent judges ---\n {\n id: \"xiaomi-flash\",\n name: \"Xiaomi-MiMo-Flash\",\n url: \"https://api.xiaomimimo.com/v1/chat/completions\",\n model: \"mimo-v2-flash\",\n keys: loadKeys(\"XIAOMI_API_KEY\", 10).concat(loadKeys(\"XIAOMI_KEY\", 10)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n {\n id: \"xiaomi-pro\",\n name: \"Xiaomi-MiMo-Pro\",\n url: \"https://api.xiaomimimo.com/v1/chat/completions\",\n model: \"mimo-v2-pro\",\n keys: loadKeys(\"XIAOMI_API_KEY\", 10).concat(loadKeys(\"XIAOMI_KEY\", 10)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 1024,\n },\n // --- Cohere: command-a-reasoning model (9 keys, reasoning/thinking model) ---\n {\n id: \"cohere\",\n name: \"Cohere-CommandA\",\n url: \"https://api.cohere.com/v2/chat\",\n model: \"command-a-reasoning-08-2025\",\n keys: loadKeys(\"COHERE_API_KEY\", 15).concat(loadKeys(\"COHERE_KEY\", 15)),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 4096, // reasoning model needs extra tokens for thinking + answer\n responseFormat: \"cohere\", // data.message.content[] array with {type:\"thinking\"} + {type:\"text\"}\n timeout: 90000, // 90s — reasoning model needs time to think through all 10 dimensions\n },\n // --- Cloudflare Workers AI: 6 models across 6 accounts (all FREE) ---\n // Ordered by model quality: best first\n {\n id: \"cloudflare-glm\",\n name: \"Cloudflare-GLM4\",\n url: `https://api.cloudflare.com/client/v4/accounts/eaffd2b52c95c69aaad8d859e9dcb52b/ai/run/@cf/zai-org/glm-4.7-flash`,\n model: \"@cf/zai-org/glm-4.7-flash\",\n keys: loadKeys(\"CF_AI_TOKEN\").concat(loadKeys(\"CLOUDFLARE_AI_TOKEN\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 2048,\n responseFormat: \"cloudflare\",\n timeout: 60000,\n },\n {\n id: \"cloudflare-gemma4\",\n name: \"Cloudflare-Gemma4\",\n url: `https://api.cloudflare.com/client/v4/accounts/a7995d3f33b6ba57955749337c9abbe0/ai/run/@cf/google/gemma-4-26b-a4b-it`,\n model: \"@cf/google/gemma-4-26b-a4b-it\",\n keys: loadKeys(\"CF_AI_TOKEN_2\"),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 1024,\n responseFormat: \"cloudflare\",\n timeout: 60000,\n },\n {\n id: \"cloudflare-nemotron\",\n name: \"Cloudflare-Nemotron\",\n url: `https://api.cloudflare.com/client/v4/accounts/194d9aea21482ac893ed81fc6b004864/ai/run/@cf/nvidia/nemotron-3-120b-a12b`,\n model: \"@cf/nvidia/nemotron-3-120b-a12b\",\n keys: loadKeys(\"CF_AI_TOKEN_3\"),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 1024,\n responseFormat: \"cloudflare\",\n timeout: 60000,\n },\n {\n id: \"cloudflare-kimi\",\n name: \"Cloudflare-Kimi\",\n url: `https://api.cloudflare.com/client/v4/accounts/401a75ead25275262c1c05eecb7a997c/ai/run/@cf/moonshotai/kimi-k2.5`,\n model: \"@cf/moonshotai/kimi-k2.5\",\n keys: loadKeys(\"CF_AI_TOKEN_4\"),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 2048,\n responseFormat: \"cloudflare\",\n timeout: 60000,\n },\n {\n id: \"cloudflare-gptoss\",\n name: \"Cloudflare-GPT-OSS\",\n url: `https://api.cloudflare.com/client/v4/accounts/73340519f6430362daee759ba0b48ce8/ai/run/@cf/openai/gpt-oss-120b`,\n model: \"@cf/openai/gpt-oss-120b\",\n keys: loadKeys(\"CF_AI_TOKEN_5\"),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 1024,\n responseFormat: \"cloudflare\",\n timeout: 60000,\n },\n {\n id: \"cloudflare-qwen3\",\n name: \"Cloudflare-Qwen3\",\n url: `https://api.cloudflare.com/client/v4/accounts/df4a7888befcbb6ce3e0a0b346ea1990/ai/run/@cf/qwen/qwen3-30b-a3b-fp8`,\n model: \"@cf/qwen/qwen3-30b-a3b-fp8\",\n keys: loadKeys(\"CF_AI_TOKEN_6\"),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 2048,\n responseFormat: \"cloudflare\",\n timeout: 60000,\n },\n // --- Cloudflare Workers AI: 3 NEW accounts (accounts 7/8/9) ---\n {\n id: \"cloudflare-llama4scout\",\n name: \"Cloudflare-Llama4Scout\",\n url: `https://api.cloudflare.com/client/v4/accounts/3cd084561890e5ab468456fae547ded0/ai/run/@cf/meta/llama-4-scout-17b-16e-instruct`,\n model: \"@cf/meta/llama-4-scout-17b-16e-instruct\",\n keys: loadKeys(\"CF_AI_TOKEN_7\"),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 1024,\n responseFormat: \"cloudflare\",\n timeout: 60000,\n },\n {\n id: \"cloudflare-mistral31\",\n name: \"Cloudflare-MistralSmall31\",\n url: `https://api.cloudflare.com/client/v4/accounts/27920eccf7d83f7ee267130cd6018eaf/ai/run/@cf/mistralai/mistral-small-3.1-24b-instruct`,\n model: \"@cf/mistralai/mistral-small-3.1-24b-instruct\",\n keys: loadKeys(\"CF_AI_TOKEN_8\"),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 1024,\n responseFormat: \"cloudflare\",\n timeout: 60000,\n },\n {\n id: \"cloudflare-deepseekr1\",\n name: \"Cloudflare-DeepSeekR1\",\n url: `https://api.cloudflare.com/client/v4/accounts/60c2dcaa7fc3377f036114648f6397ba/ai/run/@cf/deepseek-ai/deepseek-r1-distill-qwen-32b`,\n model: \"@cf/deepseek-ai/deepseek-r1-distill-qwen-32b\",\n keys: loadKeys(\"CF_AI_TOKEN_9\"),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 2048,\n responseFormat: \"cloudflare\",\n timeout: 60000,\n },\n // --- Cloudflare Workers AI: Account 10 (Agnuxo2026, GLM-4.7-flash) ---\n {\n id: \"cloudflare-glm47-10\",\n name: \"Cloudflare-GLM47-Acct10\",\n url: `https://api.cloudflare.com/client/v4/accounts/${process.env.CF_ACCOUNT_ID_10 || \"00c5ebf4df46d16450d5f1419dc36c6a\"}/ai/run/@cf/zai-org/glm-4.7-flash`,\n model: \"@cf/zai-org/glm-4.7-flash\",\n keys: loadKeys(\"CF_AI_TOKEN_10\"),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 2048,\n responseFormat: \"cloudflare\",\n timeout: 60000,\n },\n];\n\n// Deduplicate keys within each provider\nfor (const p of PROVIDERS) {\n p.keys = [...new Set(p.keys)].filter(Boolean);\n}\n\n// Log available providers\nconst available = PROVIDERS.filter(p => p.keys.length > 0);\nconsole.log(`[SCORING] ${available.length} LLM providers available: ${available.map(p => `${p.name}(${p.keys.length})`).join(\", \")}`);\nif (available.length === 0) console.warn(\"[SCORING] No LLM providers — heuristic scoring only.\");\n// Debug: log specifically which providers have NO keys\nconst unavailable = PROVIDERS.filter(p => p.keys.length === 0);\nif (unavailable.length > 0) console.log(`[SCORING] Providers with NO keys: ${unavailable.map(p => p.name).join(\", \")}`);\n\nconst SCORING_PROMPT = `You are a STRICT academic peer reviewer. You evaluate papers for the P2PCLAW benchmark.\n\nYour scoring must follow real academic standards. Most papers score 4-6. Only exceptional work reaches 7+. A score of 8+ means top-tier venue quality (NeurIPS, Nature, ICML). A 10 is historically significant (Turing Award level).\n\nCALIBRATION ANCHORS — use these to calibrate your scores:\n- 10/10 novelty: \"Attention Is All You Need\" (Vaswani 2017), Bitcoin whitepaper (Nakamoto 2008)\n- 8/10 novelty: A genuine new algorithm with proven improvements over SOTA\n- 6/10 novelty: Meaningful extension of existing work with some original insights\n- 5/10 novelty: Applying known techniques to a new domain (standard contribution)\n- 3/10 novelty: Minor variation of existing work, obvious next step\n- 1/10 novelty: Restating known results with different notation\n\nScore each criterion on 0-10:\n- abstract (0-10): Clarity, completeness, problem+scope+results summarized\n- introduction (0-10): Problem clarity, context, motivation. Needs 2+ real citations to related work\n- methodology (0-10): Rigor and reproducibility. Can someone replicate this exactly?\n- results (0-10): Strength of evidence. Real data, real experiments, statistical significance\n- discussion (0-10): Honest limitations, implications, comparison to prior work\n- conclusion (0-10): Summary of findings, concrete future directions\n- references (0-10): Real citations with authors, titles, years, DOIs. 8+ unique for score >5\n- novelty (0-10): TRUE original contribution. Applying a standard formula to a new domain = 4-5, NOT 8-9\n- reproducibility (0-10): Code, equations, parameters, data availability\n- citation_quality (0-10): Are references real, verified, and actually cited in the text?\n\nSTRICT RULES:\n- Missing section = 0 for that section\n- Placeholder/fake references = references score 1\n- Papers under 300 words = all scores below 3\n- Proving obvious things = novelty 0-1\n- Standard variance/mean/known formulas applied to new domain = novelty 4-5 MAX\n- No experimental data or only synthetic/estimated data = results 3-4 MAX\n- Self-referential citations or citing only own work = citation_quality 2-3\n- Circular reasoning (using conclusion to justify methodology) = methodology cap 4\n- Claims without evidence (e.g., \"we achieve SOTA\" with no comparison) = results cap 3\n- DO NOT give 8+ unless the work would genuinely be accepted at a top venue\n\nReturn ONLY this JSON (numbers 0-10):\n{\"abstract\":N,\"introduction\":N,\"methodology\":N,\"results\":N,\"discussion\":N,\"conclusion\":N,\"references\":N,\"novelty\":N,\"reproducibility\":N,\"citation_quality\":N}\n\nPaper content:\n`;\n\n/**\n * Call a single LLM provider to score a paper.\n * Tries up to 2 keys from the provider before giving up.\n */\nasync function callLLMForScoring(prompt, provider) {\n if (!provider.keys.length) return null;\n\n const maxAttempts = Math.min(provider.keys.length, 2);\n for (let attempt = 0; attempt < maxAttempts; attempt++) {\n const key = nextKey(provider.id, provider.keys);\n if (!key) return null;\n\n const headers = { \"Content-Type\": \"application/json\" };\n headers[provider.authHeader] = `${provider.authPrefix}${key}`;\n if (provider.extraHeaders) Object.assign(headers, provider.extraHeaders);\n\n try {\n const res = await fetch(provider.url, {\n method: \"POST\",\n headers,\n body: JSON.stringify({\n model: provider.model,\n messages: [{ role: \"user\", content: prompt }],\n max_tokens: provider.maxTokens || 512,\n temperature: 0.1,\n }),\n signal: AbortSignal.timeout(provider.timeout || 30000),\n });\n\n if (res.status === 429) {\n console.warn(`[SCORING] ${provider.name} 429 rate-limited`);\n await new Promise(r => setTimeout(r, 3000));\n continue;\n }\n if (res.status === 402) {\n console.warn(`[SCORING] ${provider.name} 402 credits exhausted — skipping provider`);\n return null;\n }\n if (!res.ok) {\n const errBody = await res.text().catch(() => \"\");\n console.warn(`[SCORING] ${provider.name} HTTP ${res.status}: ${errBody.substring(0, 150)}`);\n continue;\n }\n\n const data = await res.json();\n // Support both OpenAI format (choices[0].message.content) and Cohere v2 (message.content[] array)\n let text = \"\";\n if (provider.responseFormat === \"cohere\") {\n // Cohere v2 returns array of content blocks: [{type:\"thinking\",...}, {type:\"text\",text:\"...\"}]\n const blocks = data.message?.content || [];\n if (Array.isArray(blocks)) {\n const textBlock = blocks.find(b => b.type === \"text\");\n text = textBlock?.text || blocks[blocks.length - 1]?.text || \"\";\n } else {\n text = typeof blocks === \"string\" ? blocks : \"\";\n }\n } else if (provider.responseFormat === \"cloudflare\") {\n // Cloudflare Workers AI wraps OpenAI format: {result: {choices: [...]}, success: true}\n const inner = data.result || data;\n text = inner.choices?.[0]?.message?.content || \"\";\n } else {\n text = data.choices?.[0]?.message?.content || \"\";\n }\n\n // Strip <think>...</think> tags (Sarvam, Qwen with thinking mode)\n if (provider.stripThinkTags || text.includes(\"<think>\")) {\n // Strip closed think blocks\n text = text.replace(/<think>[\\s\\S]*?<\\/think>/g, \"\").trim();\n // Strip unclosed think blocks (model ran out of tokens mid-think)\n if (text.startsWith(\"<think>\")) {\n text = text.replace(/<think>[\\s\\S]*/g, \"\").trim();\n }\n }\n\n // Extract JSON — use balanced braces to handle nested objects like {\"score\":8,\"why\":\"...\"}\n let jsonMatch = null;\n const firstBrace = text.indexOf('{');\n if (firstBrace !== -1) {\n let depth = 0, end = -1;\n for (let i = firstBrace; i < text.length; i++) {\n if (text[i] === '{') depth++;\n else if (text[i] === '}') { depth--; if (depth === 0) { end = i; break; } }\n }\n if (end !== -1) jsonMatch = [text.substring(firstBrace, end + 1)];\n }\n if (!jsonMatch) {\n console.warn(`[SCORING] ${provider.name} — no JSON in response`);\n continue;\n }\n\n const parsed = JSON.parse(jsonMatch[0]);\n const fields = [...SECTIONS, \"novelty\", \"reproducibility\", \"citation_quality\"];\n const feedback = {};\n for (const field of fields) {\n let val = parsed[field];\n // Support both plain number and {score, why} object format\n if (val && typeof val === \"object\" && typeof val.score === \"number\") {\n feedback[field] = val.why || null;\n val = val.score;\n }\n if (typeof val !== \"number\" || val < 0 || val > 10) {\n val = typeof val === \"number\" ? Math.max(0, Math.min(10, Math.round(val))) : 5;\n }\n parsed[field] = val;\n }\n\n // Clean parsed: only keep recognized score fields (remove stray \"why\" etc.)\n const cleanScores = {};\n for (const field of fields) cleanScores[field] = parsed[field];\n\n console.log(`[SCORING] ${provider.name} scored successfully`);\n return { scores: cleanScores, provider: provider.name, feedback: Object.keys(feedback).length > 0 ? feedback : null };\n } catch (e) {\n console.warn(`[SCORING] ${provider.name} error: ${e.message}`);\n }\n }\n return null;\n}\n\n/**\n * Deterministic heuristic scoring — used when all LLMs fail.\n */\nfunction heuristicScore(content) {\n const text = content || \"\";\n const words = text.split(/\\s+/).filter(w => w.length > 0);\n const wordCount = words.length;\n const lowerText = text.toLowerCase();\n\n const sectionScores = {};\n for (const section of SECTIONS) {\n const hasSection = lowerText.includes(`## ${section}`) || lowerText.includes(`# ${section}`);\n if (!hasSection) {\n sectionScores[section] = 0;\n continue;\n }\n const sectionRegex = new RegExp(`##?\\\\s*${section}[\\\\s\\\\S]*?(?=##?\\\\s|$)`, \"i\");\n const match = text.match(sectionRegex);\n const sectionWords = match ? match[0].split(/\\s+/).length : 0;\n if (sectionWords < 20) sectionScores[section] = 2;\n else if (sectionWords < 50) sectionScores[section] = 4;\n else if (sectionWords < 100) sectionScores[section] = 5;\n else if (sectionWords < 200) sectionScores[section] = 6;\n else sectionScores[section] = 7;\n }\n\n const refMatches = text.match(/\\[\\d+\\]/g) || [];\n const uniqueRefs = new Set(refMatches).size;\n const hasPlaceholderRefs = /placeholder|author,?\\s*a\\.\\s*\\(\\d{4}\\)/i.test(text);\n const hasRealAuthors = /[A-Z][a-z]+,\\s*[A-Z]\\.\\s*(?:&|,|et al)/g.test(text);\n const hasDOI = /doi\\.org|arxiv\\.org|10\\.\\d{4}/i.test(text);\n let refScore = hasPlaceholderRefs ? 1 : Math.min(7, uniqueRefs);\n if (hasRealAuthors) refScore = Math.min(10, refScore + 1);\n if (hasDOI) refScore = Math.min(10, refScore + 1);\n sectionScores.references = refScore;\n\n const technicalTerms = (text.match(/\\b(algorithm|theorem|proof|complexity|O\\([^)]+\\)|convergence|optimal|novel|framework)\\b/gi) || []).length;\n const hasFigures = /figure \\d|fig\\.\\s*\\d|table \\d/i.test(text);\n let novelty = wordCount > 2000 ? 4 : wordCount > 1000 ? 3 : 2;\n novelty += Math.min(3, Math.floor(technicalTerms / 5));\n if (hasFigures) novelty += 1;\n\n const hasCode = /```[\\s\\S]*?```/.test(text);\n const hasEquations = /\\$[^$]+\\$/.test(text) || /\\\\begin\\{/.test(text);\n const hasNumbers = /\\d+\\.\\d+%|\\d+\\.\\d+x|p\\s*[<>]\\s*0\\.\\d/i.test(text);\n let reproducibility = (hasCode ? 5 : 3) + (hasEquations ? 1 : 0) + (wordCount > 2000 ? 1 : 0);\n if (hasNumbers) reproducibility += 1;\n\n const citation_quality = hasPlaceholderRefs ? 1 : Math.min(6, uniqueRefs);\n\n return {\n scores: {\n ...sectionScores,\n novelty: Math.min(10, novelty),\n reproducibility: Math.min(10, reproducibility),\n citation_quality: Math.min(10, citation_quality),\n },\n provider: \"heuristic\"\n };\n}\n\n/**\n * Score a paper using heterogeneous multi-LLM swarm.\n * Tries to get 2 independent judges, falls back through provider chain.\n */\nexport async function scoreGranular(content, paperType = \"research\") {\n if (!content || content.trim().length < 50) {\n return {\n sections: Object.fromEntries(SECTIONS.map(s => [s, 0])),\n overall: 0,\n novelty: 0,\n reproducibility: 0,\n citation_quality: 0,\n judges: [],\n judge_count: 0,\n error: \"Content too short to score\"\n };\n }\n\n // All LLM judges support 32k+ context. Send enough content to include ALL 7 mandatory sections.\n // Previous limit of 4000 chars truncated papers before methodology/results/conclusion, causing\n // strict judges (Cohere, Xiaomi) to correctly score missing sections as 0.\n const truncated = content.length > 16000 ? content.substring(0, 16000) + \"\\n\\n[... truncated for scoring ...]\" : content;\n const prompt = SCORING_PROMPT + truncated;\n\n // ALL available judges score independently for maximum consensus diversity.\n // Each model evaluates each section independently, then we average.\n // Run all judges in parallel for speed (each has its own timeout).\n const judgePromises = available.map(provider =>\n callLLMForScoring(prompt, provider).catch(() => null)\n );\n const judgeResults = await Promise.all(judgePromises);\n const judges = judgeResults.filter(Boolean);\n\n // If no LLM judges succeeded, use heuristic\n if (judges.length === 0) {\n const heuristic = heuristicScore(content);\n judges.push(heuristic);\n }\n\n // Average scores across all judges\n const allFields = [...SECTIONS, \"novelty\", \"reproducibility\", \"citation_quality\"];\n const averaged = {};\n for (const field of allFields) {\n const values = judges.map(j => j.scores[field]).filter(v => typeof v === \"number\");\n averaged[field] = values.length > 0\n ? Math.round((values.reduce((a, b) => a + b, 0) / values.length) * 10) / 10\n : 0;\n }\n\n // ── CALIBRATION PASS — adjust raw averages against reference benchmarks ──\n // This is the core fix for inflated scoring. Raw LLM averages are compared\n // against quality signals extracted from the paper content and calibrated\n // against recognized reference paper fingerprints.\n let calibration = null;\n try {\n const fieldResult = detectField(content);\n const signals = extractSignals(content);\n const benchmarks = REFERENCE_BENCHMARKS[fieldResult.field] || null;\n const { calibrated, adjustments } = calibrateScores(averaged, signals, benchmarks);\n\n // Apply calibrated scores over raw averages\n const adjustmentCount = Object.keys(adjustments).length;\n if (adjustmentCount > 0) {\n for (const [field, val] of Object.entries(calibrated)) {\n if (typeof val === \"number\" && averaged[field] !== undefined) {\n averaged[field] = val;\n }\n }\n console.log(`[SCORING] Calibration applied: ${adjustmentCount} adjustments, field=${fieldResult.field}, ` +\n `red_flags=${signals.red_flag_count}, depth=${signals.depth_score}`);\n }\n\n calibration = {\n field: fieldResult.field,\n field_confidence: fieldResult.confidence,\n signals_summary: {\n word_count: signals.word_count,\n sections_present: signals.sections_present.length,\n sections_missing: signals.sections_missing,\n red_flags: signals.red_flags,\n red_flag_count: signals.red_flag_count,\n has_formal_proofs: signals.has_formal_proofs,\n has_equations: signals.has_equations,\n has_code: signals.has_code,\n unique_refs: signals.unique_refs,\n has_placeholder_refs: signals.has_placeholder_refs,\n depth_score: signals.depth_score,\n evidence_markers: signals.evidence_markers,\n deception_count: signals.deception_count || 0,\n deception_matches: (signals.deception_matches || []).map(d => ({\n id: d.id, name: d.name, severity: d.severity,\n })),\n // New quality dimensions\n grammar: signals.grammar_quality ? {\n vocabulary_diversity: signals.grammar_quality.vocabulary_diversity_ttr,\n is_monotone: signals.grammar_quality.is_monotone,\n is_low_vocabulary: signals.grammar_quality.is_low_vocabulary,\n } : null,\n repetition_ratio: signals.repetition_score?.repetition_ratio || 0,\n code_quality: signals.code_quality?.blocks_found > 0 ? {\n blocks: signals.code_quality.blocks_found,\n has_real_code: signals.has_real_code || false,\n has_python: signals.code_quality.has_python || false,\n } : null,\n math_formulas: signals.math_quality?.formula_count || 0,\n lean4: signals.lean4_signals?.verification_level || \"none\",\n tables: signals.table_quality?.count || 0,\n },\n adjustments,\n adjustment_count: adjustmentCount,\n reference_papers: benchmarks ? benchmarks.references.map(r => r.title) : [],\n };\n } catch (calErr) {\n console.warn(`[SCORING] Calibration error (non-fatal): ${calErr.message}`);\n }\n\n // ── LIVE VERIFICATION PASS — real-time CrossRef, arXiv, code exec, Lean4 ──\n // Runs in parallel with independent timeouts. Non-fatal: if it fails, scoring\n // continues with just calibration. Results apply caps and bonuses to scores.\n let liveVerification = null;\n try {\n const verification = await runLiveVerification(content);\n const { adjustments: liveAdj, bonuses: liveBon } = verificationToAdjustments(verification);\n\n // ── OVERRIDE FALSE POSITIVES ──\n // If live verification confirmed code execution but calibration flagged\n // \"code_blocks_are_template_not_real\", the red_flag was a false positive.\n // Undo the -1.5 penalty applied to all dimensions by recalibrating.\n const codeExec = verification.code_execution;\n if (codeExec && codeExec.passed > 0 && calibration && calibration.signals_summary) {\n const redFlags = calibration.signals_summary.red_flags || [];\n if (redFlags.includes(\"code_blocks_are_template_not_real\")) {\n // The -1.5 penalty was applied to all 10 dimensions. Undo it.\n const allFields = [...SECTIONS, \"novelty\", \"reproducibility\", \"citation_quality\"];\n for (const field of allFields) {\n if (averaged[field] !== undefined) {\n averaged[field] = Math.min(10, Math.round((averaged[field] + 1.5) * 10) / 10);\n }\n }\n // Remove the false red flag from the report\n calibration.signals_summary.red_flags = redFlags.filter(f => f !== \"code_blocks_are_template_not_real\");\n calibration.signals_summary.red_flag_count = calibration.signals_summary.red_flags.length;\n calibration.false_positive_corrected = \"code_blocks_are_template_not_real (live verification confirmed code executes)\";\n console.log(`[SCORING] False positive corrected: code_blocks_are_template_not_real (live verification: ${codeExec.passed}/${codeExec.total} passed)`);\n }\n }\n\n // Apply caps (these override score to a max value)\n for (const [key, val] of Object.entries(liveAdj)) {\n if (key.endsWith(\"_cap\") && typeof val === \"number\") {\n const field = key.replace(\"_cap\", \"\");\n if (averaged[field] !== undefined && averaged[field] > val) {\n averaged[field] = val;\n }\n }\n }\n\n // Apply bonuses (add to score, capped at 10)\n for (const [key, val] of Object.entries(liveBon)) {\n if (key.endsWith(\"_bonus\") && typeof val === \"number\") {\n const field = key.replace(\"_bonus\", \"\");\n if (averaged[field] !== undefined) {\n averaged[field] = Math.min(10, Math.round((averaged[field] + val) * 10) / 10);\n }\n }\n }\n\n liveVerification = {\n verification_time_ms: verification.verification_time_ms,\n citations: verification.citations ? {\n total: verification.citations.total,\n verified: verification.citations.verified,\n verification_rate: verification.citations.verification_rate,\n } : null,\n novelty: verification.novelty ? {\n searched: verification.novelty.searched,\n total_found: verification.novelty.total_found,\n novelty_concern: verification.novelty.novelty_concern,\n max_similarity: verification.novelty.max_similarity,\n } : null,\n code_execution: verification.code_execution ? {\n total: verification.code_execution.total,\n passed: verification.code_execution.passed,\n failed: verification.code_execution.failed,\n } : null,\n lean4: verification.lean4 ? {\n blocks_found: verification.lean4.blocks_found,\n verified: verification.lean4.verified,\n has_unsubstantiated_claim: verification.lean4.has_unsubstantiated_claim || false,\n } : null,\n adjustments: liveAdj,\n bonuses: liveBon,\n };\n\n const adjCount = Object.keys(liveAdj).filter(k => k.endsWith(\"_cap\")).length;\n const bonCount = Object.keys(liveBon).filter(k => k.endsWith(\"_bonus\")).length;\n if (adjCount > 0 || bonCount > 0) {\n console.log(`[SCORING] Live verification: ${adjCount} caps, ${bonCount} bonuses applied (${verification.verification_time_ms}ms)`);\n }\n } catch (liveErr) {\n console.warn(`[SCORING] Live verification error (non-fatal): ${liveErr.message}`);\n }\n\n const sectionValues = SECTIONS.map(s => averaged[s]);\n const overall = Math.round((sectionValues.reduce((a, b) => a + b, 0) / SECTIONS.length) * 10) / 10;\n\n // Per-judge detail breakdown (individual scores + feedback)\n const judge_details = judges.map(j => ({\n judge: j.provider,\n scores: j.scores,\n feedback: j.feedback || null,\n }));\n\n // Consensus score per dimension (1 = all judges agree, 0 = total disagreement)\n const consensus = {};\n for (const field of allFields) {\n const values = judges.map(j => j.scores[field]).filter(v => typeof v === \"number\");\n if (values.length < 2) { consensus[field] = 1.0; continue; }\n const mean = values.reduce((a, b) => a + b, 0) / values.length;\n const variance = values.reduce((sum, v) => sum + (v - mean) ** 2, 0) / values.length;\n const stddev = Math.sqrt(variance);\n consensus[field] = Math.round(Math.max(0, 1 - stddev / 5) * 100) / 100;\n }\n const overall_consensus = Math.round(\n (allFields.reduce((sum, f) => sum + consensus[f], 0) / allFields.length) * 100\n ) / 100;\n\n // Aggregate feedback per dimension (from all judges that provided it)\n const aggregated_feedback = {};\n for (const field of allFields) {\n const comments = judges\n .filter(j => j.feedback && j.feedback[field])\n .map(j => ({ judge: j.provider, comment: j.feedback[field] }));\n if (comments.length > 0) aggregated_feedback[field] = comments;\n }\n\n const result = {\n sections: Object.fromEntries(SECTIONS.map(s => [s, averaged[s]])),\n overall,\n novelty: averaged.novelty,\n reproducibility: averaged.reproducibility,\n citation_quality: averaged.citation_quality,\n judges: judges.map(j => j.provider),\n judge_count: judges.length,\n judge_details,\n consensus,\n overall_consensus,\n feedback: Object.keys(aggregated_feedback).length > 0 ? aggregated_feedback : null,\n scored_at: new Date().toISOString(),\n paper_type: paperType,\n calibration,\n live_verification: liveVerification,\n };\n\n console.log(`[SCORING] Granular score: overall=${overall}, consensus=${overall_consensus}, judges=${result.judges.join(\",\")}`);\n return result;\n}\n"}, "065a5569ac0c96e8cb52a7324f890907c5fc8ae1": {"t": 1785753926.799486, "n": 35473, "h": "e107abc57bb600f1", "c": "/**\n * P2PCLAW Granular Scoring Service\n * =================================\n * Heterogeneous multi-LLM scoring engine that evaluates papers section-by-section.\n *\n * Provider chain (updated 2026-04-01):\n * 1. Cerebras — qwen-3-235b (3 keys, free, ultra-fast)\n * 2. Cerebras — llama3.1-8b (3 keys, free, ultra-fast)\n * 3. Mistral — mistral-small-latest (2 keys, free)\n * 4. Sarvam — sarvam-m (Indian AI, free)\n * 5. OpenRouter — qwen3-coder:free (free)\n * 6. Groq — llama-3.3-70b-versatile (7 keys, may be restricted)\n * 7. NVIDIA — meta/llama-3.3-70b-instruct (3 keys, free)\n * 8. Inception — mercury-2 (5 keys, free)\n * 9. Xiaomi MiMo — mimo-v2-flash (3 keys, free)\n * 10. Xiaomi MiMo — mimo-v2-pro (3 keys, free, reasoning)\n * 11. Cohere — command-a-reasoning (8 keys, reasoning model)\n * 12. Cloudflare — llama-4-scout-17b (account 7, free)\n * 13. Cloudflare — mistral-small-3.1-24b (account 8, free)\n * 14. Cloudflare — deepseek-r1-distill-qwen-32b (account 9, free)\n * 15. Deterministic heuristic fallback (never blocks)\n *\n * ALL available judges score independently. Final score = average across all judges.\n * Each model evaluates each section independently for maximum consensus diversity.\n */\n\nimport { detectField, extractSignals, calibrateScores, REFERENCE_BENCHMARKS } from \"./calibrationService.js\";\nimport { runLiveVerification, verificationToAdjustments } from \"./liveVerificationService.js\";\n\nconst SECTIONS = [\"abstract\", \"introduction\", \"methodology\", \"results\", \"discussion\", \"conclusion\", \"references\"];\n\n// ── Load keys with rotation ─────────────────────────────────────────────────\n\nfunction loadKeys(envPrefix, maxKeys = 10) {\n const keys = [];\n for (let i = 1; i <= maxKeys; i++) {\n const k = process.env[`${envPrefix}_${i}`] || process.env[`${envPrefix}${i}`];\n if (k) keys.push(k);\n }\n // Also check single-key env vars\n const single = process.env[envPrefix];\n if (single && !keys.includes(single)) keys.unshift(single);\n return keys;\n}\n\nconst keyIndices = {};\nfunction nextKey(providerId, keys) {\n if (!keys.length) return null;\n const idx = keyIndices[providerId] || 0;\n const key = keys[idx % keys.length];\n keyIndices[providerId] = (idx + 1) % keys.length;\n return key;\n}\n\n// ── Provider definitions ────────────────────────────────────────────────────\n\nconst PROVIDERS = [\n // --- Cerebras: 3 keys x 2 models = up to 6 independent judges ---\n {\n id: \"cerebras-qwen\",\n name: \"Cerebras-Qwen235B\",\n url: \"https://api.cerebras.ai/v1/chat/completions\",\n model: \"qwen-3-235b-a22b-instruct-2507\",\n keys: loadKeys(\"CEREBRAS_API_KEY\").concat(loadKeys(\"CEREBRAS_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 1024,\n },\n {\n id: \"cerebras-llama\",\n name: \"Cerebras-Llama8B\",\n url: \"https://api.cerebras.ai/v1/chat/completions\",\n model: \"llama3.1-8b\",\n keys: loadKeys(\"CEREBRAS_API_KEY\").concat(loadKeys(\"CEREBRAS_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n // --- Mistral: 2 keys, reliable ---\n {\n id: \"mistral\",\n name: \"Mistral\",\n url: \"https://api.mistral.ai/v1/chat/completions\",\n model: \"mistral-small-latest\",\n keys: loadKeys(\"MISTRAL_API_KEY\").concat(loadKeys(\"MISTRAL_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n // --- Sarvam (Indian AI): sarvam-m, uses <think> tags (needs 2048+ tokens) ---\n {\n id: \"sarvam\",\n name: \"Sarvam\",\n url: \"https://api.sarvam.ai/v1/chat/completions\",\n model: \"sarvam-m\",\n keys: loadKeys(\"SARVAM_KEY\").concat(loadKeys(\"SARVAM_API_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 2048,\n },\n // --- OpenRouter: free models ---\n {\n id: \"openrouter\",\n name: \"OpenRouter\",\n url: \"https://openrouter.ai/api/v1/chat/completions\",\n model: \"qwen/qwen3-coder:free\",\n keys: loadKeys(\"OPENROUTER_API_KEY\"),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n extraHeaders: { \"HTTP-Referer\": \"https://www.p2pclaw.com\", \"X-Title\": \"P2PCLAW Scoring\" },\n },\n // --- Groq: 7 keys but may be org-restricted ---\n {\n id: \"groq\",\n name: \"Groq\",\n url: \"https://api.groq.com/openai/v1/chat/completions\",\n model: \"llama-3.3-70b-versatile\",\n keys: loadKeys(\"GROQ_API_KEY\").concat(loadKeys(\"GROQ_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n // --- NVIDIA: 3 keys ---\n {\n id: \"nvidia\",\n name: \"NVIDIA\",\n url: \"https://integrate.api.nvidia.com/v1/chat/completions\",\n model: \"meta/llama-3.3-70b-instruct\",\n keys: loadKeys(\"NVAPI_KEY\").concat(loadKeys(\"NVIDIA_API_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n // --- Inception: mercury-2 ---\n {\n id: \"inception\",\n name: \"Inception\",\n url: \"https://api.inceptionlabs.ai/v1/chat/completions\",\n model: \"mercury-2\",\n keys: loadKeys(\"INCEPTION_API_KEY\").concat(loadKeys(\"INCEPTION_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n // --- Xiaomi MiMo: 3 keys x 2 models = up to 2 independent judges ---\n {\n id: \"xiaomi-flash\",\n name: \"Xiaomi-MiMo-Flash\",\n url: \"https://api.xiaomimimo.com/v1/chat/completions\",\n model: \"mimo-v2-flash\",\n keys: loadKeys(\"XIAOMI_API_KEY\").concat(loadKeys(\"XIAOMI_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n {\n id: \"xiaomi-pro\",\n name: \"Xiaomi-MiMo-Pro\",\n url: \"https://api.xiaomimimo.com/v1/chat/completions\",\n model: \"mimo-v2-pro\",\n keys: loadKeys(\"XIAOMI_API_KEY\").concat(loadKeys(\"XIAOMI_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 1024,\n },\n // --- Cohere: command-a-reasoning model (8 keys, reasoning/thinking model) ---\n {\n id: \"cohere\",\n name: \"Cohere-CommandA\",\n url: \"https://api.cohere.com/v2/chat\",\n model: \"command-a-reasoning-08-2025\",\n keys: loadKeys(\"COHERE_API_KEY\").concat(loadKeys(\"COHERE_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 4096, // reasoning model needs extra tokens for thinking + answer\n responseFormat: \"cohere\", // data.message.content[] array with {type:\"thinking\"} + {type:\"text\"}\n timeout: 90000, // 90s — reasoning model needs time to think through all 10 dimensions\n },\n // --- Cloudflare Workers AI: 6 models across 6 accounts (all FREE) ---\n // Ordered by model quality: best first\n {\n id: \"cloudflare-glm\",\n name: \"Cloudflare-GLM4\",\n url: `https://api.cloudflare.com/client/v4/accounts/eaffd2b52c95c69aaad8d859e9dcb52b/ai/run/@cf/zai-org/glm-4.7-flash`,\n model: \"@cf/zai-org/glm-4.7-flash\",\n keys: loadKeys(\"CF_AI_TOKEN\").concat(loadKeys(\"CLOUDFLARE_AI_TOKEN\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 2048,\n responseFormat: \"cloudflare\",\n timeout: 60000,\n },\n {\n id: \"cloudflare-gemma4\",\n name: \"Cloudflare-Gemma4\",\n url: `https://api.cloudflare.com/client/v4/accounts/a7995d3f33b6ba57955749337c9abbe0/ai/run/@cf/google/gemma-4-26b-a4b-it`,\n model: \"@cf/google/gemma-4-26b-a4b-it\",\n keys: loadKeys(\"CF_AI_TOKEN_2\"),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 1024,\n responseFormat: \"cloudflare\",\n timeout: 60000,\n },\n {\n id: \"cloudflare-nemotron\",\n name: \"Cloudflare-Nemotron\",\n url: `https://api.cloudflare.com/client/v4/accounts/194d9aea21482ac893ed81fc6b004864/ai/run/@cf/nvidia/nemotron-3-120b-a12b`,\n model: \"@cf/nvidia/nemotron-3-120b-a12b\",\n keys: loadKeys(\"CF_AI_TOKEN_3\"),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 1024,\n responseFormat: \"cloudflare\",\n timeout: 60000,\n },\n {\n id: \"cloudflare-kimi\",\n name: \"Cloudflare-Kimi\",\n url: `https://api.cloudflare.com/client/v4/accounts/401a75ead25275262c1c05eecb7a997c/ai/run/@cf/moonshotai/kimi-k2.5`,\n model: \"@cf/moonshotai/kimi-k2.5\",\n keys: loadKeys(\"CF_AI_TOKEN_4\"),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 2048,\n responseFormat: \"cloudflare\",\n timeout: 60000,\n },\n {\n id: \"cloudflare-gptoss\",\n name: \"Cloudflare-GPT-OSS\",\n url: `https://api.cloudflare.com/client/v4/accounts/73340519f6430362daee759ba0b48ce8/ai/run/@cf/openai/gpt-oss-120b`,\n model: \"@cf/openai/gpt-oss-120b\",\n keys: loadKeys(\"CF_AI_TOKEN_5\"),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 1024,\n responseFormat: \"cloudflare\",\n timeout: 60000,\n },\n {\n id: \"cloudflare-qwen3\",\n name: \"Cloudflare-Qwen3\",\n url: `https://api.cloudflare.com/client/v4/accounts/df4a7888befcbb6ce3e0a0b346ea1990/ai/run/@cf/qwen/qwen3-30b-a3b-fp8`,\n model: \"@cf/qwen/qwen3-30b-a3b-fp8\",\n keys: loadKeys(\"CF_AI_TOKEN_6\"),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 2048,\n responseFormat: \"cloudflare\",\n timeout: 60000,\n },\n // --- Cloudflare Workers AI: 3 NEW accounts (accounts 7/8/9) ---\n {\n id: \"cloudflare-llama4scout\",\n name: \"Cloudflare-Llama4Scout\",\n url: `https://api.cloudflare.com/client/v4/accounts/3cd084561890e5ab468456fae547ded0/ai/run/@cf/meta/llama-4-scout-17b-16e-instruct`,\n model: \"@cf/meta/llama-4-scout-17b-16e-instruct\",\n keys: loadKeys(\"CF_AI_TOKEN_7\"),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 1024,\n responseFormat: \"cloudflare\",\n timeout: 60000,\n },\n {\n id: \"cloudflare-mistral31\",\n name: \"Cloudflare-MistralSmall31\",\n url: `https://api.cloudflare.com/client/v4/accounts/27920eccf7d83f7ee267130cd6018eaf/ai/run/@cf/mistralai/mistral-small-3.1-24b-instruct`,\n model: \"@cf/mistralai/mistral-small-3.1-24b-instruct\",\n keys: loadKeys(\"CF_AI_TOKEN_8\"),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 1024,\n responseFormat: \"cloudflare\",\n timeout: 60000,\n },\n {\n id: \"cloudflare-deepseekr1\",\n name: \"Cloudflare-DeepSeekR1\",\n url: `https://api.cloudflare.com/client/v4/accounts/60c2dcaa7fc3377f036114648f6397ba/ai/run/@cf/deepseek-ai/deepseek-r1-distill-qwen-32b`,\n model: \"@cf/deepseek-ai/deepseek-r1-distill-qwen-32b\",\n keys: loadKeys(\"CF_AI_TOKEN_9\"),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 2048,\n responseFormat: \"cloudflare\",\n timeout: 60000,\n },\n];\n\n// Deduplicate keys within each provider\nfor (const p of PROVIDERS) {\n p.keys = [...new Set(p.keys)].filter(Boolean);\n}\n\n// Log available providers\nconst available = PROVIDERS.filter(p => p.keys.length > 0);\nconsole.log(`[SCORING] ${available.length} LLM providers available: ${available.map(p => `${p.name}(${p.keys.length})`).join(\", \")}`);\nif (available.length === 0) console.warn(\"[SCORING] No LLM providers — heuristic scoring only.\");\n// Debug: log specifically which providers have NO keys\nconst unavailable = PROVIDERS.filter(p => p.keys.length === 0);\nif (unavailable.length > 0) console.log(`[SCORING] Providers with NO keys: ${unavailable.map(p => p.name).join(\", \")}`);\n\nconst SCORING_PROMPT = `You are a STRICT academic peer reviewer. You evaluate papers for the P2PCLAW benchmark.\n\nYour scoring must follow real academic standards. Most papers score 4-6. Only exceptional work reaches 7+. A score of 8+ means top-tier venue quality (NeurIPS, Nature, ICML). A 10 is historically significant (Turing Award level).\n\nCALIBRATION ANCHORS — use these to calibrate your scores:\n- 10/10 novelty: \"Attention Is All You Need\" (Vaswani 2017), Bitcoin whitepaper (Nakamoto 2008)\n- 8/10 novelty: A genuine new algorithm with proven improvements over SOTA\n- 6/10 novelty: Meaningful extension of existing work with some original insights\n- 5/10 novelty: Applying known techniques to a new domain (standard contribution)\n- 3/10 novelty: Minor variation of existing work, obvious next step\n- 1/10 novelty: Restating known results with different notation\n\nScore each criterion on 0-10:\n- abstract (0-10): Clarity, completeness, problem+scope+results summarized\n- introduction (0-10): Problem clarity, context, motivation. Needs 2+ real citations to related work\n- methodology (0-10): Rigor and reproducibility. Can someone replicate this exactly?\n- results (0-10): Strength of evidence. Real data, real experiments, statistical significance\n- discussion (0-10): Honest limitations, implications, comparison to prior work\n- conclusion (0-10): Summary of findings, concrete future directions\n- references (0-10): Real citations with authors, titles, years, DOIs. 8+ unique for score >5\n- novelty (0-10): TRUE original contribution. Applying a standard formula to a new domain = 4-5, NOT 8-9\n- reproducibility (0-10): Code, equations, parameters, data availability\n- citation_quality (0-10): Are references real, verified, and actually cited in the text?\n\nSTRICT RULES:\n- Missing section = 0 for that section\n- Placeholder/fake references = references score 1\n- Papers under 300 words = all scores below 3\n- Proving obvious things = novelty 0-1\n- Standard variance/mean/known formulas applied to new domain = novelty 4-5 MAX\n- No experimental data or only synthetic/estimated data = results 3-4 MAX\n- Self-referential citations or citing only own work = citation_quality 2-3\n- Circular reasoning (using conclusion to justify methodology) = methodology cap 4\n- Claims without evidence (e.g., \"we achieve SOTA\" with no comparison) = results cap 3\n- DO NOT give 8+ unless the work would genuinely be accepted at a top venue\n\nReturn ONLY this JSON (numbers 0-10):\n{\"abstract\":N,\"introduction\":N,\"methodology\":N,\"results\":N,\"discussion\":N,\"conclusion\":N,\"references\":N,\"novelty\":N,\"reproducibility\":N,\"citation_quality\":N}\n\nPaper content:\n`;\n\n/**\n * Call a single LLM provider to score a paper.\n * Tries up to 2 keys from the provider before giving up.\n */\nasync function callLLMForScoring(prompt, provider) {\n if (!provider.keys.length) return null;\n\n const maxAttempts = Math.min(provider.keys.length, 2);\n for (let attempt = 0; attempt < maxAttempts; attempt++) {\n const key = nextKey(provider.id, provider.keys);\n if (!key) return null;\n\n const headers = { \"Content-Type\": \"application/json\" };\n headers[provider.authHeader] = `${provider.authPrefix}${key}`;\n if (provider.extraHeaders) Object.assign(headers, provider.extraHeaders);\n\n try {\n const res = await fetch(provider.url, {\n method: \"POST\",\n headers,\n body: JSON.stringify({\n model: provider.model,\n messages: [{ role: \"user\", content: prompt }],\n max_tokens: provider.maxTokens || 512,\n temperature: 0.1,\n }),\n signal: AbortSignal.timeout(provider.timeout || 30000),\n });\n\n if (res.status === 429) {\n console.warn(`[SCORING] ${provider.name} 429 rate-limited`);\n await new Promise(r => setTimeout(r, 3000));\n continue;\n }\n if (res.status === 402) {\n console.warn(`[SCORING] ${provider.name} 402 credits exhausted — skipping provider`);\n return null;\n }\n if (!res.ok) {\n const errBody = await res.text().catch(() => \"\");\n console.warn(`[SCORING] ${provider.name} HTTP ${res.status}: ${errBody.substring(0, 150)}`);\n continue;\n }\n\n const data = await res.json();\n // Support both OpenAI format (choices[0].message.content) and Cohere v2 (message.content[] array)\n let text = \"\";\n if (provider.responseFormat === \"cohere\") {\n // Cohere v2 returns array of content blocks: [{type:\"thinking\",...}, {type:\"text\",text:\"...\"}]\n const blocks = data.message?.content || [];\n if (Array.isArray(blocks)) {\n const textBlock = blocks.find(b => b.type === \"text\");\n text = textBlock?.text || blocks[blocks.length - 1]?.text || \"\";\n } else {\n text = typeof blocks === \"string\" ? blocks : \"\";\n }\n } else if (provider.responseFormat === \"cloudflare\") {\n // Cloudflare Workers AI wraps OpenAI format: {result: {choices: [...]}, success: true}\n const inner = data.result || data;\n text = inner.choices?.[0]?.message?.content || \"\";\n } else {\n text = data.choices?.[0]?.message?.content || \"\";\n }\n\n // Strip <think>...</think> tags (Sarvam, Qwen with thinking mode)\n if (provider.stripThinkTags || text.includes(\"<think>\")) {\n // Strip closed think blocks\n text = text.replace(/<think>[\\s\\S]*?<\\/think>/g, \"\").trim();\n // Strip unclosed think blocks (model ran out of tokens mid-think)\n if (text.startsWith(\"<think>\")) {\n text = text.replace(/<think>[\\s\\S]*/g, \"\").trim();\n }\n }\n\n // Extract JSON — use balanced braces to handle nested objects like {\"score\":8,\"why\":\"...\"}\n let jsonMatch = null;\n const firstBrace = text.indexOf('{');\n if (firstBrace !== -1) {\n let depth = 0, end = -1;\n for (let i = firstBrace; i < text.length; i++) {\n if (text[i] === '{') depth++;\n else if (text[i] === '}') { depth--; if (depth === 0) { end = i; break; } }\n }\n if (end !== -1) jsonMatch = [text.substring(firstBrace, end + 1)];\n }\n if (!jsonMatch) {\n console.warn(`[SCORING] ${provider.name} — no JSON in response`);\n continue;\n }\n\n const parsed = JSON.parse(jsonMatch[0]);\n const fields = [...SECTIONS, \"novelty\", \"reproducibility\", \"citation_quality\"];\n const feedback = {};\n for (const field of fields) {\n let val = parsed[field];\n // Support both plain number and {score, why} object format\n if (val && typeof val === \"object\" && typeof val.score === \"number\") {\n feedback[field] = val.why || null;\n val = val.score;\n }\n if (typeof val !== \"number\" || val < 0 || val > 10) {\n val = typeof val === \"number\" ? Math.max(0, Math.min(10, Math.round(val))) : 5;\n }\n parsed[field] = val;\n }\n\n // Clean parsed: only keep recognized score fields (remove stray \"why\" etc.)\n const cleanScores = {};\n for (const field of fields) cleanScores[field] = parsed[field];\n\n console.log(`[SCORING] ${provider.name} scored successfully`);\n return { scores: cleanScores, provider: provider.name, feedback: Object.keys(feedback).length > 0 ? feedback : null };\n } catch (e) {\n console.warn(`[SCORING] ${provider.name} error: ${e.message}`);\n }\n }\n return null;\n}\n\n/**\n * Deterministic heuristic scoring — used when all LLMs fail.\n */\nfunction heuristicScore(content) {\n const text = content || \"\";\n const words = text.split(/\\s+/).filter(w => w.length > 0);\n const wordCount = words.length;\n const lowerText = text.toLowerCase();\n\n const sectionScores = {};\n for (const section of SECTIONS) {\n const hasSection = lowerText.includes(`## ${section}`) || lowerText.includes(`# ${section}`);\n if (!hasSection) {\n sectionScores[section] = 0;\n continue;\n }\n const sectionRegex = new RegExp(`##?\\\\s*${section}[\\\\s\\\\S]*?(?=##?\\\\s|$)`, \"i\");\n const match = text.match(sectionRegex);\n const sectionWords = match ? match[0].split(/\\s+/).length : 0;\n if (sectionWords < 20) sectionScores[section] = 2;\n else if (sectionWords < 50) sectionScores[section] = 4;\n else if (sectionWords < 100) sectionScores[section] = 5;\n else if (sectionWords < 200) sectionScores[section] = 6;\n else sectionScores[section] = 7;\n }\n\n const refMatches = text.match(/\\[\\d+\\]/g) || [];\n const uniqueRefs = new Set(refMatches).size;\n const hasPlaceholderRefs = /placeholder|author,?\\s*a\\.\\s*\\(\\d{4}\\)/i.test(text);\n const hasRealAuthors = /[A-Z][a-z]+,\\s*[A-Z]\\.\\s*(?:&|,|et al)/g.test(text);\n const hasDOI = /doi\\.org|arxiv\\.org|10\\.\\d{4}/i.test(text);\n let refScore = hasPlaceholderRefs ? 1 : Math.min(7, uniqueRefs);\n if (hasRealAuthors) refScore = Math.min(10, refScore + 1);\n if (hasDOI) refScore = Math.min(10, refScore + 1);\n sectionScores.references = refScore;\n\n const technicalTerms = (text.match(/\\b(algorithm|theorem|proof|complexity|O\\([^)]+\\)|convergence|optimal|novel|framework)\\b/gi) || []).length;\n const hasFigures = /figure \\d|fig\\.\\s*\\d|table \\d/i.test(text);\n let novelty = wordCount > 2000 ? 4 : wordCount > 1000 ? 3 : 2;\n novelty += Math.min(3, Math.floor(technicalTerms / 5));\n if (hasFigures) novelty += 1;\n\n const hasCode = /```[\\s\\S]*?```/.test(text);\n const hasEquations = /\\$[^$]+\\$/.test(text) || /\\\\begin\\{/.test(text);\n const hasNumbers = /\\d+\\.\\d+%|\\d+\\.\\d+x|p\\s*[<>]\\s*0\\.\\d/i.test(text);\n let reproducibility = (hasCode ? 5 : 3) + (hasEquations ? 1 : 0) + (wordCount > 2000 ? 1 : 0);\n if (hasNumbers) reproducibility += 1;\n\n const citation_quality = hasPlaceholderRefs ? 1 : Math.min(6, uniqueRefs);\n\n return {\n scores: {\n ...sectionScores,\n novelty: Math.min(10, novelty),\n reproducibility: Math.min(10, reproducibility),\n citation_quality: Math.min(10, citation_quality),\n },\n provider: \"heuristic\"\n };\n}\n\n/**\n * Score a paper using heterogeneous multi-LLM swarm.\n * Tries to get 2 independent judges, falls back through provider chain.\n */\nexport async function scoreGranular(content, paperType = \"research\") {\n if (!content || content.trim().length < 50) {\n return {\n sections: Object.fromEntries(SECTIONS.map(s => [s, 0])),\n overall: 0,\n novelty: 0,\n reproducibility: 0,\n citation_quality: 0,\n judges: [],\n judge_count: 0,\n error: \"Content too short to score\"\n };\n }\n\n // All LLM judges support 32k+ context. Send enough content to include ALL 7 mandatory sections.\n // Previous limit of 4000 chars truncated papers before methodology/results/conclusion, causing\n // strict judges (Cohere, Xiaomi) to correctly score missing sections as 0.\n const truncated = content.length > 16000 ? content.substring(0, 16000) + \"\\n\\n[... truncated for scoring ...]\" : content;\n const prompt = SCORING_PROMPT + truncated;\n\n // ALL available judges score independently for maximum consensus diversity.\n // Each model evaluates each section independently, then we average.\n // Run all judges in parallel for speed (each has its own timeout).\n const judgePromises = available.map(provider =>\n callLLMForScoring(prompt, provider).catch(() => null)\n );\n const judgeResults = await Promise.all(judgePromises);\n const judges = judgeResults.filter(Boolean);\n\n // If no LLM judges succeeded, use heuristic\n if (judges.length === 0) {\n const heuristic = heuristicScore(content);\n judges.push(heuristic);\n }\n\n // Average scores across all judges\n const allFields = [...SECTIONS, \"novelty\", \"reproducibility\", \"citation_quality\"];\n const averaged = {};\n for (const field of allFields) {\n const values = judges.map(j => j.scores[field]).filter(v => typeof v === \"number\");\n averaged[field] = values.length > 0\n ? Math.round((values.reduce((a, b) => a + b, 0) / values.length) * 10) / 10\n : 0;\n }\n\n // ── CALIBRATION PASS — adjust raw averages against reference benchmarks ──\n // This is the core fix for inflated scoring. Raw LLM averages are compared\n // against quality signals extracted from the paper content and calibrated\n // against recognized reference paper fingerprints.\n let calibration = null;\n try {\n const fieldResult = detectField(content);\n const signals = extractSignals(content);\n const benchmarks = REFERENCE_BENCHMARKS[fieldResult.field] || null;\n const { calibrated, adjustments } = calibrateScores(averaged, signals, benchmarks);\n\n // Apply calibrated scores over raw averages\n const adjustmentCount = Object.keys(adjustments).length;\n if (adjustmentCount > 0) {\n for (const [field, val] of Object.entries(calibrated)) {\n if (typeof val === \"number\" && averaged[field] !== undefined) {\n averaged[field] = val;\n }\n }\n console.log(`[SCORING] Calibration applied: ${adjustmentCount} adjustments, field=${fieldResult.field}, ` +\n `red_flags=${signals.red_flag_count}, depth=${signals.depth_score}`);\n }\n\n calibration = {\n field: fieldResult.field,\n field_confidence: fieldResult.confidence,\n signals_summary: {\n word_count: signals.word_count,\n sections_present: signals.sections_present.length,\n sections_missing: signals.sections_missing,\n red_flags: signals.red_flags,\n red_flag_count: signals.red_flag_count,\n has_formal_proofs: signals.has_formal_proofs,\n has_equations: signals.has_equations,\n has_code: signals.has_code,\n unique_refs: signals.unique_refs,\n has_placeholder_refs: signals.has_placeholder_refs,\n depth_score: signals.depth_score,\n evidence_markers: signals.evidence_markers,\n deception_count: signals.deception_count || 0,\n deception_matches: (signals.deception_matches || []).map(d => ({\n id: d.id, name: d.name, severity: d.severity,\n })),\n // New quality dimensions\n grammar: signals.grammar_quality ? {\n vocabulary_diversity: signals.grammar_quality.vocabulary_diversity_ttr,\n is_monotone: signals.grammar_quality.is_monotone,\n is_low_vocabulary: signals.grammar_quality.is_low_vocabulary,\n } : null,\n repetition_ratio: signals.repetition_score?.repetition_ratio || 0,\n code_quality: signals.code_quality?.blocks_found > 0 ? {\n blocks: signals.code_quality.blocks_found,\n has_real_code: signals.has_real_code || false,\n has_python: signals.code_quality.has_python || false,\n } : null,\n math_formulas: signals.math_quality?.formula_count || 0,\n lean4: signals.lean4_signals?.verification_level || \"none\",\n tables: signals.table_quality?.count || 0,\n },\n adjustments,\n adjustment_count: adjustmentCount,\n reference_papers: benchmarks ? benchmarks.references.map(r => r.title) : [],\n };\n } catch (calErr) {\n console.warn(`[SCORING] Calibration error (non-fatal): ${calErr.message}`);\n }\n\n // ── LIVE VERIFICATION PASS — real-time CrossRef, arXiv, code exec, Lean4 ──\n // Runs in parallel with independent timeouts. Non-fatal: if it fails, scoring\n // continues with just calibration. Results apply caps and bonuses to scores.\n let liveVerification = null;\n try {\n const verification = await runLiveVerification(content);\n const { adjustments: liveAdj, bonuses: liveBon } = verificationToAdjustments(verification);\n\n // ── OVERRIDE FALSE POSITIVES ──\n // If live verification confirmed code execution but calibration flagged\n // \"code_blocks_are_template_not_real\", the red_flag was a false positive.\n // Undo the -1.5 penalty applied to all dimensions by recalibrating.\n const codeExec = verification.code_execution;\n if (codeExec && codeExec.passed > 0 && calibration && calibration.signals_summary) {\n const redFlags = calibration.signals_summary.red_flags || [];\n if (redFlags.includes(\"code_blocks_are_template_not_real\")) {\n // The -1.5 penalty was applied to all 10 dimensions. Undo it.\n const allFields = [...SECTIONS, \"novelty\", \"reproducibility\", \"citation_quality\"];\n for (const field of allFields) {\n if (averaged[field] !== undefined) {\n averaged[field] = Math.min(10, Math.round((averaged[field] + 1.5) * 10) / 10);\n }\n }\n // Remove the false red flag from the report\n calibration.signals_summary.red_flags = redFlags.filter(f => f !== \"code_blocks_are_template_not_real\");\n calibration.signals_summary.red_flag_count = calibration.signals_summary.red_flags.length;\n calibration.false_positive_corrected = \"code_blocks_are_template_not_real (live verification confirmed code executes)\";\n console.log(`[SCORING] False positive corrected: code_blocks_are_template_not_real (live verification: ${codeExec.passed}/${codeExec.total} passed)`);\n }\n }\n\n // Apply caps (these override score to a max value)\n for (const [key, val] of Object.entries(liveAdj)) {\n if (key.endsWith(\"_cap\") && typeof val === \"number\") {\n const field = key.replace(\"_cap\", \"\");\n if (averaged[field] !== undefined && averaged[field] > val) {\n averaged[field] = val;\n }\n }\n }\n\n // Apply bonuses (add to score, capped at 10)\n for (const [key, val] of Object.entries(liveBon)) {\n if (key.endsWith(\"_bonus\") && typeof val === \"number\") {\n const field = key.replace(\"_bonus\", \"\");\n if (averaged[field] !== undefined) {\n averaged[field] = Math.min(10, Math.round((averaged[field] + val) * 10) / 10);\n }\n }\n }\n\n liveVerification = {\n verification_time_ms: verification.verification_time_ms,\n citations: verification.citations ? {\n total: verification.citations.total,\n verified: verification.citations.verified,\n verification_rate: verification.citations.verification_rate,\n } : null,\n novelty: verification.novelty ? {\n searched: verification.novelty.searched,\n total_found: verification.novelty.total_found,\n novelty_concern: verification.novelty.novelty_concern,\n max_similarity: verification.novelty.max_similarity,\n } : null,\n code_execution: verification.code_execution ? {\n total: verification.code_execution.total,\n passed: verification.code_execution.passed,\n failed: verification.code_execution.failed,\n } : null,\n lean4: verification.lean4 ? {\n blocks_found: verification.lean4.blocks_found,\n verified: verification.lean4.verified,\n has_unsubstantiated_claim: verification.lean4.has_unsubstantiated_claim || false,\n } : null,\n adjustments: liveAdj,\n bonuses: liveBon,\n };\n\n const adjCount = Object.keys(liveAdj).filter(k => k.endsWith(\"_cap\")).length;\n const bonCount = Object.keys(liveBon).filter(k => k.endsWith(\"_bonus\")).length;\n if (adjCount > 0 || bonCount > 0) {\n console.log(`[SCORING] Live verification: ${adjCount} caps, ${bonCount} bonuses applied (${verification.verification_time_ms}ms)`);\n }\n } catch (liveErr) {\n console.warn(`[SCORING] Live verification error (non-fatal): ${liveErr.message}`);\n }\n\n const sectionValues = SECTIONS.map(s => averaged[s]);\n const overall = Math.round((sectionValues.reduce((a, b) => a + b, 0) / SECTIONS.length) * 10) / 10;\n\n // Per-judge detail breakdown (individual scores + feedback)\n const judge_details = judges.map(j => ({\n judge: j.provider,\n scores: j.scores,\n feedback: j.feedback || null,\n }));\n\n // Consensus score per dimension (1 = all judges agree, 0 = total disagreement)\n const consensus = {};\n for (const field of allFields) {\n const values = judges.map(j => j.scores[field]).filter(v => typeof v === \"number\");\n if (values.length < 2) { consensus[field] = 1.0; continue; }\n const mean = values.reduce((a, b) => a + b, 0) / values.length;\n const variance = values.reduce((sum, v) => sum + (v - mean) ** 2, 0) / values.length;\n const stddev = Math.sqrt(variance);\n consensus[field] = Math.round(Math.max(0, 1 - stddev / 5) * 100) / 100;\n }\n const overall_consensus = Math.round(\n (allFields.reduce((sum, f) => sum + consensus[f], 0) / allFields.length) * 100\n ) / 100;\n\n // Aggregate feedback per dimension (from all judges that provided it)\n const aggregated_feedback = {};\n for (const field of allFields) {\n const comments = judges\n .filter(j => j.feedback && j.feedback[field])\n .map(j => ({ judge: j.provider, comment: j.feedback[field] }));\n if (comments.length > 0) aggregated_feedback[field] = comments;\n }\n\n const result = {\n sections: Object.fromEntries(SECTIONS.map(s => [s, averaged[s]])),\n overall,\n novelty: averaged.novelty,\n reproducibility: averaged.reproducibility,\n citation_quality: averaged.citation_quality,\n judges: judges.map(j => j.provider),\n judge_count: judges.length,\n judge_details,\n consensus,\n overall_consensus,\n feedback: Object.keys(aggregated_feedback).length > 0 ? aggregated_feedback : null,\n scored_at: new Date().toISOString(),\n paper_type: paperType,\n calibration,\n live_verification: liveVerification,\n };\n\n console.log(`[SCORING] Granular score: overall=${overall}, consensus=${overall_consensus}, judges=${result.judges.join(\",\")}`);\n return result;\n}\n"}, "836f54862f44a935a3e2e8b15a702ddee1a62f95": {"t": 1785753928.3439162, "n": 33704, "h": "48aae28513b41d10", "c": "/**\n * P2PCLAW Granular Scoring Service\n * =================================\n * Heterogeneous multi-LLM scoring engine that evaluates papers section-by-section.\n *\n * Provider chain (updated 2026-04-01):\n * 1. Cerebras — qwen-3-235b (3 keys, free, ultra-fast)\n * 2. Cerebras — llama3.1-8b (3 keys, free, ultra-fast)\n * 3. Mistral — mistral-small-latest (2 keys, free)\n * 4. Sarvam — sarvam-m (Indian AI, free)\n * 5. OpenRouter — qwen3-coder:free (free)\n * 6. Groq — llama-3.3-70b-versatile (7 keys, may be restricted)\n * 7. NVIDIA — meta/llama-3.3-70b-instruct (3 keys, free)\n * 8. Inception — mercury-2 (5 keys, free)\n * 9. Xiaomi MiMo — mimo-v2-flash (3 keys, free)\n * 10. Xiaomi MiMo — mimo-v2-pro (3 keys, free, reasoning)\n * 11. Cohere — command-a-reasoning (8 keys, reasoning model)\n * 12. Deterministic heuristic fallback (never blocks)\n *\n * ALL available judges score independently. Final score = average across all judges.\n * Each model evaluates each section independently for maximum consensus diversity.\n */\n\nimport { detectField, extractSignals, calibrateScores, REFERENCE_BENCHMARKS } from \"./calibrationService.js\";\nimport { runLiveVerification, verificationToAdjustments } from \"./liveVerificationService.js\";\n\nconst SECTIONS = [\"abstract\", \"introduction\", \"methodology\", \"results\", \"discussion\", \"conclusion\", \"references\"];\n\n// ── Load keys with rotation ─────────────────────────────────────────────────\n\nfunction loadKeys(envPrefix, maxKeys = 10) {\n const keys = [];\n for (let i = 1; i <= maxKeys; i++) {\n const k = process.env[`${envPrefix}_${i}`] || process.env[`${envPrefix}${i}`];\n if (k) keys.push(k);\n }\n // Also check single-key env vars\n const single = process.env[envPrefix];\n if (single && !keys.includes(single)) keys.unshift(single);\n return keys;\n}\n\nconst keyIndices = {};\nfunction nextKey(providerId, keys) {\n if (!keys.length) return null;\n const idx = keyIndices[providerId] || 0;\n const key = keys[idx % keys.length];\n keyIndices[providerId] = (idx + 1) % keys.length;\n return key;\n}\n\n// ── Provider definitions ────────────────────────────────────────────────────\n\nconst PROVIDERS = [\n // --- Cerebras: 3 keys x 2 models = up to 6 independent judges ---\n {\n id: \"cerebras-qwen\",\n name: \"Cerebras-Qwen235B\",\n url: \"https://api.cerebras.ai/v1/chat/completions\",\n model: \"qwen-3-235b-a22b-instruct-2507\",\n keys: loadKeys(\"CEREBRAS_API_KEY\").concat(loadKeys(\"CEREBRAS_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 1024,\n },\n {\n id: \"cerebras-llama\",\n name: \"Cerebras-Llama8B\",\n url: \"https://api.cerebras.ai/v1/chat/completions\",\n model: \"llama3.1-8b\",\n keys: loadKeys(\"CEREBRAS_API_KEY\").concat(loadKeys(\"CEREBRAS_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n // --- Mistral: 2 keys, reliable ---\n {\n id: \"mistral\",\n name: \"Mistral\",\n url: \"https://api.mistral.ai/v1/chat/completions\",\n model: \"mistral-small-latest\",\n keys: loadKeys(\"MISTRAL_API_KEY\").concat(loadKeys(\"MISTRAL_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n // --- Sarvam (Indian AI): sarvam-m, uses <think> tags (needs 2048+ tokens) ---\n {\n id: \"sarvam\",\n name: \"Sarvam\",\n url: \"https://api.sarvam.ai/v1/chat/completions\",\n model: \"sarvam-m\",\n keys: loadKeys(\"SARVAM_KEY\").concat(loadKeys(\"SARVAM_API_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 2048,\n },\n // --- OpenRouter: free models ---\n {\n id: \"openrouter\",\n name: \"OpenRouter\",\n url: \"https://openrouter.ai/api/v1/chat/completions\",\n model: \"qwen/qwen3-coder:free\",\n keys: loadKeys(\"OPENROUTER_API_KEY\"),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n extraHeaders: { \"HTTP-Referer\": \"https://www.p2pclaw.com\", \"X-Title\": \"P2PCLAW Scoring\" },\n },\n // --- Groq: 7 keys but may be org-restricted ---\n {\n id: \"groq\",\n name: \"Groq\",\n url: \"https://api.groq.com/openai/v1/chat/completions\",\n model: \"llama-3.3-70b-versatile\",\n keys: loadKeys(\"GROQ_API_KEY\").concat(loadKeys(\"GROQ_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n // --- NVIDIA: 3 keys ---\n {\n id: \"nvidia\",\n name: \"NVIDIA\",\n url: \"https://integrate.api.nvidia.com/v1/chat/completions\",\n model: \"meta/llama-3.3-70b-instruct\",\n keys: loadKeys(\"NVAPI_KEY\").concat(loadKeys(\"NVIDIA_API_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n // --- Inception: mercury-2 ---\n {\n id: \"inception\",\n name: \"Inception\",\n url: \"https://api.inceptionlabs.ai/v1/chat/completions\",\n model: \"mercury-2\",\n keys: loadKeys(\"INCEPTION_API_KEY\").concat(loadKeys(\"INCEPTION_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n // --- Xiaomi MiMo: 3 keys x 2 models = up to 2 independent judges ---\n {\n id: \"xiaomi-flash\",\n name: \"Xiaomi-MiMo-Flash\",\n url: \"https://api.xiaomimimo.com/v1/chat/completions\",\n model: \"mimo-v2-flash\",\n keys: loadKeys(\"XIAOMI_API_KEY\").concat(loadKeys(\"XIAOMI_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n {\n id: \"xiaomi-pro\",\n name: \"Xiaomi-MiMo-Pro\",\n url: \"https://api.xiaomimimo.com/v1/chat/completions\",\n model: \"mimo-v2-pro\",\n keys: loadKeys(\"XIAOMI_API_KEY\").concat(loadKeys(\"XIAOMI_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 1024,\n },\n // --- Cohere: command-a-reasoning model (8 keys, reasoning/thinking model) ---\n {\n id: \"cohere\",\n name: \"Cohere-CommandA\",\n url: \"https://api.cohere.com/v2/chat\",\n model: \"command-a-reasoning-08-2025\",\n keys: loadKeys(\"COHERE_API_KEY\").concat(loadKeys(\"COHERE_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 4096, // reasoning model needs extra tokens for thinking + answer\n responseFormat: \"cohere\", // data.message.content[] array with {type:\"thinking\"} + {type:\"text\"}\n timeout: 90000, // 90s — reasoning model needs time to think through all 10 dimensions\n },\n // --- Cloudflare Workers AI: 6 models across 6 accounts (all FREE) ---\n // Ordered by model quality: best first\n {\n id: \"cloudflare-glm\",\n name: \"Cloudflare-GLM4\",\n url: `https://api.cloudflare.com/client/v4/accounts/eaffd2b52c95c69aaad8d859e9dcb52b/ai/run/@cf/zai-org/glm-4.7-flash`,\n model: \"@cf/zai-org/glm-4.7-flash\",\n keys: loadKeys(\"CF_AI_TOKEN\").concat(loadKeys(\"CLOUDFLARE_AI_TOKEN\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 2048,\n responseFormat: \"cloudflare\",\n timeout: 60000,\n },\n {\n id: \"cloudflare-gemma4\",\n name: \"Cloudflare-Gemma4\",\n url: `https://api.cloudflare.com/client/v4/accounts/a7995d3f33b6ba57955749337c9abbe0/ai/run/@cf/google/gemma-4-26B-A4B-it`,\n model: \"@cf/google/gemma-4-26B-A4B-it\",\n keys: loadKeys(\"CF_AI_TOKEN_2\"),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 1024,\n responseFormat: \"cloudflare\",\n timeout: 60000,\n },\n {\n id: \"cloudflare-nemotron\",\n name: \"Cloudflare-Nemotron\",\n url: `https://api.cloudflare.com/client/v4/accounts/194d9aea21482ac893ed81fc6b004864/ai/run/@cf/nvidia/nemotron-3-120b-a12b`,\n model: \"@cf/nvidia/nemotron-3-120b-a12b\",\n keys: loadKeys(\"CF_AI_TOKEN_3\"),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 1024,\n responseFormat: \"cloudflare\",\n timeout: 60000,\n },\n {\n id: \"cloudflare-kimi\",\n name: \"Cloudflare-Kimi\",\n url: `https://api.cloudflare.com/client/v4/accounts/401a75ead25275262c1c05eecb7a997c/ai/run/@cf/moonshotai/kimi-k2.5`,\n model: \"@cf/moonshotai/kimi-k2.5\",\n keys: loadKeys(\"CF_AI_TOKEN_4\"),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 2048,\n responseFormat: \"cloudflare\",\n timeout: 60000,\n },\n {\n id: \"cloudflare-gptoss\",\n name: \"Cloudflare-GPT-OSS\",\n url: `https://api.cloudflare.com/client/v4/accounts/73340519f6430362daee759ba0b48ce8/ai/run/@cf/openai/gpt-oss-120b`,\n model: \"@cf/openai/gpt-oss-120b\",\n keys: loadKeys(\"CF_AI_TOKEN_5\"),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 1024,\n responseFormat: \"cloudflare\",\n timeout: 60000,\n },\n {\n id: \"cloudflare-qwen3\",\n name: \"Cloudflare-Qwen3\",\n url: `https://api.cloudflare.com/client/v4/accounts/df4a7888befcbb6ce3e0a0b346ea1990/ai/run/@cf/qwen/qwen3-30b-a3b-fp8`,\n model: \"@cf/qwen/qwen3-30b-a3b-fp8\",\n keys: loadKeys(\"CF_AI_TOKEN_6\"),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 2048,\n responseFormat: \"cloudflare\",\n timeout: 60000,\n },\n];\n\n// Deduplicate keys within each provider\nfor (const p of PROVIDERS) {\n p.keys = [...new Set(p.keys)].filter(Boolean);\n}\n\n// Log available providers\nconst available = PROVIDERS.filter(p => p.keys.length > 0);\nconsole.log(`[SCORING] ${available.length} LLM providers available: ${available.map(p => `${p.name}(${p.keys.length})`).join(\", \")}`);\nif (available.length === 0) console.warn(\"[SCORING] No LLM providers — heuristic scoring only.\");\n// Debug: log specifically which providers have NO keys\nconst unavailable = PROVIDERS.filter(p => p.keys.length === 0);\nif (unavailable.length > 0) console.log(`[SCORING] Providers with NO keys: ${unavailable.map(p => p.name).join(\", \")}`);\n\nconst SCORING_PROMPT = `You are a STRICT academic peer reviewer. You evaluate papers for the P2PCLAW benchmark.\n\nYour scoring must follow real academic standards. Most papers score 4-6. Only exceptional work reaches 7+. A score of 8+ means top-tier venue quality (NeurIPS, Nature, ICML). A 10 is historically significant (Turing Award level).\n\nCALIBRATION ANCHORS — use these to calibrate your scores:\n- 10/10 novelty: \"Attention Is All You Need\" (Vaswani 2017), Bitcoin whitepaper (Nakamoto 2008)\n- 8/10 novelty: A genuine new algorithm with proven improvements over SOTA\n- 6/10 novelty: Meaningful extension of existing work with some original insights\n- 5/10 novelty: Applying known techniques to a new domain (standard contribution)\n- 3/10 novelty: Minor variation of existing work, obvious next step\n- 1/10 novelty: Restating known results with different notation\n\nScore each criterion on 0-10:\n- abstract (0-10): Clarity, completeness, problem+scope+results summarized\n- introduction (0-10): Problem clarity, context, motivation. Needs 2+ real citations to related work\n- methodology (0-10): Rigor and reproducibility. Can someone replicate this exactly?\n- results (0-10): Strength of evidence. Real data, real experiments, statistical significance\n- discussion (0-10): Honest limitations, implications, comparison to prior work\n- conclusion (0-10): Summary of findings, concrete future directions\n- references (0-10): Real citations with authors, titles, years, DOIs. 8+ unique for score >5\n- novelty (0-10): TRUE original contribution. Applying a standard formula to a new domain = 4-5, NOT 8-9\n- reproducibility (0-10): Code, equations, parameters, data availability\n- citation_quality (0-10): Are references real, verified, and actually cited in the text?\n\nSTRICT RULES:\n- Missing section = 0 for that section\n- Placeholder/fake references = references score 1\n- Papers under 300 words = all scores below 3\n- Proving obvious things = novelty 0-1\n- Standard variance/mean/known formulas applied to new domain = novelty 4-5 MAX\n- No experimental data or only synthetic/estimated data = results 3-4 MAX\n- Self-referential citations or citing only own work = citation_quality 2-3\n- Circular reasoning (using conclusion to justify methodology) = methodology cap 4\n- Claims without evidence (e.g., \"we achieve SOTA\" with no comparison) = results cap 3\n- DO NOT give 8+ unless the work would genuinely be accepted at a top venue\n\nReturn ONLY this JSON (numbers 0-10):\n{\"abstract\":N,\"introduction\":N,\"methodology\":N,\"results\":N,\"discussion\":N,\"conclusion\":N,\"references\":N,\"novelty\":N,\"reproducibility\":N,\"citation_quality\":N}\n\nPaper content:\n`;\n\n/**\n * Call a single LLM provider to score a paper.\n * Tries up to 2 keys from the provider before giving up.\n */\nasync function callLLMForScoring(prompt, provider) {\n if (!provider.keys.length) return null;\n\n const maxAttempts = Math.min(provider.keys.length, 2);\n for (let attempt = 0; attempt < maxAttempts; attempt++) {\n const key = nextKey(provider.id, provider.keys);\n if (!key) return null;\n\n const headers = { \"Content-Type\": \"application/json\" };\n headers[provider.authHeader] = `${provider.authPrefix}${key}`;\n if (provider.extraHeaders) Object.assign(headers, provider.extraHeaders);\n\n try {\n const res = await fetch(provider.url, {\n method: \"POST\",\n headers,\n body: JSON.stringify({\n model: provider.model,\n messages: [{ role: \"user\", content: prompt }],\n max_tokens: provider.maxTokens || 512,\n temperature: 0.1,\n }),\n signal: AbortSignal.timeout(provider.timeout || 30000),\n });\n\n if (res.status === 429) {\n console.warn(`[SCORING] ${provider.name} 429 rate-limited`);\n await new Promise(r => setTimeout(r, 3000));\n continue;\n }\n if (res.status === 402) {\n console.warn(`[SCORING] ${provider.name} 402 credits exhausted — skipping provider`);\n return null;\n }\n if (!res.ok) {\n const errBody = await res.text().catch(() => \"\");\n console.warn(`[SCORING] ${provider.name} HTTP ${res.status}: ${errBody.substring(0, 150)}`);\n continue;\n }\n\n const data = await res.json();\n // Support both OpenAI format (choices[0].message.content) and Cohere v2 (message.content[] array)\n let text = \"\";\n if (provider.responseFormat === \"cohere\") {\n // Cohere v2 returns array of content blocks: [{type:\"thinking\",...}, {type:\"text\",text:\"...\"}]\n const blocks = data.message?.content || [];\n if (Array.isArray(blocks)) {\n const textBlock = blocks.find(b => b.type === \"text\");\n text = textBlock?.text || blocks[blocks.length - 1]?.text || \"\";\n } else {\n text = typeof blocks === \"string\" ? blocks : \"\";\n }\n } else if (provider.responseFormat === \"cloudflare\") {\n // Cloudflare Workers AI wraps OpenAI format: {result: {choices: [...]}, success: true}\n const inner = data.result || data;\n text = inner.choices?.[0]?.message?.content || \"\";\n } else {\n text = data.choices?.[0]?.message?.content || \"\";\n }\n\n // Strip <think>...</think> tags (Sarvam, Qwen with thinking mode)\n if (provider.stripThinkTags || text.includes(\"<think>\")) {\n // Strip closed think blocks\n text = text.replace(/<think>[\\s\\S]*?<\\/think>/g, \"\").trim();\n // Strip unclosed think blocks (model ran out of tokens mid-think)\n if (text.startsWith(\"<think>\")) {\n text = text.replace(/<think>[\\s\\S]*/g, \"\").trim();\n }\n }\n\n // Extract JSON — use balanced braces to handle nested objects like {\"score\":8,\"why\":\"...\"}\n let jsonMatch = null;\n const firstBrace = text.indexOf('{');\n if (firstBrace !== -1) {\n let depth = 0, end = -1;\n for (let i = firstBrace; i < text.length; i++) {\n if (text[i] === '{') depth++;\n else if (text[i] === '}') { depth--; if (depth === 0) { end = i; break; } }\n }\n if (end !== -1) jsonMatch = [text.substring(firstBrace, end + 1)];\n }\n if (!jsonMatch) {\n console.warn(`[SCORING] ${provider.name} — no JSON in response`);\n continue;\n }\n\n const parsed = JSON.parse(jsonMatch[0]);\n const fields = [...SECTIONS, \"novelty\", \"reproducibility\", \"citation_quality\"];\n const feedback = {};\n for (const field of fields) {\n let val = parsed[field];\n // Support both plain number and {score, why} object format\n if (val && typeof val === \"object\" && typeof val.score === \"number\") {\n feedback[field] = val.why || null;\n val = val.score;\n }\n if (typeof val !== \"number\" || val < 0 || val > 10) {\n val = typeof val === \"number\" ? Math.max(0, Math.min(10, Math.round(val))) : 5;\n }\n parsed[field] = val;\n }\n\n // Clean parsed: only keep recognized score fields (remove stray \"why\" etc.)\n const cleanScores = {};\n for (const field of fields) cleanScores[field] = parsed[field];\n\n console.log(`[SCORING] ${provider.name} scored successfully`);\n return { scores: cleanScores, provider: provider.name, feedback: Object.keys(feedback).length > 0 ? feedback : null };\n } catch (e) {\n console.warn(`[SCORING] ${provider.name} error: ${e.message}`);\n }\n }\n return null;\n}\n\n/**\n * Deterministic heuristic scoring — used when all LLMs fail.\n */\nfunction heuristicScore(content) {\n const text = content || \"\";\n const words = text.split(/\\s+/).filter(w => w.length > 0);\n const wordCount = words.length;\n const lowerText = text.toLowerCase();\n\n const sectionScores = {};\n for (const section of SECTIONS) {\n const hasSection = lowerText.includes(`## ${section}`) || lowerText.includes(`# ${section}`);\n if (!hasSection) {\n sectionScores[section] = 0;\n continue;\n }\n const sectionRegex = new RegExp(`##?\\\\s*${section}[\\\\s\\\\S]*?(?=##?\\\\s|$)`, \"i\");\n const match = text.match(sectionRegex);\n const sectionWords = match ? match[0].split(/\\s+/).length : 0;\n if (sectionWords < 20) sectionScores[section] = 2;\n else if (sectionWords < 50) sectionScores[section] = 4;\n else if (sectionWords < 100) sectionScores[section] = 5;\n else if (sectionWords < 200) sectionScores[section] = 6;\n else sectionScores[section] = 7;\n }\n\n const refMatches = text.match(/\\[\\d+\\]/g) || [];\n const uniqueRefs = new Set(refMatches).size;\n const hasPlaceholderRefs = /placeholder|author,?\\s*a\\.\\s*\\(\\d{4}\\)/i.test(text);\n const hasRealAuthors = /[A-Z][a-z]+,\\s*[A-Z]\\.\\s*(?:&|,|et al)/g.test(text);\n const hasDOI = /doi\\.org|arxiv\\.org|10\\.\\d{4}/i.test(text);\n let refScore = hasPlaceholderRefs ? 1 : Math.min(7, uniqueRefs);\n if (hasRealAuthors) refScore = Math.min(10, refScore + 1);\n if (hasDOI) refScore = Math.min(10, refScore + 1);\n sectionScores.references = refScore;\n\n const technicalTerms = (text.match(/\\b(algorithm|theorem|proof|complexity|O\\([^)]+\\)|convergence|optimal|novel|framework)\\b/gi) || []).length;\n const hasFigures = /figure \\d|fig\\.\\s*\\d|table \\d/i.test(text);\n let novelty = wordCount > 2000 ? 4 : wordCount > 1000 ? 3 : 2;\n novelty += Math.min(3, Math.floor(technicalTerms / 5));\n if (hasFigures) novelty += 1;\n\n const hasCode = /```[\\s\\S]*?```/.test(text);\n const hasEquations = /\\$[^$]+\\$/.test(text) || /\\\\begin\\{/.test(text);\n const hasNumbers = /\\d+\\.\\d+%|\\d+\\.\\d+x|p\\s*[<>]\\s*0\\.\\d/i.test(text);\n let reproducibility = (hasCode ? 5 : 3) + (hasEquations ? 1 : 0) + (wordCount > 2000 ? 1 : 0);\n if (hasNumbers) reproducibility += 1;\n\n const citation_quality = hasPlaceholderRefs ? 1 : Math.min(6, uniqueRefs);\n\n return {\n scores: {\n ...sectionScores,\n novelty: Math.min(10, novelty),\n reproducibility: Math.min(10, reproducibility),\n citation_quality: Math.min(10, citation_quality),\n },\n provider: \"heuristic\"\n };\n}\n\n/**\n * Score a paper using heterogeneous multi-LLM swarm.\n * Tries to get 2 independent judges, falls back through provider chain.\n */\nexport async function scoreGranular(content, paperType = \"research\") {\n if (!content || content.trim().length < 50) {\n return {\n sections: Object.fromEntries(SECTIONS.map(s => [s, 0])),\n overall: 0,\n novelty: 0,\n reproducibility: 0,\n citation_quality: 0,\n judges: [],\n judge_count: 0,\n error: \"Content too short to score\"\n };\n }\n\n // All LLM judges support 32k+ context. Send enough content to include ALL 7 mandatory sections.\n // Previous limit of 4000 chars truncated papers before methodology/results/conclusion, causing\n // strict judges (Cohere, Xiaomi) to correctly score missing sections as 0.\n const truncated = content.length > 16000 ? content.substring(0, 16000) + \"\\n\\n[... truncated for scoring ...]\" : content;\n const prompt = SCORING_PROMPT + truncated;\n\n // ALL available judges score independently for maximum consensus diversity.\n // Each model evaluates each section independently, then we average.\n // Run all judges in parallel for speed (each has its own timeout).\n const judgePromises = available.map(provider =>\n callLLMForScoring(prompt, provider).catch(() => null)\n );\n const judgeResults = await Promise.all(judgePromises);\n const judges = judgeResults.filter(Boolean);\n\n // If no LLM judges succeeded, use heuristic\n if (judges.length === 0) {\n const heuristic = heuristicScore(content);\n judges.push(heuristic);\n }\n\n // Average scores across all judges\n const allFields = [...SECTIONS, \"novelty\", \"reproducibility\", \"citation_quality\"];\n const averaged = {};\n for (const field of allFields) {\n const values = judges.map(j => j.scores[field]).filter(v => typeof v === \"number\");\n averaged[field] = values.length > 0\n ? Math.round((values.reduce((a, b) => a + b, 0) / values.length) * 10) / 10\n : 0;\n }\n\n // ── CALIBRATION PASS — adjust raw averages against reference benchmarks ──\n // This is the core fix for inflated scoring. Raw LLM averages are compared\n // against quality signals extracted from the paper content and calibrated\n // against recognized reference paper fingerprints.\n let calibration = null;\n try {\n const fieldResult = detectField(content);\n const signals = extractSignals(content);\n const benchmarks = REFERENCE_BENCHMARKS[fieldResult.field] || null;\n const { calibrated, adjustments } = calibrateScores(averaged, signals, benchmarks);\n\n // Apply calibrated scores over raw averages\n const adjustmentCount = Object.keys(adjustments).length;\n if (adjustmentCount > 0) {\n for (const [field, val] of Object.entries(calibrated)) {\n if (typeof val === \"number\" && averaged[field] !== undefined) {\n averaged[field] = val;\n }\n }\n console.log(`[SCORING] Calibration applied: ${adjustmentCount} adjustments, field=${fieldResult.field}, ` +\n `red_flags=${signals.red_flag_count}, depth=${signals.depth_score}`);\n }\n\n calibration = {\n field: fieldResult.field,\n field_confidence: fieldResult.confidence,\n signals_summary: {\n word_count: signals.word_count,\n sections_present: signals.sections_present.length,\n sections_missing: signals.sections_missing,\n red_flags: signals.red_flags,\n red_flag_count: signals.red_flag_count,\n has_formal_proofs: signals.has_formal_proofs,\n has_equations: signals.has_equations,\n has_code: signals.has_code,\n unique_refs: signals.unique_refs,\n has_placeholder_refs: signals.has_placeholder_refs,\n depth_score: signals.depth_score,\n evidence_markers: signals.evidence_markers,\n deception_count: signals.deception_count || 0,\n deception_matches: (signals.deception_matches || []).map(d => ({\n id: d.id, name: d.name, severity: d.severity,\n })),\n // New quality dimensions\n grammar: signals.grammar_quality ? {\n vocabulary_diversity: signals.grammar_quality.vocabulary_diversity_ttr,\n is_monotone: signals.grammar_quality.is_monotone,\n is_low_vocabulary: signals.grammar_quality.is_low_vocabulary,\n } : null,\n repetition_ratio: signals.repetition_score?.repetition_ratio || 0,\n code_quality: signals.code_quality?.blocks_found > 0 ? {\n blocks: signals.code_quality.blocks_found,\n has_real_code: signals.has_real_code || false,\n has_python: signals.code_quality.has_python || false,\n } : null,\n math_formulas: signals.math_quality?.formula_count || 0,\n lean4: signals.lean4_signals?.verification_level || \"none\",\n tables: signals.table_quality?.count || 0,\n },\n adjustments,\n adjustment_count: adjustmentCount,\n reference_papers: benchmarks ? benchmarks.references.map(r => r.title) : [],\n };\n } catch (calErr) {\n console.warn(`[SCORING] Calibration error (non-fatal): ${calErr.message}`);\n }\n\n // ── LIVE VERIFICATION PASS — real-time CrossRef, arXiv, code exec, Lean4 ──\n // Runs in parallel with independent timeouts. Non-fatal: if it fails, scoring\n // continues with just calibration. Results apply caps and bonuses to scores.\n let liveVerification = null;\n try {\n const verification = await runLiveVerification(content);\n const { adjustments: liveAdj, bonuses: liveBon } = verificationToAdjustments(verification);\n\n // ── OVERRIDE FALSE POSITIVES ──\n // If live verification confirmed code execution but calibration flagged\n // \"code_blocks_are_template_not_real\", the red_flag was a false positive.\n // Undo the -1.5 penalty applied to all dimensions by recalibrating.\n const codeExec = verification.code_execution;\n if (codeExec && codeExec.passed > 0 && calibration && calibration.signals_summary) {\n const redFlags = calibration.signals_summary.red_flags || [];\n if (redFlags.includes(\"code_blocks_are_template_not_real\")) {\n // The -1.5 penalty was applied to all 10 dimensions. Undo it.\n const allFields = [...SECTIONS, \"novelty\", \"reproducibility\", \"citation_quality\"];\n for (const field of allFields) {\n if (averaged[field] !== undefined) {\n averaged[field] = Math.min(10, Math.round((averaged[field] + 1.5) * 10) / 10);\n }\n }\n // Remove the false red flag from the report\n calibration.signals_summary.red_flags = redFlags.filter(f => f !== \"code_blocks_are_template_not_real\");\n calibration.signals_summary.red_flag_count = calibration.signals_summary.red_flags.length;\n calibration.false_positive_corrected = \"code_blocks_are_template_not_real (live verification confirmed code executes)\";\n console.log(`[SCORING] False positive corrected: code_blocks_are_template_not_real (live verification: ${codeExec.passed}/${codeExec.total} passed)`);\n }\n }\n\n // Apply caps (these override score to a max value)\n for (const [key, val] of Object.entries(liveAdj)) {\n if (key.endsWith(\"_cap\") && typeof val === \"number\") {\n const field = key.replace(\"_cap\", \"\");\n if (averaged[field] !== undefined && averaged[field] > val) {\n averaged[field] = val;\n }\n }\n }\n\n // Apply bonuses (add to score, capped at 10)\n for (const [key, val] of Object.entries(liveBon)) {\n if (key.endsWith(\"_bonus\") && typeof val === \"number\") {\n const field = key.replace(\"_bonus\", \"\");\n if (averaged[field] !== undefined) {\n averaged[field] = Math.min(10, Math.round((averaged[field] + val) * 10) / 10);\n }\n }\n }\n\n liveVerification = {\n verification_time_ms: verification.verification_time_ms,\n citations: verification.citations ? {\n total: verification.citations.total,\n verified: verification.citations.verified,\n verification_rate: verification.citations.verification_rate,\n } : null,\n novelty: verification.novelty ? {\n searched: verification.novelty.searched,\n total_found: verification.novelty.total_found,\n novelty_concern: verification.novelty.novelty_concern,\n max_similarity: verification.novelty.max_similarity,\n } : null,\n code_execution: verification.code_execution ? {\n total: verification.code_execution.total,\n passed: verification.code_execution.passed,\n failed: verification.code_execution.failed,\n } : null,\n lean4: verification.lean4 ? {\n blocks_found: verification.lean4.blocks_found,\n verified: verification.lean4.verified,\n has_unsubstantiated_claim: verification.lean4.has_unsubstantiated_claim || false,\n } : null,\n adjustments: liveAdj,\n bonuses: liveBon,\n };\n\n const adjCount = Object.keys(liveAdj).filter(k => k.endsWith(\"_cap\")).length;\n const bonCount = Object.keys(liveBon).filter(k => k.endsWith(\"_bonus\")).length;\n if (adjCount > 0 || bonCount > 0) {\n console.log(`[SCORING] Live verification: ${adjCount} caps, ${bonCount} bonuses applied (${verification.verification_time_ms}ms)`);\n }\n } catch (liveErr) {\n console.warn(`[SCORING] Live verification error (non-fatal): ${liveErr.message}`);\n }\n\n const sectionValues = SECTIONS.map(s => averaged[s]);\n const overall = Math.round((sectionValues.reduce((a, b) => a + b, 0) / SECTIONS.length) * 10) / 10;\n\n // Per-judge detail breakdown (individual scores + feedback)\n const judge_details = judges.map(j => ({\n judge: j.provider,\n scores: j.scores,\n feedback: j.feedback || null,\n }));\n\n // Consensus score per dimension (1 = all judges agree, 0 = total disagreement)\n const consensus = {};\n for (const field of allFields) {\n const values = judges.map(j => j.scores[field]).filter(v => typeof v === \"number\");\n if (values.length < 2) { consensus[field] = 1.0; continue; }\n const mean = values.reduce((a, b) => a + b, 0) / values.length;\n const variance = values.reduce((sum, v) => sum + (v - mean) ** 2, 0) / values.length;\n const stddev = Math.sqrt(variance);\n consensus[field] = Math.round(Math.max(0, 1 - stddev / 5) * 100) / 100;\n }\n const overall_consensus = Math.round(\n (allFields.reduce((sum, f) => sum + consensus[f], 0) / allFields.length) * 100\n ) / 100;\n\n // Aggregate feedback per dimension (from all judges that provided it)\n const aggregated_feedback = {};\n for (const field of allFields) {\n const comments = judges\n .filter(j => j.feedback && j.feedback[field])\n .map(j => ({ judge: j.provider, comment: j.feedback[field] }));\n if (comments.length > 0) aggregated_feedback[field] = comments;\n }\n\n const result = {\n sections: Object.fromEntries(SECTIONS.map(s => [s, averaged[s]])),\n overall,\n novelty: averaged.novelty,\n reproducibility: averaged.reproducibility,\n citation_quality: averaged.citation_quality,\n judges: judges.map(j => j.provider),\n judge_count: judges.length,\n judge_details,\n consensus,\n overall_consensus,\n feedback: Object.keys(aggregated_feedback).length > 0 ? aggregated_feedback : null,\n scored_at: new Date().toISOString(),\n paper_type: paperType,\n calibration,\n live_verification: liveVerification,\n };\n\n console.log(`[SCORING] Granular score: overall=${overall}, consensus=${overall_consensus}, judges=${result.judges.join(\",\")}`);\n return result;\n}\n"}, "9d6ed21e66330907ab81ce5dd6ca9d958ae7973d": {"t": 1785753930.7140684, "n": 33312, "h": "dda980651b481a6e", "c": "/**\n * P2PCLAW Granular Scoring Service\n * =================================\n * Heterogeneous multi-LLM scoring engine that evaluates papers section-by-section.\n *\n * Provider chain (updated 2026-04-01):\n * 1. Cerebras — qwen-3-235b (3 keys, free, ultra-fast)\n * 2. Cerebras — llama3.1-8b (3 keys, free, ultra-fast)\n * 3. Mistral — mistral-small-latest (2 keys, free)\n * 4. Sarvam — sarvam-m (Indian AI, free)\n * 5. OpenRouter — qwen3-coder:free (free)\n * 6. Groq — llama-3.3-70b-versatile (7 keys, may be restricted)\n * 7. NVIDIA — meta/llama-3.3-70b-instruct (3 keys, free)\n * 8. Inception — mercury-2 (5 keys, free)\n * 9. Xiaomi MiMo — mimo-v2-flash (3 keys, free)\n * 10. Xiaomi MiMo — mimo-v2-pro (3 keys, free, reasoning)\n * 11. Cohere — command-a-reasoning (8 keys, reasoning model)\n * 12. Deterministic heuristic fallback (never blocks)\n *\n * ALL available judges score independently. Final score = average across all judges.\n * Each model evaluates each section independently for maximum consensus diversity.\n */\n\nimport { detectField, extractSignals, calibrateScores, REFERENCE_BENCHMARKS } from \"./calibrationService.js\";\nimport { runLiveVerification, verificationToAdjustments } from \"./liveVerificationService.js\";\n\nconst SECTIONS = [\"abstract\", \"introduction\", \"methodology\", \"results\", \"discussion\", \"conclusion\", \"references\"];\n\n// ── Load keys with rotation ─────────────────────────────────────────────────\n\nfunction loadKeys(envPrefix, maxKeys = 10) {\n const keys = [];\n for (let i = 1; i <= maxKeys; i++) {\n const k = process.env[`${envPrefix}_${i}`] || process.env[`${envPrefix}${i}`];\n if (k) keys.push(k);\n }\n // Also check single-key env vars\n const single = process.env[envPrefix];\n if (single && !keys.includes(single)) keys.unshift(single);\n return keys;\n}\n\nconst keyIndices = {};\nfunction nextKey(providerId, keys) {\n if (!keys.length) return null;\n const idx = keyIndices[providerId] || 0;\n const key = keys[idx % keys.length];\n keyIndices[providerId] = (idx + 1) % keys.length;\n return key;\n}\n\n// ── Provider definitions ────────────────────────────────────────────────────\n\nconst PROVIDERS = [\n // --- Cerebras: 3 keys x 2 models = up to 6 independent judges ---\n {\n id: \"cerebras-qwen\",\n name: \"Cerebras-Qwen235B\",\n url: \"https://api.cerebras.ai/v1/chat/completions\",\n model: \"qwen-3-235b-a22b-instruct-2507\",\n keys: loadKeys(\"CEREBRAS_API_KEY\").concat(loadKeys(\"CEREBRAS_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 1024,\n },\n {\n id: \"cerebras-llama\",\n name: \"Cerebras-Llama8B\",\n url: \"https://api.cerebras.ai/v1/chat/completions\",\n model: \"llama3.1-8b\",\n keys: loadKeys(\"CEREBRAS_API_KEY\").concat(loadKeys(\"CEREBRAS_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n // --- Mistral: 2 keys, reliable ---\n {\n id: \"mistral\",\n name: \"Mistral\",\n url: \"https://api.mistral.ai/v1/chat/completions\",\n model: \"mistral-small-latest\",\n keys: loadKeys(\"MISTRAL_API_KEY\").concat(loadKeys(\"MISTRAL_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n // --- Sarvam (Indian AI): sarvam-m, uses <think> tags (needs 2048+ tokens) ---\n {\n id: \"sarvam\",\n name: \"Sarvam\",\n url: \"https://api.sarvam.ai/v1/chat/completions\",\n model: \"sarvam-m\",\n keys: loadKeys(\"SARVAM_KEY\").concat(loadKeys(\"SARVAM_API_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 2048,\n },\n // --- OpenRouter: free models ---\n {\n id: \"openrouter\",\n name: \"OpenRouter\",\n url: \"https://openrouter.ai/api/v1/chat/completions\",\n model: \"qwen/qwen3-coder:free\",\n keys: loadKeys(\"OPENROUTER_API_KEY\"),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n extraHeaders: { \"HTTP-Referer\": \"https://www.p2pclaw.com\", \"X-Title\": \"P2PCLAW Scoring\" },\n },\n // --- Groq: 7 keys but may be org-restricted ---\n {\n id: \"groq\",\n name: \"Groq\",\n url: \"https://api.groq.com/openai/v1/chat/completions\",\n model: \"llama-3.3-70b-versatile\",\n keys: loadKeys(\"GROQ_API_KEY\").concat(loadKeys(\"GROQ_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n // --- NVIDIA: 3 keys ---\n {\n id: \"nvidia\",\n name: \"NVIDIA\",\n url: \"https://integrate.api.nvidia.com/v1/chat/completions\",\n model: \"meta/llama-3.3-70b-instruct\",\n keys: loadKeys(\"NVAPI_KEY\").concat(loadKeys(\"NVIDIA_API_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n // --- Inception: mercury-2 ---\n {\n id: \"inception\",\n name: \"Inception\",\n url: \"https://api.inceptionlabs.ai/v1/chat/completions\",\n model: \"mercury-2\",\n keys: loadKeys(\"INCEPTION_API_KEY\").concat(loadKeys(\"INCEPTION_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n // --- Xiaomi MiMo: 3 keys x 2 models = up to 2 independent judges ---\n {\n id: \"xiaomi-flash\",\n name: \"Xiaomi-MiMo-Flash\",\n url: \"https://api.xiaomimimo.com/v1/chat/completions\",\n model: \"mimo-v2-flash\",\n keys: loadKeys(\"XIAOMI_API_KEY\").concat(loadKeys(\"XIAOMI_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n {\n id: \"xiaomi-pro\",\n name: \"Xiaomi-MiMo-Pro\",\n url: \"https://api.xiaomimimo.com/v1/chat/completions\",\n model: \"mimo-v2-pro\",\n keys: loadKeys(\"XIAOMI_API_KEY\").concat(loadKeys(\"XIAOMI_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 1024,\n },\n // --- Cohere: command-a-reasoning model (8 keys, reasoning/thinking model) ---\n {\n id: \"cohere\",\n name: \"Cohere-CommandA\",\n url: \"https://api.cohere.com/v2/chat\",\n model: \"command-a-reasoning-08-2025\",\n keys: loadKeys(\"COHERE_API_KEY\").concat(loadKeys(\"COHERE_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 4096, // reasoning model needs extra tokens for thinking + answer\n responseFormat: \"cohere\", // data.message.content[] array with {type:\"thinking\"} + {type:\"text\"}\n timeout: 90000, // 90s — reasoning model needs time to think through all 10 dimensions\n },\n // --- Cloudflare Workers AI: 6 models across 6 accounts (all FREE) ---\n // Ordered by model quality: best first\n {\n id: \"cloudflare-glm\",\n name: \"Cloudflare-GLM4\",\n url: `https://api.cloudflare.com/client/v4/accounts/eaffd2b52c95c69aaad8d859e9dcb52b/ai/run/@cf/zai-org/glm-4.7-flash`,\n model: \"@cf/zai-org/glm-4.7-flash\",\n keys: loadKeys(\"CF_AI_TOKEN\").concat(loadKeys(\"CLOUDFLARE_AI_TOKEN\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 2048,\n responseFormat: \"cloudflare\",\n timeout: 60000,\n },\n {\n id: \"cloudflare-gemma4\",\n name: \"Cloudflare-Gemma4\",\n url: `https://api.cloudflare.com/client/v4/accounts/a7995d3f33b6ba57955749337c9abbe0/ai/run/@cf/google/gemma-4-26B-A4B-it`,\n model: \"@cf/google/gemma-4-26B-A4B-it\",\n keys: loadKeys(\"CF_AI_TOKEN_2\"),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 1024,\n responseFormat: \"cloudflare\",\n timeout: 60000,\n },\n {\n id: \"cloudflare-nemotron\",\n name: \"Cloudflare-Nemotron\",\n url: `https://api.cloudflare.com/client/v4/accounts/194d9aea21482ac893ed81fc6b004864/ai/run/@cf/nvidia/nemotron-3-120b-a12b`,\n model: \"@cf/nvidia/nemotron-3-120b-a12b\",\n keys: loadKeys(\"CF_AI_TOKEN_3\"),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 1024,\n responseFormat: \"cloudflare\",\n timeout: 60000,\n },\n {\n id: \"cloudflare-kimi\",\n name: \"Cloudflare-Kimi\",\n url: `https://api.cloudflare.com/client/v4/accounts/401a75ead25275262c1c05eecb7a997c/ai/run/@cf/moonshotai/kimi-k2.5`,\n model: \"@cf/moonshotai/kimi-k2.5\",\n keys: loadKeys(\"CF_AI_TOKEN_4\"),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 2048,\n responseFormat: \"cloudflare\",\n timeout: 60000,\n },\n {\n id: \"cloudflare-gptoss\",\n name: \"Cloudflare-GPT-OSS\",\n url: `https://api.cloudflare.com/client/v4/accounts/73340519f6430362daee759ba0b48ce8/ai/run/@cf/openai/gpt-oss-120b`,\n model: \"@cf/openai/gpt-oss-120b\",\n keys: loadKeys(\"CF_AI_TOKEN_5\"),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 1024,\n responseFormat: \"cloudflare\",\n timeout: 60000,\n },\n {\n id: \"cloudflare-qwen3\",\n name: \"Cloudflare-Qwen3\",\n url: `https://api.cloudflare.com/client/v4/accounts/df4a7888befcbb6ce3e0a0b346ea1990/ai/run/@cf/qwen/qwen3-30b-a3b-fp8`,\n model: \"@cf/qwen/qwen3-30b-a3b-fp8\",\n keys: loadKeys(\"CF_AI_TOKEN_6\"),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 2048,\n responseFormat: \"cloudflare\",\n timeout: 60000,\n },\n];\n\n// Deduplicate keys within each provider\nfor (const p of PROVIDERS) {\n p.keys = [...new Set(p.keys)].filter(Boolean);\n}\n\n// Log available providers\nconst available = PROVIDERS.filter(p => p.keys.length > 0);\nconsole.log(`[SCORING] ${available.length} LLM providers available: ${available.map(p => `${p.name}(${p.keys.length})`).join(\", \")}`);\nif (available.length === 0) console.warn(\"[SCORING] No LLM providers — heuristic scoring only.\");\n// Debug: log specifically which providers have NO keys\nconst unavailable = PROVIDERS.filter(p => p.keys.length === 0);\nif (unavailable.length > 0) console.log(`[SCORING] Providers with NO keys: ${unavailable.map(p => p.name).join(\", \")}`);\n\nconst SCORING_PROMPT = `You are an academic paper quality evaluator for the P2PCLAW decentralized research network.\n\nScore this paper on a 0-10 scale for each criterion. For EACH score, include a brief \"why\" (1 sentence explaining the score).\n\nScoring criteria:\n- abstract (0-10): Quality, clarity, and completeness of the abstract. Clear problem statement, scope, and key results summarized.\n- introduction (0-10): Problem statement clarity, context, motivation. Should cite 2+ related works.\n- methodology (0-10): Rigor, reproducibility, appropriate methods. Must describe steps clearly enough to reproduce.\n- results (0-10): Quality of findings, data presentation, significance. Quantitative data strongly preferred.\n- discussion (0-10): Interpretation depth, limitations acknowledged, implications for the field.\n- conclusion (0-10): Summary of key findings (expected, NOT penalized as repetition), concrete future work directions, and connection back to the research question. A good conclusion SHOULD summarize findings — that is standard scientific practice.\n- references (0-10): Citation quality, relevance, proper formatting (0 if no real references). Real author names, titles, years, DOIs/URLs expected.\n- novelty (0-10): Original contribution to the field. Novel frameworks, terminology, or approaches.\n- reproducibility (0-10): Could another researcher reproduce this work? Code, equations, detailed parameters help.\n- citation_quality (0-10): Are references real, relevant, and properly cited? 8+ unique real citations expected for high scores.\n\nIMPORTANT RULES:\n- If a section is MISSING entirely, score it 0\n- If references are placeholder/fake (e.g. \"[1] Author, A. (2026). Placeholder\"), score references as 1\n- Trivial proofs or papers under 300 words should score below 3 overall\n- Papers that prove obvious things (like 0*0=0) should score novelty as 0-1\n- Be STRICT — a score of 8+ means genuinely publishable quality\n- Lean 4 verified papers with valid proof hashes deserve reproducibility bonus\n\nReturn ONLY this JSON format (numbers 0-10, nothing else):\n{\"abstract\":N,\"introduction\":N,\"methodology\":N,\"results\":N,\"discussion\":N,\"conclusion\":N,\"references\":N,\"novelty\":N,\"reproducibility\":N,\"citation_quality\":N}\n\nPaper content:\n`;\n\n/**\n * Call a single LLM provider to score a paper.\n * Tries up to 2 keys from the provider before giving up.\n */\nasync function callLLMForScoring(prompt, provider) {\n if (!provider.keys.length) return null;\n\n const maxAttempts = Math.min(provider.keys.length, 2);\n for (let attempt = 0; attempt < maxAttempts; attempt++) {\n const key = nextKey(provider.id, provider.keys);\n if (!key) return null;\n\n const headers = { \"Content-Type\": \"application/json\" };\n headers[provider.authHeader] = `${provider.authPrefix}${key}`;\n if (provider.extraHeaders) Object.assign(headers, provider.extraHeaders);\n\n try {\n const res = await fetch(provider.url, {\n method: \"POST\",\n headers,\n body: JSON.stringify({\n model: provider.model,\n messages: [{ role: \"user\", content: prompt }],\n max_tokens: provider.maxTokens || 512,\n temperature: 0.1,\n }),\n signal: AbortSignal.timeout(provider.timeout || 30000),\n });\n\n if (res.status === 429) {\n console.warn(`[SCORING] ${provider.name} 429 rate-limited`);\n await new Promise(r => setTimeout(r, 3000));\n continue;\n }\n if (res.status === 402) {\n console.warn(`[SCORING] ${provider.name} 402 credits exhausted — skipping provider`);\n return null;\n }\n if (!res.ok) {\n const errBody = await res.text().catch(() => \"\");\n console.warn(`[SCORING] ${provider.name} HTTP ${res.status}: ${errBody.substring(0, 150)}`);\n continue;\n }\n\n const data = await res.json();\n // Support both OpenAI format (choices[0].message.content) and Cohere v2 (message.content[] array)\n let text = \"\";\n if (provider.responseFormat === \"cohere\") {\n // Cohere v2 returns array of content blocks: [{type:\"thinking\",...}, {type:\"text\",text:\"...\"}]\n const blocks = data.message?.content || [];\n if (Array.isArray(blocks)) {\n const textBlock = blocks.find(b => b.type === \"text\");\n text = textBlock?.text || blocks[blocks.length - 1]?.text || \"\";\n } else {\n text = typeof blocks === \"string\" ? blocks : \"\";\n }\n } else if (provider.responseFormat === \"cloudflare\") {\n // Cloudflare Workers AI wraps OpenAI format: {result: {choices: [...]}, success: true}\n const inner = data.result || data;\n text = inner.choices?.[0]?.message?.content || \"\";\n } else {\n text = data.choices?.[0]?.message?.content || \"\";\n }\n\n // Strip <think>...</think> tags (Sarvam, Qwen with thinking mode)\n if (provider.stripThinkTags || text.includes(\"<think>\")) {\n // Strip closed think blocks\n text = text.replace(/<think>[\\s\\S]*?<\\/think>/g, \"\").trim();\n // Strip unclosed think blocks (model ran out of tokens mid-think)\n if (text.startsWith(\"<think>\")) {\n text = text.replace(/<think>[\\s\\S]*/g, \"\").trim();\n }\n }\n\n // Extract JSON — use balanced braces to handle nested objects like {\"score\":8,\"why\":\"...\"}\n let jsonMatch = null;\n const firstBrace = text.indexOf('{');\n if (firstBrace !== -1) {\n let depth = 0, end = -1;\n for (let i = firstBrace; i < text.length; i++) {\n if (text[i] === '{') depth++;\n else if (text[i] === '}') { depth--; if (depth === 0) { end = i; break; } }\n }\n if (end !== -1) jsonMatch = [text.substring(firstBrace, end + 1)];\n }\n if (!jsonMatch) {\n console.warn(`[SCORING] ${provider.name} — no JSON in response`);\n continue;\n }\n\n const parsed = JSON.parse(jsonMatch[0]);\n const fields = [...SECTIONS, \"novelty\", \"reproducibility\", \"citation_quality\"];\n const feedback = {};\n for (const field of fields) {\n let val = parsed[field];\n // Support both plain number and {score, why} object format\n if (val && typeof val === \"object\" && typeof val.score === \"number\") {\n feedback[field] = val.why || null;\n val = val.score;\n }\n if (typeof val !== \"number\" || val < 0 || val > 10) {\n val = typeof val === \"number\" ? Math.max(0, Math.min(10, Math.round(val))) : 5;\n }\n parsed[field] = val;\n }\n\n // Clean parsed: only keep recognized score fields (remove stray \"why\" etc.)\n const cleanScores = {};\n for (const field of fields) cleanScores[field] = parsed[field];\n\n console.log(`[SCORING] ${provider.name} scored successfully`);\n return { scores: cleanScores, provider: provider.name, feedback: Object.keys(feedback).length > 0 ? feedback : null };\n } catch (e) {\n console.warn(`[SCORING] ${provider.name} error: ${e.message}`);\n }\n }\n return null;\n}\n\n/**\n * Deterministic heuristic scoring — used when all LLMs fail.\n */\nfunction heuristicScore(content) {\n const text = content || \"\";\n const words = text.split(/\\s+/).filter(w => w.length > 0);\n const wordCount = words.length;\n const lowerText = text.toLowerCase();\n\n const sectionScores = {};\n for (const section of SECTIONS) {\n const hasSection = lowerText.includes(`## ${section}`) || lowerText.includes(`# ${section}`);\n if (!hasSection) {\n sectionScores[section] = 0;\n continue;\n }\n const sectionRegex = new RegExp(`##?\\\\s*${section}[\\\\s\\\\S]*?(?=##?\\\\s|$)`, \"i\");\n const match = text.match(sectionRegex);\n const sectionWords = match ? match[0].split(/\\s+/).length : 0;\n if (sectionWords < 20) sectionScores[section] = 2;\n else if (sectionWords < 50) sectionScores[section] = 4;\n else if (sectionWords < 100) sectionScores[section] = 5;\n else if (sectionWords < 200) sectionScores[section] = 6;\n else sectionScores[section] = 7;\n }\n\n const refMatches = text.match(/\\[\\d+\\]/g) || [];\n const uniqueRefs = new Set(refMatches).size;\n const hasPlaceholderRefs = /placeholder|author,?\\s*a\\.\\s*\\(\\d{4}\\)/i.test(text);\n const hasRealAuthors = /[A-Z][a-z]+,\\s*[A-Z]\\.\\s*(?:&|,|et al)/g.test(text);\n const hasDOI = /doi\\.org|arxiv\\.org|10\\.\\d{4}/i.test(text);\n let refScore = hasPlaceholderRefs ? 1 : Math.min(7, uniqueRefs);\n if (hasRealAuthors) refScore = Math.min(10, refScore + 1);\n if (hasDOI) refScore = Math.min(10, refScore + 1);\n sectionScores.references = refScore;\n\n const technicalTerms = (text.match(/\\b(algorithm|theorem|proof|complexity|O\\([^)]+\\)|convergence|optimal|novel|framework)\\b/gi) || []).length;\n const hasFigures = /figure \\d|fig\\.\\s*\\d|table \\d/i.test(text);\n let novelty = wordCount > 2000 ? 4 : wordCount > 1000 ? 3 : 2;\n novelty += Math.min(3, Math.floor(technicalTerms / 5));\n if (hasFigures) novelty += 1;\n\n const hasCode = /```[\\s\\S]*?```/.test(text);\n const hasEquations = /\\$[^$]+\\$/.test(text) || /\\\\begin\\{/.test(text);\n const hasNumbers = /\\d+\\.\\d+%|\\d+\\.\\d+x|p\\s*[<>]\\s*0\\.\\d/i.test(text);\n let reproducibility = (hasCode ? 5 : 3) + (hasEquations ? 1 : 0) + (wordCount > 2000 ? 1 : 0);\n if (hasNumbers) reproducibility += 1;\n\n const citation_quality = hasPlaceholderRefs ? 1 : Math.min(6, uniqueRefs);\n\n return {\n scores: {\n ...sectionScores,\n novelty: Math.min(10, novelty),\n reproducibility: Math.min(10, reproducibility),\n citation_quality: Math.min(10, citation_quality),\n },\n provider: \"heuristic\"\n };\n}\n\n/**\n * Score a paper using heterogeneous multi-LLM swarm.\n * Tries to get 2 independent judges, falls back through provider chain.\n */\nexport async function scoreGranular(content, paperType = \"research\") {\n if (!content || content.trim().length < 50) {\n return {\n sections: Object.fromEntries(SECTIONS.map(s => [s, 0])),\n overall: 0,\n novelty: 0,\n reproducibility: 0,\n citation_quality: 0,\n judges: [],\n judge_count: 0,\n error: \"Content too short to score\"\n };\n }\n\n // All LLM judges support 32k+ context. Send enough content to include ALL 7 mandatory sections.\n // Previous limit of 4000 chars truncated papers before methodology/results/conclusion, causing\n // strict judges (Cohere, Xiaomi) to correctly score missing sections as 0.\n const truncated = content.length > 16000 ? content.substring(0, 16000) + \"\\n\\n[... truncated for scoring ...]\" : content;\n const prompt = SCORING_PROMPT + truncated;\n\n // ALL available judges score independently for maximum consensus diversity.\n // Each model evaluates each section independently, then we average.\n // Run all judges in parallel for speed (each has its own timeout).\n const judgePromises = available.map(provider =>\n callLLMForScoring(prompt, provider).catch(() => null)\n );\n const judgeResults = await Promise.all(judgePromises);\n const judges = judgeResults.filter(Boolean);\n\n // If no LLM judges succeeded, use heuristic\n if (judges.length === 0) {\n const heuristic = heuristicScore(content);\n judges.push(heuristic);\n }\n\n // Average scores across all judges\n const allFields = [...SECTIONS, \"novelty\", \"reproducibility\", \"citation_quality\"];\n const averaged = {};\n for (const field of allFields) {\n const values = judges.map(j => j.scores[field]).filter(v => typeof v === \"number\");\n averaged[field] = values.length > 0\n ? Math.round((values.reduce((a, b) => a + b, 0) / values.length) * 10) / 10\n : 0;\n }\n\n // ── CALIBRATION PASS — adjust raw averages against reference benchmarks ──\n // This is the core fix for inflated scoring. Raw LLM averages are compared\n // against quality signals extracted from the paper content and calibrated\n // against recognized reference paper fingerprints.\n let calibration = null;\n try {\n const fieldResult = detectField(content);\n const signals = extractSignals(content);\n const benchmarks = REFERENCE_BENCHMARKS[fieldResult.field] || null;\n const { calibrated, adjustments } = calibrateScores(averaged, signals, benchmarks);\n\n // Apply calibrated scores over raw averages\n const adjustmentCount = Object.keys(adjustments).length;\n if (adjustmentCount > 0) {\n for (const [field, val] of Object.entries(calibrated)) {\n if (typeof val === \"number\" && averaged[field] !== undefined) {\n averaged[field] = val;\n }\n }\n console.log(`[SCORING] Calibration applied: ${adjustmentCount} adjustments, field=${fieldResult.field}, ` +\n `red_flags=${signals.red_flag_count}, depth=${signals.depth_score}`);\n }\n\n calibration = {\n field: fieldResult.field,\n field_confidence: fieldResult.confidence,\n signals_summary: {\n word_count: signals.word_count,\n sections_present: signals.sections_present.length,\n sections_missing: signals.sections_missing,\n red_flags: signals.red_flags,\n red_flag_count: signals.red_flag_count,\n has_formal_proofs: signals.has_formal_proofs,\n has_equations: signals.has_equations,\n has_code: signals.has_code,\n unique_refs: signals.unique_refs,\n has_placeholder_refs: signals.has_placeholder_refs,\n depth_score: signals.depth_score,\n evidence_markers: signals.evidence_markers,\n deception_count: signals.deception_count || 0,\n deception_matches: (signals.deception_matches || []).map(d => ({\n id: d.id, name: d.name, severity: d.severity,\n })),\n // New quality dimensions\n grammar: signals.grammar_quality ? {\n vocabulary_diversity: signals.grammar_quality.vocabulary_diversity_ttr,\n is_monotone: signals.grammar_quality.is_monotone,\n is_low_vocabulary: signals.grammar_quality.is_low_vocabulary,\n } : null,\n repetition_ratio: signals.repetition_score?.repetition_ratio || 0,\n code_quality: signals.code_quality?.blocks_found > 0 ? {\n blocks: signals.code_quality.blocks_found,\n has_real_code: signals.has_real_code || false,\n has_python: signals.code_quality.has_python || false,\n } : null,\n math_formulas: signals.math_quality?.formula_count || 0,\n lean4: signals.lean4_signals?.verification_level || \"none\",\n tables: signals.table_quality?.count || 0,\n },\n adjustments,\n adjustment_count: adjustmentCount,\n reference_papers: benchmarks ? benchmarks.references.map(r => r.title) : [],\n };\n } catch (calErr) {\n console.warn(`[SCORING] Calibration error (non-fatal): ${calErr.message}`);\n }\n\n // ── LIVE VERIFICATION PASS — real-time CrossRef, arXiv, code exec, Lean4 ──\n // Runs in parallel with independent timeouts. Non-fatal: if it fails, scoring\n // continues with just calibration. Results apply caps and bonuses to scores.\n let liveVerification = null;\n try {\n const verification = await runLiveVerification(content);\n const { adjustments: liveAdj, bonuses: liveBon } = verificationToAdjustments(verification);\n\n // ── OVERRIDE FALSE POSITIVES ──\n // If live verification confirmed code execution but calibration flagged\n // \"code_blocks_are_template_not_real\", the red_flag was a false positive.\n // Undo the -1.5 penalty applied to all dimensions by recalibrating.\n const codeExec = verification.code_execution;\n if (codeExec && codeExec.passed > 0 && calibration && calibration.signals_summary) {\n const redFlags = calibration.signals_summary.red_flags || [];\n if (redFlags.includes(\"code_blocks_are_template_not_real\")) {\n // The -1.5 penalty was applied to all 10 dimensions. Undo it.\n const allFields = [...SECTIONS, \"novelty\", \"reproducibility\", \"citation_quality\"];\n for (const field of allFields) {\n if (averaged[field] !== undefined) {\n averaged[field] = Math.min(10, Math.round((averaged[field] + 1.5) * 10) / 10);\n }\n }\n // Remove the false red flag from the report\n calibration.signals_summary.red_flags = redFlags.filter(f => f !== \"code_blocks_are_template_not_real\");\n calibration.signals_summary.red_flag_count = calibration.signals_summary.red_flags.length;\n calibration.false_positive_corrected = \"code_blocks_are_template_not_real (live verification confirmed code executes)\";\n console.log(`[SCORING] False positive corrected: code_blocks_are_template_not_real (live verification: ${codeExec.passed}/${codeExec.total} passed)`);\n }\n }\n\n // Apply caps (these override score to a max value)\n for (const [key, val] of Object.entries(liveAdj)) {\n if (key.endsWith(\"_cap\") && typeof val === \"number\") {\n const field = key.replace(\"_cap\", \"\");\n if (averaged[field] !== undefined && averaged[field] > val) {\n averaged[field] = val;\n }\n }\n }\n\n // Apply bonuses (add to score, capped at 10)\n for (const [key, val] of Object.entries(liveBon)) {\n if (key.endsWith(\"_bonus\") && typeof val === \"number\") {\n const field = key.replace(\"_bonus\", \"\");\n if (averaged[field] !== undefined) {\n averaged[field] = Math.min(10, Math.round((averaged[field] + val) * 10) / 10);\n }\n }\n }\n\n liveVerification = {\n verification_time_ms: verification.verification_time_ms,\n citations: verification.citations ? {\n total: verification.citations.total,\n verified: verification.citations.verified,\n verification_rate: verification.citations.verification_rate,\n } : null,\n novelty: verification.novelty ? {\n searched: verification.novelty.searched,\n total_found: verification.novelty.total_found,\n novelty_concern: verification.novelty.novelty_concern,\n max_similarity: verification.novelty.max_similarity,\n } : null,\n code_execution: verification.code_execution ? {\n total: verification.code_execution.total,\n passed: verification.code_execution.passed,\n failed: verification.code_execution.failed,\n } : null,\n lean4: verification.lean4 ? {\n blocks_found: verification.lean4.blocks_found,\n verified: verification.lean4.verified,\n has_unsubstantiated_claim: verification.lean4.has_unsubstantiated_claim || false,\n } : null,\n adjustments: liveAdj,\n bonuses: liveBon,\n };\n\n const adjCount = Object.keys(liveAdj).filter(k => k.endsWith(\"_cap\")).length;\n const bonCount = Object.keys(liveBon).filter(k => k.endsWith(\"_bonus\")).length;\n if (adjCount > 0 || bonCount > 0) {\n console.log(`[SCORING] Live verification: ${adjCount} caps, ${bonCount} bonuses applied (${verification.verification_time_ms}ms)`);\n }\n } catch (liveErr) {\n console.warn(`[SCORING] Live verification error (non-fatal): ${liveErr.message}`);\n }\n\n const sectionValues = SECTIONS.map(s => averaged[s]);\n const overall = Math.round((sectionValues.reduce((a, b) => a + b, 0) / SECTIONS.length) * 10) / 10;\n\n // Per-judge detail breakdown (individual scores + feedback)\n const judge_details = judges.map(j => ({\n judge: j.provider,\n scores: j.scores,\n feedback: j.feedback || null,\n }));\n\n // Consensus score per dimension (1 = all judges agree, 0 = total disagreement)\n const consensus = {};\n for (const field of allFields) {\n const values = judges.map(j => j.scores[field]).filter(v => typeof v === \"number\");\n if (values.length < 2) { consensus[field] = 1.0; continue; }\n const mean = values.reduce((a, b) => a + b, 0) / values.length;\n const variance = values.reduce((sum, v) => sum + (v - mean) ** 2, 0) / values.length;\n const stddev = Math.sqrt(variance);\n consensus[field] = Math.round(Math.max(0, 1 - stddev / 5) * 100) / 100;\n }\n const overall_consensus = Math.round(\n (allFields.reduce((sum, f) => sum + consensus[f], 0) / allFields.length) * 100\n ) / 100;\n\n // Aggregate feedback per dimension (from all judges that provided it)\n const aggregated_feedback = {};\n for (const field of allFields) {\n const comments = judges\n .filter(j => j.feedback && j.feedback[field])\n .map(j => ({ judge: j.provider, comment: j.feedback[field] }));\n if (comments.length > 0) aggregated_feedback[field] = comments;\n }\n\n const result = {\n sections: Object.fromEntries(SECTIONS.map(s => [s, averaged[s]])),\n overall,\n novelty: averaged.novelty,\n reproducibility: averaged.reproducibility,\n citation_quality: averaged.citation_quality,\n judges: judges.map(j => j.provider),\n judge_count: judges.length,\n judge_details,\n consensus,\n overall_consensus,\n feedback: Object.keys(aggregated_feedback).length > 0 ? aggregated_feedback : null,\n scored_at: new Date().toISOString(),\n paper_type: paperType,\n calibration,\n live_verification: liveVerification,\n };\n\n console.log(`[SCORING] Granular score: overall=${overall}, consensus=${overall_consensus}, judges=${result.judges.join(\",\")}`);\n return result;\n}\n"}, "db48a46481fefbe6fa76a30ca81e939ddc43f098": {"t": 1785753933.6928687, "n": 31135, "h": "3d70e8f97f0aa8c6", "c": "/**\n * P2PCLAW Granular Scoring Service\n * =================================\n * Heterogeneous multi-LLM scoring engine that evaluates papers section-by-section.\n *\n * Provider chain (updated 2026-04-01):\n * 1. Cerebras — qwen-3-235b (3 keys, free, ultra-fast)\n * 2. Cerebras — llama3.1-8b (3 keys, free, ultra-fast)\n * 3. Mistral — mistral-small-latest (2 keys, free)\n * 4. Sarvam — sarvam-m (Indian AI, free)\n * 5. OpenRouter — qwen3-coder:free (free)\n * 6. Groq — llama-3.3-70b-versatile (7 keys, may be restricted)\n * 7. NVIDIA — meta/llama-3.3-70b-instruct (3 keys, free)\n * 8. Inception — mercury-2 (5 keys, free)\n * 9. Xiaomi MiMo — mimo-v2-flash (3 keys, free)\n * 10. Xiaomi MiMo — mimo-v2-pro (3 keys, free, reasoning)\n * 11. Cohere — command-a-reasoning (8 keys, reasoning model)\n * 12. Deterministic heuristic fallback (never blocks)\n *\n * ALL available judges score independently. Final score = average across all judges.\n * Each model evaluates each section independently for maximum consensus diversity.\n */\n\nimport { detectField, extractSignals, calibrateScores, REFERENCE_BENCHMARKS } from \"./calibrationService.js\";\nimport { runLiveVerification, verificationToAdjustments } from \"./liveVerificationService.js\";\n\nconst SECTIONS = [\"abstract\", \"introduction\", \"methodology\", \"results\", \"discussion\", \"conclusion\", \"references\"];\n\n// ── Load keys with rotation ─────────────────────────────────────────────────\n\nfunction loadKeys(envPrefix, maxKeys = 10) {\n const keys = [];\n for (let i = 1; i <= maxKeys; i++) {\n const k = process.env[`${envPrefix}_${i}`] || process.env[`${envPrefix}${i}`];\n if (k) keys.push(k);\n }\n // Also check single-key env vars\n const single = process.env[envPrefix];\n if (single && !keys.includes(single)) keys.unshift(single);\n return keys;\n}\n\nconst keyIndices = {};\nfunction nextKey(providerId, keys) {\n if (!keys.length) return null;\n const idx = keyIndices[providerId] || 0;\n const key = keys[idx % keys.length];\n keyIndices[providerId] = (idx + 1) % keys.length;\n return key;\n}\n\n// ── Provider definitions ────────────────────────────────────────────────────\n\nconst PROVIDERS = [\n // --- Cerebras: 3 keys x 2 models = up to 6 independent judges ---\n {\n id: \"cerebras-qwen\",\n name: \"Cerebras-Qwen235B\",\n url: \"https://api.cerebras.ai/v1/chat/completions\",\n model: \"qwen-3-235b-a22b-instruct-2507\",\n keys: loadKeys(\"CEREBRAS_API_KEY\").concat(loadKeys(\"CEREBRAS_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 1024,\n },\n {\n id: \"cerebras-llama\",\n name: \"Cerebras-Llama8B\",\n url: \"https://api.cerebras.ai/v1/chat/completions\",\n model: \"llama3.1-8b\",\n keys: loadKeys(\"CEREBRAS_API_KEY\").concat(loadKeys(\"CEREBRAS_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n // --- Mistral: 2 keys, reliable ---\n {\n id: \"mistral\",\n name: \"Mistral\",\n url: \"https://api.mistral.ai/v1/chat/completions\",\n model: \"mistral-small-latest\",\n keys: loadKeys(\"MISTRAL_API_KEY\").concat(loadKeys(\"MISTRAL_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n // --- Sarvam (Indian AI): sarvam-m, uses <think> tags (needs 2048+ tokens) ---\n {\n id: \"sarvam\",\n name: \"Sarvam\",\n url: \"https://api.sarvam.ai/v1/chat/completions\",\n model: \"sarvam-m\",\n keys: loadKeys(\"SARVAM_KEY\").concat(loadKeys(\"SARVAM_API_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 2048,\n },\n // --- OpenRouter: free models ---\n {\n id: \"openrouter\",\n name: \"OpenRouter\",\n url: \"https://openrouter.ai/api/v1/chat/completions\",\n model: \"qwen/qwen3-coder:free\",\n keys: loadKeys(\"OPENROUTER_API_KEY\"),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n extraHeaders: { \"HTTP-Referer\": \"https://www.p2pclaw.com\", \"X-Title\": \"P2PCLAW Scoring\" },\n },\n // --- Groq: 7 keys but may be org-restricted ---\n {\n id: \"groq\",\n name: \"Groq\",\n url: \"https://api.groq.com/openai/v1/chat/completions\",\n model: \"llama-3.3-70b-versatile\",\n keys: loadKeys(\"GROQ_API_KEY\").concat(loadKeys(\"GROQ_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n // --- NVIDIA: 3 keys ---\n {\n id: \"nvidia\",\n name: \"NVIDIA\",\n url: \"https://integrate.api.nvidia.com/v1/chat/completions\",\n model: \"meta/llama-3.3-70b-instruct\",\n keys: loadKeys(\"NVAPI_KEY\").concat(loadKeys(\"NVIDIA_API_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n // --- Inception: mercury-2 ---\n {\n id: \"inception\",\n name: \"Inception\",\n url: \"https://api.inceptionlabs.ai/v1/chat/completions\",\n model: \"mercury-2\",\n keys: loadKeys(\"INCEPTION_API_KEY\").concat(loadKeys(\"INCEPTION_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n // --- Xiaomi MiMo: 3 keys x 2 models = up to 2 independent judges ---\n {\n id: \"xiaomi-flash\",\n name: \"Xiaomi-MiMo-Flash\",\n url: \"https://api.xiaomimimo.com/v1/chat/completions\",\n model: \"mimo-v2-flash\",\n keys: loadKeys(\"XIAOMI_API_KEY\").concat(loadKeys(\"XIAOMI_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n {\n id: \"xiaomi-pro\",\n name: \"Xiaomi-MiMo-Pro\",\n url: \"https://api.xiaomimimo.com/v1/chat/completions\",\n model: \"mimo-v2-pro\",\n keys: loadKeys(\"XIAOMI_API_KEY\").concat(loadKeys(\"XIAOMI_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 1024,\n },\n // --- Cohere: command-a-reasoning model (8 keys, reasoning/thinking model) ---\n {\n id: \"cohere\",\n name: \"Cohere-CommandA\",\n url: \"https://api.cohere.com/v2/chat\",\n model: \"command-a-reasoning-08-2025\",\n keys: loadKeys(\"COHERE_API_KEY\").concat(loadKeys(\"COHERE_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 4096, // reasoning model needs extra tokens for thinking + answer\n responseFormat: \"cohere\", // data.message.content[] array with {type:\"thinking\"} + {type:\"text\"}\n timeout: 90000, // 90s — reasoning model needs time to think through all 10 dimensions\n },\n // --- Cloudflare Workers AI: GLM-4.7 Flash (ZhipuAI, reasoning model, free) ---\n {\n id: \"cloudflare-glm\",\n name: \"Cloudflare-GLM4\",\n url: `https://api.cloudflare.com/client/v4/accounts/${process.env.CF_ACCOUNT_ID || \"eaffd2b52c95c69aaad8d859e9dcb52b\"}/ai/run/@cf/zai-org/glm-4.7-flash`,\n model: \"@cf/zai-org/glm-4.7-flash\",\n keys: loadKeys(\"CF_AI_TOKEN\").concat(loadKeys(\"CLOUDFLARE_AI_TOKEN\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 2048, // reasoning model includes thinking + answer\n responseFormat: \"cloudflare\", // {result: {choices: [{message: {content}}]}, success: true}\n timeout: 60000, // 60s — reasoning model needs time\n },\n];\n\n// Deduplicate keys within each provider\nfor (const p of PROVIDERS) {\n p.keys = [...new Set(p.keys)].filter(Boolean);\n}\n\n// Log available providers\nconst available = PROVIDERS.filter(p => p.keys.length > 0);\nconsole.log(`[SCORING] ${available.length} LLM providers available: ${available.map(p => `${p.name}(${p.keys.length})`).join(\", \")}`);\nif (available.length === 0) console.warn(\"[SCORING] No LLM providers — heuristic scoring only.\");\n// Debug: log specifically which providers have NO keys\nconst unavailable = PROVIDERS.filter(p => p.keys.length === 0);\nif (unavailable.length > 0) console.log(`[SCORING] Providers with NO keys: ${unavailable.map(p => p.name).join(\", \")}`);\n\nconst SCORING_PROMPT = `You are an academic paper quality evaluator for the P2PCLAW decentralized research network.\n\nScore this paper on a 0-10 scale for each criterion. For EACH score, include a brief \"why\" (1 sentence explaining the score).\n\nScoring criteria:\n- abstract (0-10): Quality, clarity, and completeness of the abstract. Clear problem statement, scope, and key results summarized.\n- introduction (0-10): Problem statement clarity, context, motivation. Should cite 2+ related works.\n- methodology (0-10): Rigor, reproducibility, appropriate methods. Must describe steps clearly enough to reproduce.\n- results (0-10): Quality of findings, data presentation, significance. Quantitative data strongly preferred.\n- discussion (0-10): Interpretation depth, limitations acknowledged, implications for the field.\n- conclusion (0-10): Summary of key findings (expected, NOT penalized as repetition), concrete future work directions, and connection back to the research question. A good conclusion SHOULD summarize findings — that is standard scientific practice.\n- references (0-10): Citation quality, relevance, proper formatting (0 if no real references). Real author names, titles, years, DOIs/URLs expected.\n- novelty (0-10): Original contribution to the field. Novel frameworks, terminology, or approaches.\n- reproducibility (0-10): Could another researcher reproduce this work? Code, equations, detailed parameters help.\n- citation_quality (0-10): Are references real, relevant, and properly cited? 8+ unique real citations expected for high scores.\n\nIMPORTANT RULES:\n- If a section is MISSING entirely, score it 0\n- If references are placeholder/fake (e.g. \"[1] Author, A. (2026). Placeholder\"), score references as 1\n- Trivial proofs or papers under 300 words should score below 3 overall\n- Papers that prove obvious things (like 0*0=0) should score novelty as 0-1\n- Be STRICT — a score of 8+ means genuinely publishable quality\n- Lean 4 verified papers with valid proof hashes deserve reproducibility bonus\n\nReturn ONLY this JSON format (numbers 0-10, nothing else):\n{\"abstract\":N,\"introduction\":N,\"methodology\":N,\"results\":N,\"discussion\":N,\"conclusion\":N,\"references\":N,\"novelty\":N,\"reproducibility\":N,\"citation_quality\":N}\n\nPaper content:\n`;\n\n/**\n * Call a single LLM provider to score a paper.\n * Tries up to 2 keys from the provider before giving up.\n */\nasync function callLLMForScoring(prompt, provider) {\n if (!provider.keys.length) return null;\n\n const maxAttempts = Math.min(provider.keys.length, 2);\n for (let attempt = 0; attempt < maxAttempts; attempt++) {\n const key = nextKey(provider.id, provider.keys);\n if (!key) return null;\n\n const headers = { \"Content-Type\": \"application/json\" };\n headers[provider.authHeader] = `${provider.authPrefix}${key}`;\n if (provider.extraHeaders) Object.assign(headers, provider.extraHeaders);\n\n try {\n const res = await fetch(provider.url, {\n method: \"POST\",\n headers,\n body: JSON.stringify({\n model: provider.model,\n messages: [{ role: \"user\", content: prompt }],\n max_tokens: provider.maxTokens || 512,\n temperature: 0.1,\n }),\n signal: AbortSignal.timeout(provider.timeout || 30000),\n });\n\n if (res.status === 429) {\n console.warn(`[SCORING] ${provider.name} 429 rate-limited`);\n await new Promise(r => setTimeout(r, 3000));\n continue;\n }\n if (res.status === 402) {\n console.warn(`[SCORING] ${provider.name} 402 credits exhausted — skipping provider`);\n return null;\n }\n if (!res.ok) {\n const errBody = await res.text().catch(() => \"\");\n console.warn(`[SCORING] ${provider.name} HTTP ${res.status}: ${errBody.substring(0, 150)}`);\n continue;\n }\n\n const data = await res.json();\n // Support both OpenAI format (choices[0].message.content) and Cohere v2 (message.content[] array)\n let text = \"\";\n if (provider.responseFormat === \"cohere\") {\n // Cohere v2 returns array of content blocks: [{type:\"thinking\",...}, {type:\"text\",text:\"...\"}]\n const blocks = data.message?.content || [];\n if (Array.isArray(blocks)) {\n const textBlock = blocks.find(b => b.type === \"text\");\n text = textBlock?.text || blocks[blocks.length - 1]?.text || \"\";\n } else {\n text = typeof blocks === \"string\" ? blocks : \"\";\n }\n } else if (provider.responseFormat === \"cloudflare\") {\n // Cloudflare Workers AI wraps OpenAI format: {result: {choices: [...]}, success: true}\n const inner = data.result || data;\n text = inner.choices?.[0]?.message?.content || \"\";\n } else {\n text = data.choices?.[0]?.message?.content || \"\";\n }\n\n // Strip <think>...</think> tags (Sarvam, Qwen with thinking mode)\n if (provider.stripThinkTags || text.includes(\"<think>\")) {\n // Strip closed think blocks\n text = text.replace(/<think>[\\s\\S]*?<\\/think>/g, \"\").trim();\n // Strip unclosed think blocks (model ran out of tokens mid-think)\n if (text.startsWith(\"<think>\")) {\n text = text.replace(/<think>[\\s\\S]*/g, \"\").trim();\n }\n }\n\n // Extract JSON — use balanced braces to handle nested objects like {\"score\":8,\"why\":\"...\"}\n let jsonMatch = null;\n const firstBrace = text.indexOf('{');\n if (firstBrace !== -1) {\n let depth = 0, end = -1;\n for (let i = firstBrace; i < text.length; i++) {\n if (text[i] === '{') depth++;\n else if (text[i] === '}') { depth--; if (depth === 0) { end = i; break; } }\n }\n if (end !== -1) jsonMatch = [text.substring(firstBrace, end + 1)];\n }\n if (!jsonMatch) {\n console.warn(`[SCORING] ${provider.name} — no JSON in response`);\n continue;\n }\n\n const parsed = JSON.parse(jsonMatch[0]);\n const fields = [...SECTIONS, \"novelty\", \"reproducibility\", \"citation_quality\"];\n const feedback = {};\n for (const field of fields) {\n let val = parsed[field];\n // Support both plain number and {score, why} object format\n if (val && typeof val === \"object\" && typeof val.score === \"number\") {\n feedback[field] = val.why || null;\n val = val.score;\n }\n if (typeof val !== \"number\" || val < 0 || val > 10) {\n val = typeof val === \"number\" ? Math.max(0, Math.min(10, Math.round(val))) : 5;\n }\n parsed[field] = val;\n }\n\n // Clean parsed: only keep recognized score fields (remove stray \"why\" etc.)\n const cleanScores = {};\n for (const field of fields) cleanScores[field] = parsed[field];\n\n console.log(`[SCORING] ${provider.name} scored successfully`);\n return { scores: cleanScores, provider: provider.name, feedback: Object.keys(feedback).length > 0 ? feedback : null };\n } catch (e) {\n console.warn(`[SCORING] ${provider.name} error: ${e.message}`);\n }\n }\n return null;\n}\n\n/**\n * Deterministic heuristic scoring — used when all LLMs fail.\n */\nfunction heuristicScore(content) {\n const text = content || \"\";\n const words = text.split(/\\s+/).filter(w => w.length > 0);\n const wordCount = words.length;\n const lowerText = text.toLowerCase();\n\n const sectionScores = {};\n for (const section of SECTIONS) {\n const hasSection = lowerText.includes(`## ${section}`) || lowerText.includes(`# ${section}`);\n if (!hasSection) {\n sectionScores[section] = 0;\n continue;\n }\n const sectionRegex = new RegExp(`##?\\\\s*${section}[\\\\s\\\\S]*?(?=##?\\\\s|$)`, \"i\");\n const match = text.match(sectionRegex);\n const sectionWords = match ? match[0].split(/\\s+/).length : 0;\n if (sectionWords < 20) sectionScores[section] = 2;\n else if (sectionWords < 50) sectionScores[section] = 4;\n else if (sectionWords < 100) sectionScores[section] = 5;\n else if (sectionWords < 200) sectionScores[section] = 6;\n else sectionScores[section] = 7;\n }\n\n const refMatches = text.match(/\\[\\d+\\]/g) || [];\n const uniqueRefs = new Set(refMatches).size;\n const hasPlaceholderRefs = /placeholder|author,?\\s*a\\.\\s*\\(\\d{4}\\)/i.test(text);\n const hasRealAuthors = /[A-Z][a-z]+,\\s*[A-Z]\\.\\s*(?:&|,|et al)/g.test(text);\n const hasDOI = /doi\\.org|arxiv\\.org|10\\.\\d{4}/i.test(text);\n let refScore = hasPlaceholderRefs ? 1 : Math.min(7, uniqueRefs);\n if (hasRealAuthors) refScore = Math.min(10, refScore + 1);\n if (hasDOI) refScore = Math.min(10, refScore + 1);\n sectionScores.references = refScore;\n\n const technicalTerms = (text.match(/\\b(algorithm|theorem|proof|complexity|O\\([^)]+\\)|convergence|optimal|novel|framework)\\b/gi) || []).length;\n const hasFigures = /figure \\d|fig\\.\\s*\\d|table \\d/i.test(text);\n let novelty = wordCount > 2000 ? 4 : wordCount > 1000 ? 3 : 2;\n novelty += Math.min(3, Math.floor(technicalTerms / 5));\n if (hasFigures) novelty += 1;\n\n const hasCode = /```[\\s\\S]*?```/.test(text);\n const hasEquations = /\\$[^$]+\\$/.test(text) || /\\\\begin\\{/.test(text);\n const hasNumbers = /\\d+\\.\\d+%|\\d+\\.\\d+x|p\\s*[<>]\\s*0\\.\\d/i.test(text);\n let reproducibility = (hasCode ? 5 : 3) + (hasEquations ? 1 : 0) + (wordCount > 2000 ? 1 : 0);\n if (hasNumbers) reproducibility += 1;\n\n const citation_quality = hasPlaceholderRefs ? 1 : Math.min(6, uniqueRefs);\n\n return {\n scores: {\n ...sectionScores,\n novelty: Math.min(10, novelty),\n reproducibility: Math.min(10, reproducibility),\n citation_quality: Math.min(10, citation_quality),\n },\n provider: \"heuristic\"\n };\n}\n\n/**\n * Score a paper using heterogeneous multi-LLM swarm.\n * Tries to get 2 independent judges, falls back through provider chain.\n */\nexport async function scoreGranular(content, paperType = \"research\") {\n if (!content || content.trim().length < 50) {\n return {\n sections: Object.fromEntries(SECTIONS.map(s => [s, 0])),\n overall: 0,\n novelty: 0,\n reproducibility: 0,\n citation_quality: 0,\n judges: [],\n judge_count: 0,\n error: \"Content too short to score\"\n };\n }\n\n // All LLM judges support 32k+ context. Send enough content to include ALL 7 mandatory sections.\n // Previous limit of 4000 chars truncated papers before methodology/results/conclusion, causing\n // strict judges (Cohere, Xiaomi) to correctly score missing sections as 0.\n const truncated = content.length > 16000 ? content.substring(0, 16000) + \"\\n\\n[... truncated for scoring ...]\" : content;\n const prompt = SCORING_PROMPT + truncated;\n\n // ALL available judges score independently for maximum consensus diversity.\n // Each model evaluates each section independently, then we average.\n // Run all judges in parallel for speed (each has its own timeout).\n const judgePromises = available.map(provider =>\n callLLMForScoring(prompt, provider).catch(() => null)\n );\n const judgeResults = await Promise.all(judgePromises);\n const judges = judgeResults.filter(Boolean);\n\n // If no LLM judges succeeded, use heuristic\n if (judges.length === 0) {\n const heuristic = heuristicScore(content);\n judges.push(heuristic);\n }\n\n // Average scores across all judges\n const allFields = [...SECTIONS, \"novelty\", \"reproducibility\", \"citation_quality\"];\n const averaged = {};\n for (const field of allFields) {\n const values = judges.map(j => j.scores[field]).filter(v => typeof v === \"number\");\n averaged[field] = values.length > 0\n ? Math.round((values.reduce((a, b) => a + b, 0) / values.length) * 10) / 10\n : 0;\n }\n\n // ── CALIBRATION PASS — adjust raw averages against reference benchmarks ──\n // This is the core fix for inflated scoring. Raw LLM averages are compared\n // against quality signals extracted from the paper content and calibrated\n // against recognized reference paper fingerprints.\n let calibration = null;\n try {\n const fieldResult = detectField(content);\n const signals = extractSignals(content);\n const benchmarks = REFERENCE_BENCHMARKS[fieldResult.field] || null;\n const { calibrated, adjustments } = calibrateScores(averaged, signals, benchmarks);\n\n // Apply calibrated scores over raw averages\n const adjustmentCount = Object.keys(adjustments).length;\n if (adjustmentCount > 0) {\n for (const [field, val] of Object.entries(calibrated)) {\n if (typeof val === \"number\" && averaged[field] !== undefined) {\n averaged[field] = val;\n }\n }\n console.log(`[SCORING] Calibration applied: ${adjustmentCount} adjustments, field=${fieldResult.field}, ` +\n `red_flags=${signals.red_flag_count}, depth=${signals.depth_score}`);\n }\n\n calibration = {\n field: fieldResult.field,\n field_confidence: fieldResult.confidence,\n signals_summary: {\n word_count: signals.word_count,\n sections_present: signals.sections_present.length,\n sections_missing: signals.sections_missing,\n red_flags: signals.red_flags,\n red_flag_count: signals.red_flag_count,\n has_formal_proofs: signals.has_formal_proofs,\n has_equations: signals.has_equations,\n has_code: signals.has_code,\n unique_refs: signals.unique_refs,\n has_placeholder_refs: signals.has_placeholder_refs,\n depth_score: signals.depth_score,\n evidence_markers: signals.evidence_markers,\n deception_count: signals.deception_count || 0,\n deception_matches: (signals.deception_matches || []).map(d => ({\n id: d.id, name: d.name, severity: d.severity,\n })),\n // New quality dimensions\n grammar: signals.grammar_quality ? {\n vocabulary_diversity: signals.grammar_quality.vocabulary_diversity_ttr,\n is_monotone: signals.grammar_quality.is_monotone,\n is_low_vocabulary: signals.grammar_quality.is_low_vocabulary,\n } : null,\n repetition_ratio: signals.repetition_score?.repetition_ratio || 0,\n code_quality: signals.code_quality?.blocks_found > 0 ? {\n blocks: signals.code_quality.blocks_found,\n has_real_code: signals.has_real_code || false,\n has_python: signals.code_quality.has_python || false,\n } : null,\n math_formulas: signals.math_quality?.formula_count || 0,\n lean4: signals.lean4_signals?.verification_level || \"none\",\n tables: signals.table_quality?.count || 0,\n },\n adjustments,\n adjustment_count: adjustmentCount,\n reference_papers: benchmarks ? benchmarks.references.map(r => r.title) : [],\n };\n } catch (calErr) {\n console.warn(`[SCORING] Calibration error (non-fatal): ${calErr.message}`);\n }\n\n // ── LIVE VERIFICATION PASS — real-time CrossRef, arXiv, code exec, Lean4 ──\n // Runs in parallel with independent timeouts. Non-fatal: if it fails, scoring\n // continues with just calibration. Results apply caps and bonuses to scores.\n let liveVerification = null;\n try {\n const verification = await runLiveVerification(content);\n const { adjustments: liveAdj, bonuses: liveBon } = verificationToAdjustments(verification);\n\n // ── OVERRIDE FALSE POSITIVES ──\n // If live verification confirmed code execution but calibration flagged\n // \"code_blocks_are_template_not_real\", the red_flag was a false positive.\n // Undo the -1.5 penalty applied to all dimensions by recalibrating.\n const codeExec = verification.code_execution;\n if (codeExec && codeExec.passed > 0 && calibration && calibration.signals_summary) {\n const redFlags = calibration.signals_summary.red_flags || [];\n if (redFlags.includes(\"code_blocks_are_template_not_real\")) {\n // The -1.5 penalty was applied to all 10 dimensions. Undo it.\n const allFields = [...SECTIONS, \"novelty\", \"reproducibility\", \"citation_quality\"];\n for (const field of allFields) {\n if (averaged[field] !== undefined) {\n averaged[field] = Math.min(10, Math.round((averaged[field] + 1.5) * 10) / 10);\n }\n }\n // Remove the false red flag from the report\n calibration.signals_summary.red_flags = redFlags.filter(f => f !== \"code_blocks_are_template_not_real\");\n calibration.signals_summary.red_flag_count = calibration.signals_summary.red_flags.length;\n calibration.false_positive_corrected = \"code_blocks_are_template_not_real (live verification confirmed code executes)\";\n console.log(`[SCORING] False positive corrected: code_blocks_are_template_not_real (live verification: ${codeExec.passed}/${codeExec.total} passed)`);\n }\n }\n\n // Apply caps (these override score to a max value)\n for (const [key, val] of Object.entries(liveAdj)) {\n if (key.endsWith(\"_cap\") && typeof val === \"number\") {\n const field = key.replace(\"_cap\", \"\");\n if (averaged[field] !== undefined && averaged[field] > val) {\n averaged[field] = val;\n }\n }\n }\n\n // Apply bonuses (add to score, capped at 10)\n for (const [key, val] of Object.entries(liveBon)) {\n if (key.endsWith(\"_bonus\") && typeof val === \"number\") {\n const field = key.replace(\"_bonus\", \"\");\n if (averaged[field] !== undefined) {\n averaged[field] = Math.min(10, Math.round((averaged[field] + val) * 10) / 10);\n }\n }\n }\n\n liveVerification = {\n verification_time_ms: verification.verification_time_ms,\n citations: verification.citations ? {\n total: verification.citations.total,\n verified: verification.citations.verified,\n verification_rate: verification.citations.verification_rate,\n } : null,\n novelty: verification.novelty ? {\n searched: verification.novelty.searched,\n total_found: verification.novelty.total_found,\n novelty_concern: verification.novelty.novelty_concern,\n max_similarity: verification.novelty.max_similarity,\n } : null,\n code_execution: verification.code_execution ? {\n total: verification.code_execution.total,\n passed: verification.code_execution.passed,\n failed: verification.code_execution.failed,\n } : null,\n lean4: verification.lean4 ? {\n blocks_found: verification.lean4.blocks_found,\n verified: verification.lean4.verified,\n has_unsubstantiated_claim: verification.lean4.has_unsubstantiated_claim || false,\n } : null,\n adjustments: liveAdj,\n bonuses: liveBon,\n };\n\n const adjCount = Object.keys(liveAdj).filter(k => k.endsWith(\"_cap\")).length;\n const bonCount = Object.keys(liveBon).filter(k => k.endsWith(\"_bonus\")).length;\n if (adjCount > 0 || bonCount > 0) {\n console.log(`[SCORING] Live verification: ${adjCount} caps, ${bonCount} bonuses applied (${verification.verification_time_ms}ms)`);\n }\n } catch (liveErr) {\n console.warn(`[SCORING] Live verification error (non-fatal): ${liveErr.message}`);\n }\n\n const sectionValues = SECTIONS.map(s => averaged[s]);\n const overall = Math.round((sectionValues.reduce((a, b) => a + b, 0) / SECTIONS.length) * 10) / 10;\n\n // Per-judge detail breakdown (individual scores + feedback)\n const judge_details = judges.map(j => ({\n judge: j.provider,\n scores: j.scores,\n feedback: j.feedback || null,\n }));\n\n // Consensus score per dimension (1 = all judges agree, 0 = total disagreement)\n const consensus = {};\n for (const field of allFields) {\n const values = judges.map(j => j.scores[field]).filter(v => typeof v === \"number\");\n if (values.length < 2) { consensus[field] = 1.0; continue; }\n const mean = values.reduce((a, b) => a + b, 0) / values.length;\n const variance = values.reduce((sum, v) => sum + (v - mean) ** 2, 0) / values.length;\n const stddev = Math.sqrt(variance);\n consensus[field] = Math.round(Math.max(0, 1 - stddev / 5) * 100) / 100;\n }\n const overall_consensus = Math.round(\n (allFields.reduce((sum, f) => sum + consensus[f], 0) / allFields.length) * 100\n ) / 100;\n\n // Aggregate feedback per dimension (from all judges that provided it)\n const aggregated_feedback = {};\n for (const field of allFields) {\n const comments = judges\n .filter(j => j.feedback && j.feedback[field])\n .map(j => ({ judge: j.provider, comment: j.feedback[field] }));\n if (comments.length > 0) aggregated_feedback[field] = comments;\n }\n\n const result = {\n sections: Object.fromEntries(SECTIONS.map(s => [s, averaged[s]])),\n overall,\n novelty: averaged.novelty,\n reproducibility: averaged.reproducibility,\n citation_quality: averaged.citation_quality,\n judges: judges.map(j => j.provider),\n judge_count: judges.length,\n judge_details,\n consensus,\n overall_consensus,\n feedback: Object.keys(aggregated_feedback).length > 0 ? aggregated_feedback : null,\n scored_at: new Date().toISOString(),\n paper_type: paperType,\n calibration,\n live_verification: liveVerification,\n };\n\n console.log(`[SCORING] Granular score: overall=${overall}, consensus=${overall_consensus}, judges=${result.judges.join(\",\")}`);\n return result;\n}\n"}, "ec8970b6e53ff01adc0845683bebc6b3b1a9f447": {"t": 1785753935.1020837, "n": 30071, "h": "3fac3a6aa4e094b1", "c": "/**\n * P2PCLAW Granular Scoring Service\n * =================================\n * Heterogeneous multi-LLM scoring engine that evaluates papers section-by-section.\n *\n * Provider chain (updated 2026-04-01):\n * 1. Cerebras — qwen-3-235b (3 keys, free, ultra-fast)\n * 2. Cerebras — llama3.1-8b (3 keys, free, ultra-fast)\n * 3. Mistral — mistral-small-latest (2 keys, free)\n * 4. Sarvam — sarvam-m (Indian AI, free)\n * 5. OpenRouter — qwen3-coder:free (free)\n * 6. Groq — llama-3.3-70b-versatile (7 keys, may be restricted)\n * 7. NVIDIA — meta/llama-3.3-70b-instruct (3 keys, free)\n * 8. Inception — mercury-2 (5 keys, free)\n * 9. Xiaomi MiMo — mimo-v2-flash (3 keys, free)\n * 10. Xiaomi MiMo — mimo-v2-pro (3 keys, free, reasoning)\n * 11. Cohere — command-a-reasoning (8 keys, reasoning model)\n * 12. Deterministic heuristic fallback (never blocks)\n *\n * ALL available judges score independently. Final score = average across all judges.\n * Each model evaluates each section independently for maximum consensus diversity.\n */\n\nimport { detectField, extractSignals, calibrateScores, REFERENCE_BENCHMARKS } from \"./calibrationService.js\";\nimport { runLiveVerification, verificationToAdjustments } from \"./liveVerificationService.js\";\n\nconst SECTIONS = [\"abstract\", \"introduction\", \"methodology\", \"results\", \"discussion\", \"conclusion\", \"references\"];\n\n// ── Load keys with rotation ─────────────────────────────────────────────────\n\nfunction loadKeys(envPrefix, maxKeys = 10) {\n const keys = [];\n for (let i = 1; i <= maxKeys; i++) {\n const k = process.env[`${envPrefix}_${i}`] || process.env[`${envPrefix}${i}`];\n if (k) keys.push(k);\n }\n // Also check single-key env vars\n const single = process.env[envPrefix];\n if (single && !keys.includes(single)) keys.unshift(single);\n return keys;\n}\n\nconst keyIndices = {};\nfunction nextKey(providerId, keys) {\n if (!keys.length) return null;\n const idx = keyIndices[providerId] || 0;\n const key = keys[idx % keys.length];\n keyIndices[providerId] = (idx + 1) % keys.length;\n return key;\n}\n\n// ── Provider definitions ────────────────────────────────────────────────────\n\nconst PROVIDERS = [\n // --- Cerebras: 3 keys x 2 models = up to 6 independent judges ---\n {\n id: \"cerebras-qwen\",\n name: \"Cerebras-Qwen235B\",\n url: \"https://api.cerebras.ai/v1/chat/completions\",\n model: \"qwen-3-235b-a22b-instruct-2507\",\n keys: loadKeys(\"CEREBRAS_API_KEY\").concat(loadKeys(\"CEREBRAS_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 1024,\n },\n {\n id: \"cerebras-llama\",\n name: \"Cerebras-Llama8B\",\n url: \"https://api.cerebras.ai/v1/chat/completions\",\n model: \"llama3.1-8b\",\n keys: loadKeys(\"CEREBRAS_API_KEY\").concat(loadKeys(\"CEREBRAS_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n // --- Mistral: 2 keys, reliable ---\n {\n id: \"mistral\",\n name: \"Mistral\",\n url: \"https://api.mistral.ai/v1/chat/completions\",\n model: \"mistral-small-latest\",\n keys: loadKeys(\"MISTRAL_API_KEY\").concat(loadKeys(\"MISTRAL_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n // --- Sarvam (Indian AI): sarvam-m, uses <think> tags (needs 2048+ tokens) ---\n {\n id: \"sarvam\",\n name: \"Sarvam\",\n url: \"https://api.sarvam.ai/v1/chat/completions\",\n model: \"sarvam-m\",\n keys: loadKeys(\"SARVAM_KEY\").concat(loadKeys(\"SARVAM_API_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 2048,\n },\n // --- OpenRouter: free models ---\n {\n id: \"openrouter\",\n name: \"OpenRouter\",\n url: \"https://openrouter.ai/api/v1/chat/completions\",\n model: \"qwen/qwen3-coder:free\",\n keys: loadKeys(\"OPENROUTER_API_KEY\"),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n extraHeaders: { \"HTTP-Referer\": \"https://www.p2pclaw.com\", \"X-Title\": \"P2PCLAW Scoring\" },\n },\n // --- Groq: 7 keys but may be org-restricted ---\n {\n id: \"groq\",\n name: \"Groq\",\n url: \"https://api.groq.com/openai/v1/chat/completions\",\n model: \"llama-3.3-70b-versatile\",\n keys: loadKeys(\"GROQ_API_KEY\").concat(loadKeys(\"GROQ_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n // --- NVIDIA: 3 keys ---\n {\n id: \"nvidia\",\n name: \"NVIDIA\",\n url: \"https://integrate.api.nvidia.com/v1/chat/completions\",\n model: \"meta/llama-3.3-70b-instruct\",\n keys: loadKeys(\"NVAPI_KEY\").concat(loadKeys(\"NVIDIA_API_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n // --- Inception: mercury-2 ---\n {\n id: \"inception\",\n name: \"Inception\",\n url: \"https://api.inceptionlabs.ai/v1/chat/completions\",\n model: \"mercury-2\",\n keys: loadKeys(\"INCEPTION_API_KEY\").concat(loadKeys(\"INCEPTION_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n // --- Xiaomi MiMo: 3 keys x 2 models = up to 2 independent judges ---\n {\n id: \"xiaomi-flash\",\n name: \"Xiaomi-MiMo-Flash\",\n url: \"https://api.xiaomimimo.com/v1/chat/completions\",\n model: \"mimo-v2-flash\",\n keys: loadKeys(\"XIAOMI_API_KEY\").concat(loadKeys(\"XIAOMI_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n {\n id: \"xiaomi-pro\",\n name: \"Xiaomi-MiMo-Pro\",\n url: \"https://api.xiaomimimo.com/v1/chat/completions\",\n model: \"mimo-v2-pro\",\n keys: loadKeys(\"XIAOMI_API_KEY\").concat(loadKeys(\"XIAOMI_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 1024,\n },\n // --- Cohere: command-a-reasoning model (8 keys, reasoning/thinking model) ---\n {\n id: \"cohere\",\n name: \"Cohere-CommandA\",\n url: \"https://api.cohere.com/v2/chat\",\n model: \"command-a-reasoning-08-2025\",\n keys: loadKeys(\"COHERE_API_KEY\").concat(loadKeys(\"COHERE_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 4096, // reasoning model needs extra tokens for thinking + answer\n responseFormat: \"cohere\", // data.message.content[] array with {type:\"thinking\"} + {type:\"text\"}\n timeout: 90000, // 90s — reasoning model needs time to think through all 10 dimensions\n },\n];\n\n// Deduplicate keys within each provider\nfor (const p of PROVIDERS) {\n p.keys = [...new Set(p.keys)].filter(Boolean);\n}\n\n// Log available providers\nconst available = PROVIDERS.filter(p => p.keys.length > 0);\nconsole.log(`[SCORING] ${available.length} LLM providers available: ${available.map(p => `${p.name}(${p.keys.length})`).join(\", \")}`);\nif (available.length === 0) console.warn(\"[SCORING] No LLM providers — heuristic scoring only.\");\n// Debug: log specifically which providers have NO keys\nconst unavailable = PROVIDERS.filter(p => p.keys.length === 0);\nif (unavailable.length > 0) console.log(`[SCORING] Providers with NO keys: ${unavailable.map(p => p.name).join(\", \")}`);\n\nconst SCORING_PROMPT = `You are an academic paper quality evaluator for the P2PCLAW decentralized research network.\n\nScore this paper on a 0-10 scale for each criterion. For EACH score, include a brief \"why\" (1 sentence explaining the score).\n\nScoring criteria:\n- abstract (0-10): Quality, clarity, and completeness of the abstract. Clear problem statement, scope, and key results summarized.\n- introduction (0-10): Problem statement clarity, context, motivation. Should cite 2+ related works.\n- methodology (0-10): Rigor, reproducibility, appropriate methods. Must describe steps clearly enough to reproduce.\n- results (0-10): Quality of findings, data presentation, significance. Quantitative data strongly preferred.\n- discussion (0-10): Interpretation depth, limitations acknowledged, implications for the field.\n- conclusion (0-10): Summary of key findings (expected, NOT penalized as repetition), concrete future work directions, and connection back to the research question. A good conclusion SHOULD summarize findings — that is standard scientific practice.\n- references (0-10): Citation quality, relevance, proper formatting (0 if no real references). Real author names, titles, years, DOIs/URLs expected.\n- novelty (0-10): Original contribution to the field. Novel frameworks, terminology, or approaches.\n- reproducibility (0-10): Could another researcher reproduce this work? Code, equations, detailed parameters help.\n- citation_quality (0-10): Are references real, relevant, and properly cited? 8+ unique real citations expected for high scores.\n\nIMPORTANT RULES:\n- If a section is MISSING entirely, score it 0\n- If references are placeholder/fake (e.g. \"[1] Author, A. (2026). Placeholder\"), score references as 1\n- Trivial proofs or papers under 300 words should score below 3 overall\n- Papers that prove obvious things (like 0*0=0) should score novelty as 0-1\n- Be STRICT — a score of 8+ means genuinely publishable quality\n- Lean 4 verified papers with valid proof hashes deserve reproducibility bonus\n\nReturn ONLY this JSON format (numbers 0-10, nothing else):\n{\"abstract\":N,\"introduction\":N,\"methodology\":N,\"results\":N,\"discussion\":N,\"conclusion\":N,\"references\":N,\"novelty\":N,\"reproducibility\":N,\"citation_quality\":N}\n\nPaper content:\n`;\n\n/**\n * Call a single LLM provider to score a paper.\n * Tries up to 2 keys from the provider before giving up.\n */\nasync function callLLMForScoring(prompt, provider) {\n if (!provider.keys.length) return null;\n\n const maxAttempts = Math.min(provider.keys.length, 2);\n for (let attempt = 0; attempt < maxAttempts; attempt++) {\n const key = nextKey(provider.id, provider.keys);\n if (!key) return null;\n\n const headers = { \"Content-Type\": \"application/json\" };\n headers[provider.authHeader] = `${provider.authPrefix}${key}`;\n if (provider.extraHeaders) Object.assign(headers, provider.extraHeaders);\n\n try {\n const res = await fetch(provider.url, {\n method: \"POST\",\n headers,\n body: JSON.stringify({\n model: provider.model,\n messages: [{ role: \"user\", content: prompt }],\n max_tokens: provider.maxTokens || 512,\n temperature: 0.1,\n }),\n signal: AbortSignal.timeout(provider.timeout || 30000),\n });\n\n if (res.status === 429) {\n console.warn(`[SCORING] ${provider.name} 429 rate-limited`);\n await new Promise(r => setTimeout(r, 3000));\n continue;\n }\n if (res.status === 402) {\n console.warn(`[SCORING] ${provider.name} 402 credits exhausted — skipping provider`);\n return null;\n }\n if (!res.ok) {\n const errBody = await res.text().catch(() => \"\");\n console.warn(`[SCORING] ${provider.name} HTTP ${res.status}: ${errBody.substring(0, 150)}`);\n continue;\n }\n\n const data = await res.json();\n // Support both OpenAI format (choices[0].message.content) and Cohere v2 (message.content[] array)\n let text = \"\";\n if (provider.responseFormat === \"cohere\") {\n // Cohere v2 returns array of content blocks: [{type:\"thinking\",...}, {type:\"text\",text:\"...\"}]\n const blocks = data.message?.content || [];\n if (Array.isArray(blocks)) {\n const textBlock = blocks.find(b => b.type === \"text\");\n text = textBlock?.text || blocks[blocks.length - 1]?.text || \"\";\n } else {\n text = typeof blocks === \"string\" ? blocks : \"\";\n }\n } else {\n text = data.choices?.[0]?.message?.content || \"\";\n }\n\n // Strip <think>...</think> tags (Sarvam, Qwen with thinking mode)\n if (provider.stripThinkTags || text.includes(\"<think>\")) {\n // Strip closed think blocks\n text = text.replace(/<think>[\\s\\S]*?<\\/think>/g, \"\").trim();\n // Strip unclosed think blocks (model ran out of tokens mid-think)\n if (text.startsWith(\"<think>\")) {\n text = text.replace(/<think>[\\s\\S]*/g, \"\").trim();\n }\n }\n\n // Extract JSON — use balanced braces to handle nested objects like {\"score\":8,\"why\":\"...\"}\n let jsonMatch = null;\n const firstBrace = text.indexOf('{');\n if (firstBrace !== -1) {\n let depth = 0, end = -1;\n for (let i = firstBrace; i < text.length; i++) {\n if (text[i] === '{') depth++;\n else if (text[i] === '}') { depth--; if (depth === 0) { end = i; break; } }\n }\n if (end !== -1) jsonMatch = [text.substring(firstBrace, end + 1)];\n }\n if (!jsonMatch) {\n console.warn(`[SCORING] ${provider.name} — no JSON in response`);\n continue;\n }\n\n const parsed = JSON.parse(jsonMatch[0]);\n const fields = [...SECTIONS, \"novelty\", \"reproducibility\", \"citation_quality\"];\n const feedback = {};\n for (const field of fields) {\n let val = parsed[field];\n // Support both plain number and {score, why} object format\n if (val && typeof val === \"object\" && typeof val.score === \"number\") {\n feedback[field] = val.why || null;\n val = val.score;\n }\n if (typeof val !== \"number\" || val < 0 || val > 10) {\n val = typeof val === \"number\" ? Math.max(0, Math.min(10, Math.round(val))) : 5;\n }\n parsed[field] = val;\n }\n\n // Clean parsed: only keep recognized score fields (remove stray \"why\" etc.)\n const cleanScores = {};\n for (const field of fields) cleanScores[field] = parsed[field];\n\n console.log(`[SCORING] ${provider.name} scored successfully`);\n return { scores: cleanScores, provider: provider.name, feedback: Object.keys(feedback).length > 0 ? feedback : null };\n } catch (e) {\n console.warn(`[SCORING] ${provider.name} error: ${e.message}`);\n }\n }\n return null;\n}\n\n/**\n * Deterministic heuristic scoring — used when all LLMs fail.\n */\nfunction heuristicScore(content) {\n const text = content || \"\";\n const words = text.split(/\\s+/).filter(w => w.length > 0);\n const wordCount = words.length;\n const lowerText = text.toLowerCase();\n\n const sectionScores = {};\n for (const section of SECTIONS) {\n const hasSection = lowerText.includes(`## ${section}`) || lowerText.includes(`# ${section}`);\n if (!hasSection) {\n sectionScores[section] = 0;\n continue;\n }\n const sectionRegex = new RegExp(`##?\\\\s*${section}[\\\\s\\\\S]*?(?=##?\\\\s|$)`, \"i\");\n const match = text.match(sectionRegex);\n const sectionWords = match ? match[0].split(/\\s+/).length : 0;\n if (sectionWords < 20) sectionScores[section] = 2;\n else if (sectionWords < 50) sectionScores[section] = 4;\n else if (sectionWords < 100) sectionScores[section] = 5;\n else if (sectionWords < 200) sectionScores[section] = 6;\n else sectionScores[section] = 7;\n }\n\n const refMatches = text.match(/\\[\\d+\\]/g) || [];\n const uniqueRefs = new Set(refMatches).size;\n const hasPlaceholderRefs = /placeholder|author,?\\s*a\\.\\s*\\(\\d{4}\\)/i.test(text);\n const hasRealAuthors = /[A-Z][a-z]+,\\s*[A-Z]\\.\\s*(?:&|,|et al)/g.test(text);\n const hasDOI = /doi\\.org|arxiv\\.org|10\\.\\d{4}/i.test(text);\n let refScore = hasPlaceholderRefs ? 1 : Math.min(7, uniqueRefs);\n if (hasRealAuthors) refScore = Math.min(10, refScore + 1);\n if (hasDOI) refScore = Math.min(10, refScore + 1);\n sectionScores.references = refScore;\n\n const technicalTerms = (text.match(/\\b(algorithm|theorem|proof|complexity|O\\([^)]+\\)|convergence|optimal|novel|framework)\\b/gi) || []).length;\n const hasFigures = /figure \\d|fig\\.\\s*\\d|table \\d/i.test(text);\n let novelty = wordCount > 2000 ? 4 : wordCount > 1000 ? 3 : 2;\n novelty += Math.min(3, Math.floor(technicalTerms / 5));\n if (hasFigures) novelty += 1;\n\n const hasCode = /```[\\s\\S]*?```/.test(text);\n const hasEquations = /\\$[^$]+\\$/.test(text) || /\\\\begin\\{/.test(text);\n const hasNumbers = /\\d+\\.\\d+%|\\d+\\.\\d+x|p\\s*[<>]\\s*0\\.\\d/i.test(text);\n let reproducibility = (hasCode ? 5 : 3) + (hasEquations ? 1 : 0) + (wordCount > 2000 ? 1 : 0);\n if (hasNumbers) reproducibility += 1;\n\n const citation_quality = hasPlaceholderRefs ? 1 : Math.min(6, uniqueRefs);\n\n return {\n scores: {\n ...sectionScores,\n novelty: Math.min(10, novelty),\n reproducibility: Math.min(10, reproducibility),\n citation_quality: Math.min(10, citation_quality),\n },\n provider: \"heuristic\"\n };\n}\n\n/**\n * Score a paper using heterogeneous multi-LLM swarm.\n * Tries to get 2 independent judges, falls back through provider chain.\n */\nexport async function scoreGranular(content, paperType = \"research\") {\n if (!content || content.trim().length < 50) {\n return {\n sections: Object.fromEntries(SECTIONS.map(s => [s, 0])),\n overall: 0,\n novelty: 0,\n reproducibility: 0,\n citation_quality: 0,\n judges: [],\n judge_count: 0,\n error: \"Content too short to score\"\n };\n }\n\n // All LLM judges support 32k+ context. Send enough content to include ALL 7 mandatory sections.\n // Previous limit of 4000 chars truncated papers before methodology/results/conclusion, causing\n // strict judges (Cohere, Xiaomi) to correctly score missing sections as 0.\n const truncated = content.length > 16000 ? content.substring(0, 16000) + \"\\n\\n[... truncated for scoring ...]\" : content;\n const prompt = SCORING_PROMPT + truncated;\n\n // ALL available judges score independently for maximum consensus diversity.\n // Each model evaluates each section independently, then we average.\n // Run all judges in parallel for speed (each has its own timeout).\n const judgePromises = available.map(provider =>\n callLLMForScoring(prompt, provider).catch(() => null)\n );\n const judgeResults = await Promise.all(judgePromises);\n const judges = judgeResults.filter(Boolean);\n\n // If no LLM judges succeeded, use heuristic\n if (judges.length === 0) {\n const heuristic = heuristicScore(content);\n judges.push(heuristic);\n }\n\n // Average scores across all judges\n const allFields = [...SECTIONS, \"novelty\", \"reproducibility\", \"citation_quality\"];\n const averaged = {};\n for (const field of allFields) {\n const values = judges.map(j => j.scores[field]).filter(v => typeof v === \"number\");\n averaged[field] = values.length > 0\n ? Math.round((values.reduce((a, b) => a + b, 0) / values.length) * 10) / 10\n : 0;\n }\n\n // ── CALIBRATION PASS — adjust raw averages against reference benchmarks ──\n // This is the core fix for inflated scoring. Raw LLM averages are compared\n // against quality signals extracted from the paper content and calibrated\n // against recognized reference paper fingerprints.\n let calibration = null;\n try {\n const fieldResult = detectField(content);\n const signals = extractSignals(content);\n const benchmarks = REFERENCE_BENCHMARKS[fieldResult.field] || null;\n const { calibrated, adjustments } = calibrateScores(averaged, signals, benchmarks);\n\n // Apply calibrated scores over raw averages\n const adjustmentCount = Object.keys(adjustments).length;\n if (adjustmentCount > 0) {\n for (const [field, val] of Object.entries(calibrated)) {\n if (typeof val === \"number\" && averaged[field] !== undefined) {\n averaged[field] = val;\n }\n }\n console.log(`[SCORING] Calibration applied: ${adjustmentCount} adjustments, field=${fieldResult.field}, ` +\n `red_flags=${signals.red_flag_count}, depth=${signals.depth_score}`);\n }\n\n calibration = {\n field: fieldResult.field,\n field_confidence: fieldResult.confidence,\n signals_summary: {\n word_count: signals.word_count,\n sections_present: signals.sections_present.length,\n sections_missing: signals.sections_missing,\n red_flags: signals.red_flags,\n red_flag_count: signals.red_flag_count,\n has_formal_proofs: signals.has_formal_proofs,\n has_equations: signals.has_equations,\n has_code: signals.has_code,\n unique_refs: signals.unique_refs,\n has_placeholder_refs: signals.has_placeholder_refs,\n depth_score: signals.depth_score,\n evidence_markers: signals.evidence_markers,\n deception_count: signals.deception_count || 0,\n deception_matches: (signals.deception_matches || []).map(d => ({\n id: d.id, name: d.name, severity: d.severity,\n })),\n // New quality dimensions\n grammar: signals.grammar_quality ? {\n vocabulary_diversity: signals.grammar_quality.vocabulary_diversity_ttr,\n is_monotone: signals.grammar_quality.is_monotone,\n is_low_vocabulary: signals.grammar_quality.is_low_vocabulary,\n } : null,\n repetition_ratio: signals.repetition_score?.repetition_ratio || 0,\n code_quality: signals.code_quality?.blocks_found > 0 ? {\n blocks: signals.code_quality.blocks_found,\n has_real_code: signals.has_real_code || false,\n has_python: signals.code_quality.has_python || false,\n } : null,\n math_formulas: signals.math_quality?.formula_count || 0,\n lean4: signals.lean4_signals?.verification_level || \"none\",\n tables: signals.table_quality?.count || 0,\n },\n adjustments,\n adjustment_count: adjustmentCount,\n reference_papers: benchmarks ? benchmarks.references.map(r => r.title) : [],\n };\n } catch (calErr) {\n console.warn(`[SCORING] Calibration error (non-fatal): ${calErr.message}`);\n }\n\n // ── LIVE VERIFICATION PASS — real-time CrossRef, arXiv, code exec, Lean4 ──\n // Runs in parallel with independent timeouts. Non-fatal: if it fails, scoring\n // continues with just calibration. Results apply caps and bonuses to scores.\n let liveVerification = null;\n try {\n const verification = await runLiveVerification(content);\n const { adjustments: liveAdj, bonuses: liveBon } = verificationToAdjustments(verification);\n\n // ── OVERRIDE FALSE POSITIVES ──\n // If live verification confirmed code execution but calibration flagged\n // \"code_blocks_are_template_not_real\", the red_flag was a false positive.\n // Undo the -1.5 penalty applied to all dimensions by recalibrating.\n const codeExec = verification.code_execution;\n if (codeExec && codeExec.passed > 0 && calibration && calibration.signals_summary) {\n const redFlags = calibration.signals_summary.red_flags || [];\n if (redFlags.includes(\"code_blocks_are_template_not_real\")) {\n // The -1.5 penalty was applied to all 10 dimensions. Undo it.\n const allFields = [...SECTIONS, \"novelty\", \"reproducibility\", \"citation_quality\"];\n for (const field of allFields) {\n if (averaged[field] !== undefined) {\n averaged[field] = Math.min(10, Math.round((averaged[field] + 1.5) * 10) / 10);\n }\n }\n // Remove the false red flag from the report\n calibration.signals_summary.red_flags = redFlags.filter(f => f !== \"code_blocks_are_template_not_real\");\n calibration.signals_summary.red_flag_count = calibration.signals_summary.red_flags.length;\n calibration.false_positive_corrected = \"code_blocks_are_template_not_real (live verification confirmed code executes)\";\n console.log(`[SCORING] False positive corrected: code_blocks_are_template_not_real (live verification: ${codeExec.passed}/${codeExec.total} passed)`);\n }\n }\n\n // Apply caps (these override score to a max value)\n for (const [key, val] of Object.entries(liveAdj)) {\n if (key.endsWith(\"_cap\") && typeof val === \"number\") {\n const field = key.replace(\"_cap\", \"\");\n if (averaged[field] !== undefined && averaged[field] > val) {\n averaged[field] = val;\n }\n }\n }\n\n // Apply bonuses (add to score, capped at 10)\n for (const [key, val] of Object.entries(liveBon)) {\n if (key.endsWith(\"_bonus\") && typeof val === \"number\") {\n const field = key.replace(\"_bonus\", \"\");\n if (averaged[field] !== undefined) {\n averaged[field] = Math.min(10, Math.round((averaged[field] + val) * 10) / 10);\n }\n }\n }\n\n liveVerification = {\n verification_time_ms: verification.verification_time_ms,\n citations: verification.citations ? {\n total: verification.citations.total,\n verified: verification.citations.verified,\n verification_rate: verification.citations.verification_rate,\n } : null,\n novelty: verification.novelty ? {\n searched: verification.novelty.searched,\n total_found: verification.novelty.total_found,\n novelty_concern: verification.novelty.novelty_concern,\n max_similarity: verification.novelty.max_similarity,\n } : null,\n code_execution: verification.code_execution ? {\n total: verification.code_execution.total,\n passed: verification.code_execution.passed,\n failed: verification.code_execution.failed,\n } : null,\n lean4: verification.lean4 ? {\n blocks_found: verification.lean4.blocks_found,\n verified: verification.lean4.verified,\n has_unsubstantiated_claim: verification.lean4.has_unsubstantiated_claim || false,\n } : null,\n adjustments: liveAdj,\n bonuses: liveBon,\n };\n\n const adjCount = Object.keys(liveAdj).filter(k => k.endsWith(\"_cap\")).length;\n const bonCount = Object.keys(liveBon).filter(k => k.endsWith(\"_bonus\")).length;\n if (adjCount > 0 || bonCount > 0) {\n console.log(`[SCORING] Live verification: ${adjCount} caps, ${bonCount} bonuses applied (${verification.verification_time_ms}ms)`);\n }\n } catch (liveErr) {\n console.warn(`[SCORING] Live verification error (non-fatal): ${liveErr.message}`);\n }\n\n const sectionValues = SECTIONS.map(s => averaged[s]);\n const overall = Math.round((sectionValues.reduce((a, b) => a + b, 0) / SECTIONS.length) * 10) / 10;\n\n // Per-judge detail breakdown (individual scores + feedback)\n const judge_details = judges.map(j => ({\n judge: j.provider,\n scores: j.scores,\n feedback: j.feedback || null,\n }));\n\n // Consensus score per dimension (1 = all judges agree, 0 = total disagreement)\n const consensus = {};\n for (const field of allFields) {\n const values = judges.map(j => j.scores[field]).filter(v => typeof v === \"number\");\n if (values.length < 2) { consensus[field] = 1.0; continue; }\n const mean = values.reduce((a, b) => a + b, 0) / values.length;\n const variance = values.reduce((sum, v) => sum + (v - mean) ** 2, 0) / values.length;\n const stddev = Math.sqrt(variance);\n consensus[field] = Math.round(Math.max(0, 1 - stddev / 5) * 100) / 100;\n }\n const overall_consensus = Math.round(\n (allFields.reduce((sum, f) => sum + consensus[f], 0) / allFields.length) * 100\n ) / 100;\n\n // Aggregate feedback per dimension (from all judges that provided it)\n const aggregated_feedback = {};\n for (const field of allFields) {\n const comments = judges\n .filter(j => j.feedback && j.feedback[field])\n .map(j => ({ judge: j.provider, comment: j.feedback[field] }));\n if (comments.length > 0) aggregated_feedback[field] = comments;\n }\n\n const result = {\n sections: Object.fromEntries(SECTIONS.map(s => [s, averaged[s]])),\n overall,\n novelty: averaged.novelty,\n reproducibility: averaged.reproducibility,\n citation_quality: averaged.citation_quality,\n judges: judges.map(j => j.provider),\n judge_count: judges.length,\n judge_details,\n consensus,\n overall_consensus,\n feedback: Object.keys(aggregated_feedback).length > 0 ? aggregated_feedback : null,\n scored_at: new Date().toISOString(),\n paper_type: paperType,\n calibration,\n live_verification: liveVerification,\n };\n\n console.log(`[SCORING] Granular score: overall=${overall}, consensus=${overall_consensus}, judges=${result.judges.join(\",\")}`);\n return result;\n}\n"}, "4e7da8091611acc908019c77b5cd80c756c3327c": {"t": 1785753937.3610854, "n": 28449, "h": "24f8cc3f249ebba9", "c": "/**\n * P2PCLAW Granular Scoring Service\n * =================================\n * Heterogeneous multi-LLM scoring engine that evaluates papers section-by-section.\n *\n * Provider chain (updated 2026-04-01):\n * 1. Cerebras — qwen-3-235b (3 keys, free, ultra-fast)\n * 2. Cerebras — llama3.1-8b (3 keys, free, ultra-fast)\n * 3. Mistral — mistral-small-latest (2 keys, free)\n * 4. Sarvam — sarvam-m (Indian AI, free)\n * 5. OpenRouter — qwen3-coder:free (free)\n * 6. Groq — llama-3.3-70b-versatile (7 keys, may be restricted)\n * 7. NVIDIA — meta/llama-3.3-70b-instruct (3 keys, free)\n * 8. Inception — mercury-2 (5 keys, free)\n * 9. Xiaomi MiMo — mimo-v2-flash (3 keys, free)\n * 10. Xiaomi MiMo — mimo-v2-pro (3 keys, free, reasoning)\n * 11. Cohere — command-a-reasoning (8 keys, reasoning model)\n * 12. Deterministic heuristic fallback (never blocks)\n *\n * ALL available judges score independently. Final score = average across all judges.\n * Each model evaluates each section independently for maximum consensus diversity.\n */\n\nimport { detectField, extractSignals, calibrateScores, REFERENCE_BENCHMARKS } from \"./calibrationService.js\";\nimport { runLiveVerification, verificationToAdjustments } from \"./liveVerificationService.js\";\n\nconst SECTIONS = [\"abstract\", \"introduction\", \"methodology\", \"results\", \"discussion\", \"conclusion\", \"references\"];\n\n// ── Load keys with rotation ─────────────────────────────────────────────────\n\nfunction loadKeys(envPrefix, maxKeys = 10) {\n const keys = [];\n for (let i = 1; i <= maxKeys; i++) {\n const k = process.env[`${envPrefix}_${i}`] || process.env[`${envPrefix}${i}`];\n if (k) keys.push(k);\n }\n // Also check single-key env vars\n const single = process.env[envPrefix];\n if (single && !keys.includes(single)) keys.unshift(single);\n return keys;\n}\n\nconst keyIndices = {};\nfunction nextKey(providerId, keys) {\n if (!keys.length) return null;\n const idx = keyIndices[providerId] || 0;\n const key = keys[idx % keys.length];\n keyIndices[providerId] = (idx + 1) % keys.length;\n return key;\n}\n\n// ── Provider definitions ────────────────────────────────────────────────────\n\nconst PROVIDERS = [\n // --- Cerebras: 3 keys x 2 models = up to 6 independent judges ---\n {\n id: \"cerebras-qwen\",\n name: \"Cerebras-Qwen235B\",\n url: \"https://api.cerebras.ai/v1/chat/completions\",\n model: \"qwen-3-235b-a22b-instruct-2507\",\n keys: loadKeys(\"CEREBRAS_API_KEY\").concat(loadKeys(\"CEREBRAS_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 1024,\n },\n {\n id: \"cerebras-llama\",\n name: \"Cerebras-Llama8B\",\n url: \"https://api.cerebras.ai/v1/chat/completions\",\n model: \"llama3.1-8b\",\n keys: loadKeys(\"CEREBRAS_API_KEY\").concat(loadKeys(\"CEREBRAS_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n // --- Mistral: 2 keys, reliable ---\n {\n id: \"mistral\",\n name: \"Mistral\",\n url: \"https://api.mistral.ai/v1/chat/completions\",\n model: \"mistral-small-latest\",\n keys: loadKeys(\"MISTRAL_API_KEY\").concat(loadKeys(\"MISTRAL_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n // --- Sarvam (Indian AI): sarvam-m, uses <think> tags (needs 2048+ tokens) ---\n {\n id: \"sarvam\",\n name: \"Sarvam\",\n url: \"https://api.sarvam.ai/v1/chat/completions\",\n model: \"sarvam-m\",\n keys: loadKeys(\"SARVAM_KEY\").concat(loadKeys(\"SARVAM_API_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 2048,\n },\n // --- OpenRouter: free models ---\n {\n id: \"openrouter\",\n name: \"OpenRouter\",\n url: \"https://openrouter.ai/api/v1/chat/completions\",\n model: \"qwen/qwen3-coder:free\",\n keys: loadKeys(\"OPENROUTER_API_KEY\"),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n extraHeaders: { \"HTTP-Referer\": \"https://www.p2pclaw.com\", \"X-Title\": \"P2PCLAW Scoring\" },\n },\n // --- Groq: 7 keys but may be org-restricted ---\n {\n id: \"groq\",\n name: \"Groq\",\n url: \"https://api.groq.com/openai/v1/chat/completions\",\n model: \"llama-3.3-70b-versatile\",\n keys: loadKeys(\"GROQ_API_KEY\").concat(loadKeys(\"GROQ_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n // --- NVIDIA: 3 keys ---\n {\n id: \"nvidia\",\n name: \"NVIDIA\",\n url: \"https://integrate.api.nvidia.com/v1/chat/completions\",\n model: \"meta/llama-3.3-70b-instruct\",\n keys: loadKeys(\"NVAPI_KEY\").concat(loadKeys(\"NVIDIA_API_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n // --- Inception: mercury-2 ---\n {\n id: \"inception\",\n name: \"Inception\",\n url: \"https://api.inceptionlabs.ai/v1/chat/completions\",\n model: \"mercury-2\",\n keys: loadKeys(\"INCEPTION_API_KEY\").concat(loadKeys(\"INCEPTION_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n // --- Xiaomi MiMo: 3 keys x 2 models = up to 2 independent judges ---\n {\n id: \"xiaomi-flash\",\n name: \"Xiaomi-MiMo-Flash\",\n url: \"https://api.xiaomimimo.com/v1/chat/completions\",\n model: \"mimo-v2-flash\",\n keys: loadKeys(\"XIAOMI_API_KEY\").concat(loadKeys(\"XIAOMI_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n {\n id: \"xiaomi-pro\",\n name: \"Xiaomi-MiMo-Pro\",\n url: \"https://api.xiaomimimo.com/v1/chat/completions\",\n model: \"mimo-v2-pro\",\n keys: loadKeys(\"XIAOMI_API_KEY\").concat(loadKeys(\"XIAOMI_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 1024,\n },\n // --- Cohere: command-a-reasoning model (8 keys, reasoning/thinking model) ---\n {\n id: \"cohere\",\n name: \"Cohere-CommandA\",\n url: \"https://api.cohere.com/v2/chat\",\n model: \"command-a-reasoning-08-2025\",\n keys: loadKeys(\"COHERE_API_KEY\").concat(loadKeys(\"COHERE_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 4096, // reasoning model needs extra tokens for thinking + answer\n responseFormat: \"cohere\", // data.message.content[] array with {type:\"thinking\"} + {type:\"text\"}\n timeout: 90000, // 90s — reasoning model needs time to think through all 10 dimensions\n },\n];\n\n// Deduplicate keys within each provider\nfor (const p of PROVIDERS) {\n p.keys = [...new Set(p.keys)].filter(Boolean);\n}\n\n// Log available providers\nconst available = PROVIDERS.filter(p => p.keys.length > 0);\nconsole.log(`[SCORING] ${available.length} LLM providers available: ${available.map(p => `${p.name}(${p.keys.length})`).join(\", \")}`);\nif (available.length === 0) console.warn(\"[SCORING] No LLM providers — heuristic scoring only.\");\n// Debug: log specifically which providers have NO keys\nconst unavailable = PROVIDERS.filter(p => p.keys.length === 0);\nif (unavailable.length > 0) console.log(`[SCORING] Providers with NO keys: ${unavailable.map(p => p.name).join(\", \")}`);\n\nconst SCORING_PROMPT = `You are an academic paper quality evaluator for the P2PCLAW decentralized research network.\n\nScore this paper on a 0-10 scale for each criterion. For EACH score, include a brief \"why\" (1 sentence explaining the score).\n\nScoring criteria:\n- abstract (0-10): Quality, clarity, and completeness of the abstract. Clear problem statement, scope, and key results summarized.\n- introduction (0-10): Problem statement clarity, context, motivation. Should cite 2+ related works.\n- methodology (0-10): Rigor, reproducibility, appropriate methods. Must describe steps clearly enough to reproduce.\n- results (0-10): Quality of findings, data presentation, significance. Quantitative data strongly preferred.\n- discussion (0-10): Interpretation depth, limitations acknowledged, implications for the field.\n- conclusion (0-10): Summary of key findings (expected, NOT penalized as repetition), concrete future work directions, and connection back to the research question. A good conclusion SHOULD summarize findings — that is standard scientific practice.\n- references (0-10): Citation quality, relevance, proper formatting (0 if no real references). Real author names, titles, years, DOIs/URLs expected.\n- novelty (0-10): Original contribution to the field. Novel frameworks, terminology, or approaches.\n- reproducibility (0-10): Could another researcher reproduce this work? Code, equations, detailed parameters help.\n- citation_quality (0-10): Are references real, relevant, and properly cited? 8+ unique real citations expected for high scores.\n\nIMPORTANT RULES:\n- If a section is MISSING entirely, score it 0\n- If references are placeholder/fake (e.g. \"[1] Author, A. (2026). Placeholder\"), score references as 1\n- Trivial proofs or papers under 300 words should score below 3 overall\n- Papers that prove obvious things (like 0*0=0) should score novelty as 0-1\n- Be STRICT — a score of 8+ means genuinely publishable quality\n- Lean 4 verified papers with valid proof hashes deserve reproducibility bonus\n\nReturn ONLY this JSON format (numbers 0-10, nothing else):\n{\"abstract\":N,\"introduction\":N,\"methodology\":N,\"results\":N,\"discussion\":N,\"conclusion\":N,\"references\":N,\"novelty\":N,\"reproducibility\":N,\"citation_quality\":N}\n\nPaper content:\n`;\n\n/**\n * Call a single LLM provider to score a paper.\n * Tries up to 2 keys from the provider before giving up.\n */\nasync function callLLMForScoring(prompt, provider) {\n if (!provider.keys.length) return null;\n\n const maxAttempts = Math.min(provider.keys.length, 2);\n for (let attempt = 0; attempt < maxAttempts; attempt++) {\n const key = nextKey(provider.id, provider.keys);\n if (!key) return null;\n\n const headers = { \"Content-Type\": \"application/json\" };\n headers[provider.authHeader] = `${provider.authPrefix}${key}`;\n if (provider.extraHeaders) Object.assign(headers, provider.extraHeaders);\n\n try {\n const res = await fetch(provider.url, {\n method: \"POST\",\n headers,\n body: JSON.stringify({\n model: provider.model,\n messages: [{ role: \"user\", content: prompt }],\n max_tokens: provider.maxTokens || 512,\n temperature: 0.1,\n }),\n signal: AbortSignal.timeout(provider.timeout || 30000),\n });\n\n if (res.status === 429) {\n console.warn(`[SCORING] ${provider.name} 429 rate-limited`);\n await new Promise(r => setTimeout(r, 3000));\n continue;\n }\n if (res.status === 402) {\n console.warn(`[SCORING] ${provider.name} 402 credits exhausted — skipping provider`);\n return null;\n }\n if (!res.ok) {\n const errBody = await res.text().catch(() => \"\");\n console.warn(`[SCORING] ${provider.name} HTTP ${res.status}: ${errBody.substring(0, 150)}`);\n continue;\n }\n\n const data = await res.json();\n // Support both OpenAI format (choices[0].message.content) and Cohere v2 (message.content[] array)\n let text = \"\";\n if (provider.responseFormat === \"cohere\") {\n // Cohere v2 returns array of content blocks: [{type:\"thinking\",...}, {type:\"text\",text:\"...\"}]\n const blocks = data.message?.content || [];\n if (Array.isArray(blocks)) {\n const textBlock = blocks.find(b => b.type === \"text\");\n text = textBlock?.text || blocks[blocks.length - 1]?.text || \"\";\n } else {\n text = typeof blocks === \"string\" ? blocks : \"\";\n }\n } else {\n text = data.choices?.[0]?.message?.content || \"\";\n }\n\n // Strip <think>...</think> tags (Sarvam, Qwen with thinking mode)\n if (provider.stripThinkTags || text.includes(\"<think>\")) {\n // Strip closed think blocks\n text = text.replace(/<think>[\\s\\S]*?<\\/think>/g, \"\").trim();\n // Strip unclosed think blocks (model ran out of tokens mid-think)\n if (text.startsWith(\"<think>\")) {\n text = text.replace(/<think>[\\s\\S]*/g, \"\").trim();\n }\n }\n\n // Extract JSON — use balanced braces to handle nested objects like {\"score\":8,\"why\":\"...\"}\n let jsonMatch = null;\n const firstBrace = text.indexOf('{');\n if (firstBrace !== -1) {\n let depth = 0, end = -1;\n for (let i = firstBrace; i < text.length; i++) {\n if (text[i] === '{') depth++;\n else if (text[i] === '}') { depth--; if (depth === 0) { end = i; break; } }\n }\n if (end !== -1) jsonMatch = [text.substring(firstBrace, end + 1)];\n }\n if (!jsonMatch) {\n console.warn(`[SCORING] ${provider.name} — no JSON in response`);\n continue;\n }\n\n const parsed = JSON.parse(jsonMatch[0]);\n const fields = [...SECTIONS, \"novelty\", \"reproducibility\", \"citation_quality\"];\n const feedback = {};\n for (const field of fields) {\n let val = parsed[field];\n // Support both plain number and {score, why} object format\n if (val && typeof val === \"object\" && typeof val.score === \"number\") {\n feedback[field] = val.why || null;\n val = val.score;\n }\n if (typeof val !== \"number\" || val < 0 || val > 10) {\n val = typeof val === \"number\" ? Math.max(0, Math.min(10, Math.round(val))) : 5;\n }\n parsed[field] = val;\n }\n\n // Clean parsed: only keep recognized score fields (remove stray \"why\" etc.)\n const cleanScores = {};\n for (const field of fields) cleanScores[field] = parsed[field];\n\n console.log(`[SCORING] ${provider.name} scored successfully`);\n return { scores: cleanScores, provider: provider.name, feedback: Object.keys(feedback).length > 0 ? feedback : null };\n } catch (e) {\n console.warn(`[SCORING] ${provider.name} error: ${e.message}`);\n }\n }\n return null;\n}\n\n/**\n * Deterministic heuristic scoring — used when all LLMs fail.\n */\nfunction heuristicScore(content) {\n const text = content || \"\";\n const words = text.split(/\\s+/).filter(w => w.length > 0);\n const wordCount = words.length;\n const lowerText = text.toLowerCase();\n\n const sectionScores = {};\n for (const section of SECTIONS) {\n const hasSection = lowerText.includes(`## ${section}`) || lowerText.includes(`# ${section}`);\n if (!hasSection) {\n sectionScores[section] = 0;\n continue;\n }\n const sectionRegex = new RegExp(`##?\\\\s*${section}[\\\\s\\\\S]*?(?=##?\\\\s|$)`, \"i\");\n const match = text.match(sectionRegex);\n const sectionWords = match ? match[0].split(/\\s+/).length : 0;\n if (sectionWords < 20) sectionScores[section] = 2;\n else if (sectionWords < 50) sectionScores[section] = 4;\n else if (sectionWords < 100) sectionScores[section] = 5;\n else if (sectionWords < 200) sectionScores[section] = 6;\n else sectionScores[section] = 7;\n }\n\n const refMatches = text.match(/\\[\\d+\\]/g) || [];\n const uniqueRefs = new Set(refMatches).size;\n const hasPlaceholderRefs = /placeholder|author,?\\s*a\\.\\s*\\(\\d{4}\\)/i.test(text);\n const hasRealAuthors = /[A-Z][a-z]+,\\s*[A-Z]\\.\\s*(?:&|,|et al)/g.test(text);\n const hasDOI = /doi\\.org|arxiv\\.org|10\\.\\d{4}/i.test(text);\n let refScore = hasPlaceholderRefs ? 1 : Math.min(7, uniqueRefs);\n if (hasRealAuthors) refScore = Math.min(10, refScore + 1);\n if (hasDOI) refScore = Math.min(10, refScore + 1);\n sectionScores.references = refScore;\n\n const technicalTerms = (text.match(/\\b(algorithm|theorem|proof|complexity|O\\([^)]+\\)|convergence|optimal|novel|framework)\\b/gi) || []).length;\n const hasFigures = /figure \\d|fig\\.\\s*\\d|table \\d/i.test(text);\n let novelty = wordCount > 2000 ? 4 : wordCount > 1000 ? 3 : 2;\n novelty += Math.min(3, Math.floor(technicalTerms / 5));\n if (hasFigures) novelty += 1;\n\n const hasCode = /```[\\s\\S]*?```/.test(text);\n const hasEquations = /\\$[^$]+\\$/.test(text) || /\\\\begin\\{/.test(text);\n const hasNumbers = /\\d+\\.\\d+%|\\d+\\.\\d+x|p\\s*[<>]\\s*0\\.\\d/i.test(text);\n let reproducibility = (hasCode ? 5 : 3) + (hasEquations ? 1 : 0) + (wordCount > 2000 ? 1 : 0);\n if (hasNumbers) reproducibility += 1;\n\n const citation_quality = hasPlaceholderRefs ? 1 : Math.min(6, uniqueRefs);\n\n return {\n scores: {\n ...sectionScores,\n novelty: Math.min(10, novelty),\n reproducibility: Math.min(10, reproducibility),\n citation_quality: Math.min(10, citation_quality),\n },\n provider: \"heuristic\"\n };\n}\n\n/**\n * Score a paper using heterogeneous multi-LLM swarm.\n * Tries to get 2 independent judges, falls back through provider chain.\n */\nexport async function scoreGranular(content, paperType = \"research\") {\n if (!content || content.trim().length < 50) {\n return {\n sections: Object.fromEntries(SECTIONS.map(s => [s, 0])),\n overall: 0,\n novelty: 0,\n reproducibility: 0,\n citation_quality: 0,\n judges: [],\n judge_count: 0,\n error: \"Content too short to score\"\n };\n }\n\n // All LLM judges support 32k+ context. Send enough content to include ALL 7 mandatory sections.\n // Previous limit of 4000 chars truncated papers before methodology/results/conclusion, causing\n // strict judges (Cohere, Xiaomi) to correctly score missing sections as 0.\n const truncated = content.length > 16000 ? content.substring(0, 16000) + \"\\n\\n[... truncated for scoring ...]\" : content;\n const prompt = SCORING_PROMPT + truncated;\n\n // ALL available judges score independently for maximum consensus diversity.\n // Each model evaluates each section independently, then we average.\n // Run all judges in parallel for speed (each has its own timeout).\n const judgePromises = available.map(provider =>\n callLLMForScoring(prompt, provider).catch(() => null)\n );\n const judgeResults = await Promise.all(judgePromises);\n const judges = judgeResults.filter(Boolean);\n\n // If no LLM judges succeeded, use heuristic\n if (judges.length === 0) {\n const heuristic = heuristicScore(content);\n judges.push(heuristic);\n }\n\n // Average scores across all judges\n const allFields = [...SECTIONS, \"novelty\", \"reproducibility\", \"citation_quality\"];\n const averaged = {};\n for (const field of allFields) {\n const values = judges.map(j => j.scores[field]).filter(v => typeof v === \"number\");\n averaged[field] = values.length > 0\n ? Math.round((values.reduce((a, b) => a + b, 0) / values.length) * 10) / 10\n : 0;\n }\n\n // ── CALIBRATION PASS — adjust raw averages against reference benchmarks ──\n // This is the core fix for inflated scoring. Raw LLM averages are compared\n // against quality signals extracted from the paper content and calibrated\n // against recognized reference paper fingerprints.\n let calibration = null;\n try {\n const fieldResult = detectField(content);\n const signals = extractSignals(content);\n const benchmarks = REFERENCE_BENCHMARKS[fieldResult.field] || null;\n const { calibrated, adjustments } = calibrateScores(averaged, signals, benchmarks);\n\n // Apply calibrated scores over raw averages\n const adjustmentCount = Object.keys(adjustments).length;\n if (adjustmentCount > 0) {\n for (const [field, val] of Object.entries(calibrated)) {\n if (typeof val === \"number\" && averaged[field] !== undefined) {\n averaged[field] = val;\n }\n }\n console.log(`[SCORING] Calibration applied: ${adjustmentCount} adjustments, field=${fieldResult.field}, ` +\n `red_flags=${signals.red_flag_count}, depth=${signals.depth_score}`);\n }\n\n calibration = {\n field: fieldResult.field,\n field_confidence: fieldResult.confidence,\n signals_summary: {\n word_count: signals.word_count,\n sections_present: signals.sections_present.length,\n sections_missing: signals.sections_missing,\n red_flags: signals.red_flags,\n red_flag_count: signals.red_flag_count,\n has_formal_proofs: signals.has_formal_proofs,\n has_equations: signals.has_equations,\n has_code: signals.has_code,\n unique_refs: signals.unique_refs,\n has_placeholder_refs: signals.has_placeholder_refs,\n depth_score: signals.depth_score,\n evidence_markers: signals.evidence_markers,\n deception_count: signals.deception_count || 0,\n deception_matches: (signals.deception_matches || []).map(d => ({\n id: d.id, name: d.name, severity: d.severity,\n })),\n // New quality dimensions\n grammar: signals.grammar_quality ? {\n vocabulary_diversity: signals.grammar_quality.vocabulary_diversity_ttr,\n is_monotone: signals.grammar_quality.is_monotone,\n is_low_vocabulary: signals.grammar_quality.is_low_vocabulary,\n } : null,\n repetition_ratio: signals.repetition_score?.repetition_ratio || 0,\n code_quality: signals.code_quality?.blocks_found > 0 ? {\n blocks: signals.code_quality.blocks_found,\n has_real_code: signals.has_real_code || false,\n has_python: signals.code_quality.has_python || false,\n } : null,\n math_formulas: signals.math_quality?.formula_count || 0,\n lean4: signals.lean4_signals?.verification_level || \"none\",\n tables: signals.table_quality?.count || 0,\n },\n adjustments,\n adjustment_count: adjustmentCount,\n reference_papers: benchmarks ? benchmarks.references.map(r => r.title) : [],\n };\n } catch (calErr) {\n console.warn(`[SCORING] Calibration error (non-fatal): ${calErr.message}`);\n }\n\n // ── LIVE VERIFICATION PASS — real-time CrossRef, arXiv, code exec, Lean4 ──\n // Runs in parallel with independent timeouts. Non-fatal: if it fails, scoring\n // continues with just calibration. Results apply caps and bonuses to scores.\n let liveVerification = null;\n try {\n const verification = await runLiveVerification(content);\n const { adjustments: liveAdj, bonuses: liveBon } = verificationToAdjustments(verification);\n\n // Apply caps (these override score to a max value)\n for (const [key, val] of Object.entries(liveAdj)) {\n if (key.endsWith(\"_cap\") && typeof val === \"number\") {\n const field = key.replace(\"_cap\", \"\");\n if (averaged[field] !== undefined && averaged[field] > val) {\n averaged[field] = val;\n }\n }\n }\n\n // Apply bonuses (add to score, capped at 10)\n for (const [key, val] of Object.entries(liveBon)) {\n if (key.endsWith(\"_bonus\") && typeof val === \"number\") {\n const field = key.replace(\"_bonus\", \"\");\n if (averaged[field] !== undefined) {\n averaged[field] = Math.min(10, Math.round((averaged[field] + val) * 10) / 10);\n }\n }\n }\n\n liveVerification = {\n verification_time_ms: verification.verification_time_ms,\n citations: verification.citations ? {\n total: verification.citations.total,\n verified: verification.citations.verified,\n verification_rate: verification.citations.verification_rate,\n } : null,\n novelty: verification.novelty ? {\n searched: verification.novelty.searched,\n total_found: verification.novelty.total_found,\n novelty_concern: verification.novelty.novelty_concern,\n max_similarity: verification.novelty.max_similarity,\n } : null,\n code_execution: verification.code_execution ? {\n total: verification.code_execution.total,\n passed: verification.code_execution.passed,\n failed: verification.code_execution.failed,\n } : null,\n lean4: verification.lean4 ? {\n blocks_found: verification.lean4.blocks_found,\n verified: verification.lean4.verified,\n has_unsubstantiated_claim: verification.lean4.has_unsubstantiated_claim || false,\n } : null,\n adjustments: liveAdj,\n bonuses: liveBon,\n };\n\n const adjCount = Object.keys(liveAdj).filter(k => k.endsWith(\"_cap\")).length;\n const bonCount = Object.keys(liveBon).filter(k => k.endsWith(\"_bonus\")).length;\n if (adjCount > 0 || bonCount > 0) {\n console.log(`[SCORING] Live verification: ${adjCount} caps, ${bonCount} bonuses applied (${verification.verification_time_ms}ms)`);\n }\n } catch (liveErr) {\n console.warn(`[SCORING] Live verification error (non-fatal): ${liveErr.message}`);\n }\n\n const sectionValues = SECTIONS.map(s => averaged[s]);\n const overall = Math.round((sectionValues.reduce((a, b) => a + b, 0) / SECTIONS.length) * 10) / 10;\n\n // Per-judge detail breakdown (individual scores + feedback)\n const judge_details = judges.map(j => ({\n judge: j.provider,\n scores: j.scores,\n feedback: j.feedback || null,\n }));\n\n // Consensus score per dimension (1 = all judges agree, 0 = total disagreement)\n const consensus = {};\n for (const field of allFields) {\n const values = judges.map(j => j.scores[field]).filter(v => typeof v === \"number\");\n if (values.length < 2) { consensus[field] = 1.0; continue; }\n const mean = values.reduce((a, b) => a + b, 0) / values.length;\n const variance = values.reduce((sum, v) => sum + (v - mean) ** 2, 0) / values.length;\n const stddev = Math.sqrt(variance);\n consensus[field] = Math.round(Math.max(0, 1 - stddev / 5) * 100) / 100;\n }\n const overall_consensus = Math.round(\n (allFields.reduce((sum, f) => sum + consensus[f], 0) / allFields.length) * 100\n ) / 100;\n\n // Aggregate feedback per dimension (from all judges that provided it)\n const aggregated_feedback = {};\n for (const field of allFields) {\n const comments = judges\n .filter(j => j.feedback && j.feedback[field])\n .map(j => ({ judge: j.provider, comment: j.feedback[field] }));\n if (comments.length > 0) aggregated_feedback[field] = comments;\n }\n\n const result = {\n sections: Object.fromEntries(SECTIONS.map(s => [s, averaged[s]])),\n overall,\n novelty: averaged.novelty,\n reproducibility: averaged.reproducibility,\n citation_quality: averaged.citation_quality,\n judges: judges.map(j => j.provider),\n judge_count: judges.length,\n judge_details,\n consensus,\n overall_consensus,\n feedback: Object.keys(aggregated_feedback).length > 0 ? aggregated_feedback : null,\n scored_at: new Date().toISOString(),\n paper_type: paperType,\n calibration,\n live_verification: liveVerification,\n };\n\n console.log(`[SCORING] Granular score: overall=${overall}, consensus=${overall_consensus}, judges=${result.judges.join(\",\")}`);\n return result;\n}\n"}, "84a245f46a49a69ab3c20b95b2735507f697685d": {"t": 1785753940.860885, "n": 25215, "h": "2eaad721baa03aad", "c": "/**\n * P2PCLAW Granular Scoring Service\n * =================================\n * Heterogeneous multi-LLM scoring engine that evaluates papers section-by-section.\n *\n * Provider chain (updated 2026-04-01):\n * 1. Cerebras — qwen-3-235b (3 keys, free, ultra-fast)\n * 2. Cerebras — llama3.1-8b (3 keys, free, ultra-fast)\n * 3. Mistral — mistral-small-latest (2 keys, free)\n * 4. Sarvam — sarvam-m (Indian AI, free)\n * 5. OpenRouter — qwen3-coder:free (free)\n * 6. Groq — llama-3.3-70b-versatile (7 keys, may be restricted)\n * 7. NVIDIA — meta/llama-3.3-70b-instruct (3 keys, free)\n * 8. Inception — mercury-2 (5 keys, free)\n * 9. Xiaomi MiMo — mimo-v2-flash (3 keys, free)\n * 10. Xiaomi MiMo — mimo-v2-pro (3 keys, free, reasoning)\n * 11. Cohere — command-a-reasoning (8 keys, reasoning model)\n * 12. Deterministic heuristic fallback (never blocks)\n *\n * ALL available judges score independently. Final score = average across all judges.\n * Each model evaluates each section independently for maximum consensus diversity.\n */\n\nimport { detectField, extractSignals, calibrateScores, REFERENCE_BENCHMARKS } from \"./calibrationService.js\";\n\nconst SECTIONS = [\"abstract\", \"introduction\", \"methodology\", \"results\", \"discussion\", \"conclusion\", \"references\"];\n\n// ── Load keys with rotation ─────────────────────────────────────────────────\n\nfunction loadKeys(envPrefix, maxKeys = 10) {\n const keys = [];\n for (let i = 1; i <= maxKeys; i++) {\n const k = process.env[`${envPrefix}_${i}`] || process.env[`${envPrefix}${i}`];\n if (k) keys.push(k);\n }\n // Also check single-key env vars\n const single = process.env[envPrefix];\n if (single && !keys.includes(single)) keys.unshift(single);\n return keys;\n}\n\nconst keyIndices = {};\nfunction nextKey(providerId, keys) {\n if (!keys.length) return null;\n const idx = keyIndices[providerId] || 0;\n const key = keys[idx % keys.length];\n keyIndices[providerId] = (idx + 1) % keys.length;\n return key;\n}\n\n// ── Provider definitions ────────────────────────────────────────────────────\n\nconst PROVIDERS = [\n // --- Cerebras: 3 keys x 2 models = up to 6 independent judges ---\n {\n id: \"cerebras-qwen\",\n name: \"Cerebras-Qwen235B\",\n url: \"https://api.cerebras.ai/v1/chat/completions\",\n model: \"qwen-3-235b-a22b-instruct-2507\",\n keys: loadKeys(\"CEREBRAS_API_KEY\").concat(loadKeys(\"CEREBRAS_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 1024,\n },\n {\n id: \"cerebras-llama\",\n name: \"Cerebras-Llama8B\",\n url: \"https://api.cerebras.ai/v1/chat/completions\",\n model: \"llama3.1-8b\",\n keys: loadKeys(\"CEREBRAS_API_KEY\").concat(loadKeys(\"CEREBRAS_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n // --- Mistral: 2 keys, reliable ---\n {\n id: \"mistral\",\n name: \"Mistral\",\n url: \"https://api.mistral.ai/v1/chat/completions\",\n model: \"mistral-small-latest\",\n keys: loadKeys(\"MISTRAL_API_KEY\").concat(loadKeys(\"MISTRAL_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n // --- Sarvam (Indian AI): sarvam-m, uses <think> tags (needs 2048+ tokens) ---\n {\n id: \"sarvam\",\n name: \"Sarvam\",\n url: \"https://api.sarvam.ai/v1/chat/completions\",\n model: \"sarvam-m\",\n keys: loadKeys(\"SARVAM_KEY\").concat(loadKeys(\"SARVAM_API_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 2048,\n },\n // --- OpenRouter: free models ---\n {\n id: \"openrouter\",\n name: \"OpenRouter\",\n url: \"https://openrouter.ai/api/v1/chat/completions\",\n model: \"qwen/qwen3-coder:free\",\n keys: loadKeys(\"OPENROUTER_API_KEY\"),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n extraHeaders: { \"HTTP-Referer\": \"https://www.p2pclaw.com\", \"X-Title\": \"P2PCLAW Scoring\" },\n },\n // --- Groq: 7 keys but may be org-restricted ---\n {\n id: \"groq\",\n name: \"Groq\",\n url: \"https://api.groq.com/openai/v1/chat/completions\",\n model: \"llama-3.3-70b-versatile\",\n keys: loadKeys(\"GROQ_API_KEY\").concat(loadKeys(\"GROQ_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n // --- NVIDIA: 3 keys ---\n {\n id: \"nvidia\",\n name: \"NVIDIA\",\n url: \"https://integrate.api.nvidia.com/v1/chat/completions\",\n model: \"meta/llama-3.3-70b-instruct\",\n keys: loadKeys(\"NVAPI_KEY\").concat(loadKeys(\"NVIDIA_API_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n // --- Inception: mercury-2 ---\n {\n id: \"inception\",\n name: \"Inception\",\n url: \"https://api.inceptionlabs.ai/v1/chat/completions\",\n model: \"mercury-2\",\n keys: loadKeys(\"INCEPTION_API_KEY\").concat(loadKeys(\"INCEPTION_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n // --- Xiaomi MiMo: 3 keys x 2 models = up to 2 independent judges ---\n {\n id: \"xiaomi-flash\",\n name: \"Xiaomi-MiMo-Flash\",\n url: \"https://api.xiaomimimo.com/v1/chat/completions\",\n model: \"mimo-v2-flash\",\n keys: loadKeys(\"XIAOMI_API_KEY\").concat(loadKeys(\"XIAOMI_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n {\n id: \"xiaomi-pro\",\n name: \"Xiaomi-MiMo-Pro\",\n url: \"https://api.xiaomimimo.com/v1/chat/completions\",\n model: \"mimo-v2-pro\",\n keys: loadKeys(\"XIAOMI_API_KEY\").concat(loadKeys(\"XIAOMI_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 1024,\n },\n // --- Cohere: command-a-reasoning model (8 keys, reasoning/thinking model) ---\n {\n id: \"cohere\",\n name: \"Cohere-CommandA\",\n url: \"https://api.cohere.com/v2/chat\",\n model: \"command-a-reasoning-08-2025\",\n keys: loadKeys(\"COHERE_API_KEY\").concat(loadKeys(\"COHERE_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 4096, // reasoning model needs extra tokens for thinking + answer\n responseFormat: \"cohere\", // data.message.content[] array with {type:\"thinking\"} + {type:\"text\"}\n timeout: 90000, // 90s — reasoning model needs time to think through all 10 dimensions\n },\n];\n\n// Deduplicate keys within each provider\nfor (const p of PROVIDERS) {\n p.keys = [...new Set(p.keys)].filter(Boolean);\n}\n\n// Log available providers\nconst available = PROVIDERS.filter(p => p.keys.length > 0);\nconsole.log(`[SCORING] ${available.length} LLM providers available: ${available.map(p => `${p.name}(${p.keys.length})`).join(\", \")}`);\nif (available.length === 0) console.warn(\"[SCORING] No LLM providers — heuristic scoring only.\");\n// Debug: log specifically which providers have NO keys\nconst unavailable = PROVIDERS.filter(p => p.keys.length === 0);\nif (unavailable.length > 0) console.log(`[SCORING] Providers with NO keys: ${unavailable.map(p => p.name).join(\", \")}`);\n\nconst SCORING_PROMPT = `You are an academic paper quality evaluator for the P2PCLAW decentralized research network.\n\nScore this paper on a 0-10 scale for each criterion. For EACH score, include a brief \"why\" (1 sentence explaining the score).\n\nScoring criteria:\n- abstract (0-10): Quality, clarity, and completeness of the abstract. Clear problem statement, scope, and key results summarized.\n- introduction (0-10): Problem statement clarity, context, motivation. Should cite 2+ related works.\n- methodology (0-10): Rigor, reproducibility, appropriate methods. Must describe steps clearly enough to reproduce.\n- results (0-10): Quality of findings, data presentation, significance. Quantitative data strongly preferred.\n- discussion (0-10): Interpretation depth, limitations acknowledged, implications for the field.\n- conclusion (0-10): Summary of key findings (expected, NOT penalized as repetition), concrete future work directions, and connection back to the research question. A good conclusion SHOULD summarize findings — that is standard scientific practice.\n- references (0-10): Citation quality, relevance, proper formatting (0 if no real references). Real author names, titles, years, DOIs/URLs expected.\n- novelty (0-10): Original contribution to the field. Novel frameworks, terminology, or approaches.\n- reproducibility (0-10): Could another researcher reproduce this work? Code, equations, detailed parameters help.\n- citation_quality (0-10): Are references real, relevant, and properly cited? 8+ unique real citations expected for high scores.\n\nIMPORTANT RULES:\n- If a section is MISSING entirely, score it 0\n- If references are placeholder/fake (e.g. \"[1] Author, A. (2026). Placeholder\"), score references as 1\n- Trivial proofs or papers under 300 words should score below 3 overall\n- Papers that prove obvious things (like 0*0=0) should score novelty as 0-1\n- Be STRICT — a score of 8+ means genuinely publishable quality\n- Lean 4 verified papers with valid proof hashes deserve reproducibility bonus\n\nReturn ONLY this JSON format (numbers 0-10, nothing else):\n{\"abstract\":N,\"introduction\":N,\"methodology\":N,\"results\":N,\"discussion\":N,\"conclusion\":N,\"references\":N,\"novelty\":N,\"reproducibility\":N,\"citation_quality\":N}\n\nPaper content:\n`;\n\n/**\n * Call a single LLM provider to score a paper.\n * Tries up to 2 keys from the provider before giving up.\n */\nasync function callLLMForScoring(prompt, provider) {\n if (!provider.keys.length) return null;\n\n const maxAttempts = Math.min(provider.keys.length, 2);\n for (let attempt = 0; attempt < maxAttempts; attempt++) {\n const key = nextKey(provider.id, provider.keys);\n if (!key) return null;\n\n const headers = { \"Content-Type\": \"application/json\" };\n headers[provider.authHeader] = `${provider.authPrefix}${key}`;\n if (provider.extraHeaders) Object.assign(headers, provider.extraHeaders);\n\n try {\n const res = await fetch(provider.url, {\n method: \"POST\",\n headers,\n body: JSON.stringify({\n model: provider.model,\n messages: [{ role: \"user\", content: prompt }],\n max_tokens: provider.maxTokens || 512,\n temperature: 0.1,\n }),\n signal: AbortSignal.timeout(provider.timeout || 30000),\n });\n\n if (res.status === 429) {\n console.warn(`[SCORING] ${provider.name} 429 rate-limited`);\n await new Promise(r => setTimeout(r, 3000));\n continue;\n }\n if (res.status === 402) {\n console.warn(`[SCORING] ${provider.name} 402 credits exhausted — skipping provider`);\n return null;\n }\n if (!res.ok) {\n const errBody = await res.text().catch(() => \"\");\n console.warn(`[SCORING] ${provider.name} HTTP ${res.status}: ${errBody.substring(0, 150)}`);\n continue;\n }\n\n const data = await res.json();\n // Support both OpenAI format (choices[0].message.content) and Cohere v2 (message.content[] array)\n let text = \"\";\n if (provider.responseFormat === \"cohere\") {\n // Cohere v2 returns array of content blocks: [{type:\"thinking\",...}, {type:\"text\",text:\"...\"}]\n const blocks = data.message?.content || [];\n if (Array.isArray(blocks)) {\n const textBlock = blocks.find(b => b.type === \"text\");\n text = textBlock?.text || blocks[blocks.length - 1]?.text || \"\";\n } else {\n text = typeof blocks === \"string\" ? blocks : \"\";\n }\n } else {\n text = data.choices?.[0]?.message?.content || \"\";\n }\n\n // Strip <think>...</think> tags (Sarvam, Qwen with thinking mode)\n if (provider.stripThinkTags || text.includes(\"<think>\")) {\n // Strip closed think blocks\n text = text.replace(/<think>[\\s\\S]*?<\\/think>/g, \"\").trim();\n // Strip unclosed think blocks (model ran out of tokens mid-think)\n if (text.startsWith(\"<think>\")) {\n text = text.replace(/<think>[\\s\\S]*/g, \"\").trim();\n }\n }\n\n // Extract JSON — use balanced braces to handle nested objects like {\"score\":8,\"why\":\"...\"}\n let jsonMatch = null;\n const firstBrace = text.indexOf('{');\n if (firstBrace !== -1) {\n let depth = 0, end = -1;\n for (let i = firstBrace; i < text.length; i++) {\n if (text[i] === '{') depth++;\n else if (text[i] === '}') { depth--; if (depth === 0) { end = i; break; } }\n }\n if (end !== -1) jsonMatch = [text.substring(firstBrace, end + 1)];\n }\n if (!jsonMatch) {\n console.warn(`[SCORING] ${provider.name} — no JSON in response`);\n continue;\n }\n\n const parsed = JSON.parse(jsonMatch[0]);\n const fields = [...SECTIONS, \"novelty\", \"reproducibility\", \"citation_quality\"];\n const feedback = {};\n for (const field of fields) {\n let val = parsed[field];\n // Support both plain number and {score, why} object format\n if (val && typeof val === \"object\" && typeof val.score === \"number\") {\n feedback[field] = val.why || null;\n val = val.score;\n }\n if (typeof val !== \"number\" || val < 0 || val > 10) {\n val = typeof val === \"number\" ? Math.max(0, Math.min(10, Math.round(val))) : 5;\n }\n parsed[field] = val;\n }\n\n // Clean parsed: only keep recognized score fields (remove stray \"why\" etc.)\n const cleanScores = {};\n for (const field of fields) cleanScores[field] = parsed[field];\n\n console.log(`[SCORING] ${provider.name} scored successfully`);\n return { scores: cleanScores, provider: provider.name, feedback: Object.keys(feedback).length > 0 ? feedback : null };\n } catch (e) {\n console.warn(`[SCORING] ${provider.name} error: ${e.message}`);\n }\n }\n return null;\n}\n\n/**\n * Deterministic heuristic scoring — used when all LLMs fail.\n */\nfunction heuristicScore(content) {\n const text = content || \"\";\n const words = text.split(/\\s+/).filter(w => w.length > 0);\n const wordCount = words.length;\n const lowerText = text.toLowerCase();\n\n const sectionScores = {};\n for (const section of SECTIONS) {\n const hasSection = lowerText.includes(`## ${section}`) || lowerText.includes(`# ${section}`);\n if (!hasSection) {\n sectionScores[section] = 0;\n continue;\n }\n const sectionRegex = new RegExp(`##?\\\\s*${section}[\\\\s\\\\S]*?(?=##?\\\\s|$)`, \"i\");\n const match = text.match(sectionRegex);\n const sectionWords = match ? match[0].split(/\\s+/).length : 0;\n if (sectionWords < 20) sectionScores[section] = 2;\n else if (sectionWords < 50) sectionScores[section] = 4;\n else if (sectionWords < 100) sectionScores[section] = 5;\n else if (sectionWords < 200) sectionScores[section] = 6;\n else sectionScores[section] = 7;\n }\n\n const refMatches = text.match(/\\[\\d+\\]/g) || [];\n const uniqueRefs = new Set(refMatches).size;\n const hasPlaceholderRefs = /placeholder|author,?\\s*a\\.\\s*\\(\\d{4}\\)/i.test(text);\n const hasRealAuthors = /[A-Z][a-z]+,\\s*[A-Z]\\.\\s*(?:&|,|et al)/g.test(text);\n const hasDOI = /doi\\.org|arxiv\\.org|10\\.\\d{4}/i.test(text);\n let refScore = hasPlaceholderRefs ? 1 : Math.min(7, uniqueRefs);\n if (hasRealAuthors) refScore = Math.min(10, refScore + 1);\n if (hasDOI) refScore = Math.min(10, refScore + 1);\n sectionScores.references = refScore;\n\n const technicalTerms = (text.match(/\\b(algorithm|theorem|proof|complexity|O\\([^)]+\\)|convergence|optimal|novel|framework)\\b/gi) || []).length;\n const hasFigures = /figure \\d|fig\\.\\s*\\d|table \\d/i.test(text);\n let novelty = wordCount > 2000 ? 4 : wordCount > 1000 ? 3 : 2;\n novelty += Math.min(3, Math.floor(technicalTerms / 5));\n if (hasFigures) novelty += 1;\n\n const hasCode = /```[\\s\\S]*?```/.test(text);\n const hasEquations = /\\$[^$]+\\$/.test(text) || /\\\\begin\\{/.test(text);\n const hasNumbers = /\\d+\\.\\d+%|\\d+\\.\\d+x|p\\s*[<>]\\s*0\\.\\d/i.test(text);\n let reproducibility = (hasCode ? 5 : 3) + (hasEquations ? 1 : 0) + (wordCount > 2000 ? 1 : 0);\n if (hasNumbers) reproducibility += 1;\n\n const citation_quality = hasPlaceholderRefs ? 1 : Math.min(6, uniqueRefs);\n\n return {\n scores: {\n ...sectionScores,\n novelty: Math.min(10, novelty),\n reproducibility: Math.min(10, reproducibility),\n citation_quality: Math.min(10, citation_quality),\n },\n provider: \"heuristic\"\n };\n}\n\n/**\n * Score a paper using heterogeneous multi-LLM swarm.\n * Tries to get 2 independent judges, falls back through provider chain.\n */\nexport async function scoreGranular(content, paperType = \"research\") {\n if (!content || content.trim().length < 50) {\n return {\n sections: Object.fromEntries(SECTIONS.map(s => [s, 0])),\n overall: 0,\n novelty: 0,\n reproducibility: 0,\n citation_quality: 0,\n judges: [],\n judge_count: 0,\n error: \"Content too short to score\"\n };\n }\n\n // All LLM judges support 32k+ context. Send enough content to include ALL 7 mandatory sections.\n // Previous limit of 4000 chars truncated papers before methodology/results/conclusion, causing\n // strict judges (Cohere, Xiaomi) to correctly score missing sections as 0.\n const truncated = content.length > 16000 ? content.substring(0, 16000) + \"\\n\\n[... truncated for scoring ...]\" : content;\n const prompt = SCORING_PROMPT + truncated;\n\n // ALL available judges score independently for maximum consensus diversity.\n // Each model evaluates each section independently, then we average.\n // Run all judges in parallel for speed (each has its own timeout).\n const judgePromises = available.map(provider =>\n callLLMForScoring(prompt, provider).catch(() => null)\n );\n const judgeResults = await Promise.all(judgePromises);\n const judges = judgeResults.filter(Boolean);\n\n // If no LLM judges succeeded, use heuristic\n if (judges.length === 0) {\n const heuristic = heuristicScore(content);\n judges.push(heuristic);\n }\n\n // Average scores across all judges\n const allFields = [...SECTIONS, \"novelty\", \"reproducibility\", \"citation_quality\"];\n const averaged = {};\n for (const field of allFields) {\n const values = judges.map(j => j.scores[field]).filter(v => typeof v === \"number\");\n averaged[field] = values.length > 0\n ? Math.round((values.reduce((a, b) => a + b, 0) / values.length) * 10) / 10\n : 0;\n }\n\n // ── CALIBRATION PASS — adjust raw averages against reference benchmarks ──\n // This is the core fix for inflated scoring. Raw LLM averages are compared\n // against quality signals extracted from the paper content and calibrated\n // against recognized reference paper fingerprints.\n let calibration = null;\n try {\n const fieldResult = detectField(content);\n const signals = extractSignals(content);\n const benchmarks = REFERENCE_BENCHMARKS[fieldResult.field] || null;\n const { calibrated, adjustments } = calibrateScores(averaged, signals, benchmarks);\n\n // Apply calibrated scores over raw averages\n const adjustmentCount = Object.keys(adjustments).length;\n if (adjustmentCount > 0) {\n for (const [field, val] of Object.entries(calibrated)) {\n if (typeof val === \"number\" && averaged[field] !== undefined) {\n averaged[field] = val;\n }\n }\n console.log(`[SCORING] Calibration applied: ${adjustmentCount} adjustments, field=${fieldResult.field}, ` +\n `red_flags=${signals.red_flag_count}, depth=${signals.depth_score}`);\n }\n\n calibration = {\n field: fieldResult.field,\n field_confidence: fieldResult.confidence,\n signals_summary: {\n word_count: signals.word_count,\n sections_present: signals.sections_present.length,\n sections_missing: signals.sections_missing,\n red_flags: signals.red_flags,\n red_flag_count: signals.red_flag_count,\n has_formal_proofs: signals.has_formal_proofs,\n has_equations: signals.has_equations,\n has_code: signals.has_code,\n unique_refs: signals.unique_refs,\n has_placeholder_refs: signals.has_placeholder_refs,\n depth_score: signals.depth_score,\n evidence_markers: signals.evidence_markers,\n deception_count: signals.deception_count || 0,\n deception_matches: (signals.deception_matches || []).map(d => ({\n id: d.id, name: d.name, severity: d.severity,\n })),\n // New quality dimensions\n grammar: signals.grammar_quality ? {\n vocabulary_diversity: signals.grammar_quality.vocabulary_diversity_ttr,\n is_monotone: signals.grammar_quality.is_monotone,\n is_low_vocabulary: signals.grammar_quality.is_low_vocabulary,\n } : null,\n repetition_ratio: signals.repetition_score?.repetition_ratio || 0,\n code_quality: signals.code_quality?.blocks_found > 0 ? {\n blocks: signals.code_quality.blocks_found,\n has_real_code: signals.has_real_code || false,\n has_python: signals.code_quality.has_python || false,\n } : null,\n math_formulas: signals.math_quality?.formula_count || 0,\n lean4: signals.lean4_signals?.verification_level || \"none\",\n tables: signals.table_quality?.count || 0,\n },\n adjustments,\n adjustment_count: adjustmentCount,\n reference_papers: benchmarks ? benchmarks.references.map(r => r.title) : [],\n };\n } catch (calErr) {\n console.warn(`[SCORING] Calibration error (non-fatal): ${calErr.message}`);\n }\n\n const sectionValues = SECTIONS.map(s => averaged[s]);\n const overall = Math.round((sectionValues.reduce((a, b) => a + b, 0) / SECTIONS.length) * 10) / 10;\n\n // Per-judge detail breakdown (individual scores + feedback)\n const judge_details = judges.map(j => ({\n judge: j.provider,\n scores: j.scores,\n feedback: j.feedback || null,\n }));\n\n // Consensus score per dimension (1 = all judges agree, 0 = total disagreement)\n const consensus = {};\n for (const field of allFields) {\n const values = judges.map(j => j.scores[field]).filter(v => typeof v === \"number\");\n if (values.length < 2) { consensus[field] = 1.0; continue; }\n const mean = values.reduce((a, b) => a + b, 0) / values.length;\n const variance = values.reduce((sum, v) => sum + (v - mean) ** 2, 0) / values.length;\n const stddev = Math.sqrt(variance);\n consensus[field] = Math.round(Math.max(0, 1 - stddev / 5) * 100) / 100;\n }\n const overall_consensus = Math.round(\n (allFields.reduce((sum, f) => sum + consensus[f], 0) / allFields.length) * 100\n ) / 100;\n\n // Aggregate feedback per dimension (from all judges that provided it)\n const aggregated_feedback = {};\n for (const field of allFields) {\n const comments = judges\n .filter(j => j.feedback && j.feedback[field])\n .map(j => ({ judge: j.provider, comment: j.feedback[field] }));\n if (comments.length > 0) aggregated_feedback[field] = comments;\n }\n\n const result = {\n sections: Object.fromEntries(SECTIONS.map(s => [s, averaged[s]])),\n overall,\n novelty: averaged.novelty,\n reproducibility: averaged.reproducibility,\n citation_quality: averaged.citation_quality,\n judges: judges.map(j => j.provider),\n judge_count: judges.length,\n judge_details,\n consensus,\n overall_consensus,\n feedback: Object.keys(aggregated_feedback).length > 0 ? aggregated_feedback : null,\n scored_at: new Date().toISOString(),\n paper_type: paperType,\n calibration,\n };\n\n console.log(`[SCORING] Granular score: overall=${overall}, consensus=${overall_consensus}, judges=${result.judges.join(\",\")}`);\n return result;\n}\n"}, "812e6119b092f30b392466dde9afdb469ff79468": {"t": 1785753942.7920098, "n": 24256, "h": "6eaa7f8c7d5b25bb", "c": "/**\n * P2PCLAW Granular Scoring Service\n * =================================\n * Heterogeneous multi-LLM scoring engine that evaluates papers section-by-section.\n *\n * Provider chain (updated 2026-04-01):\n * 1. Cerebras — qwen-3-235b (3 keys, free, ultra-fast)\n * 2. Cerebras — llama3.1-8b (3 keys, free, ultra-fast)\n * 3. Mistral — mistral-small-latest (2 keys, free)\n * 4. Sarvam — sarvam-m (Indian AI, free)\n * 5. OpenRouter — qwen3-coder:free (free)\n * 6. Groq — llama-3.3-70b-versatile (7 keys, may be restricted)\n * 7. NVIDIA — meta/llama-3.3-70b-instruct (3 keys, free)\n * 8. Inception — mercury-2 (5 keys, free)\n * 9. Xiaomi MiMo — mimo-v2-flash (3 keys, free)\n * 10. Xiaomi MiMo — mimo-v2-pro (3 keys, free, reasoning)\n * 11. Cohere — command-a-reasoning (8 keys, reasoning model)\n * 12. Deterministic heuristic fallback (never blocks)\n *\n * ALL available judges score independently. Final score = average across all judges.\n * Each model evaluates each section independently for maximum consensus diversity.\n */\n\nimport { detectField, extractSignals, calibrateScores, REFERENCE_BENCHMARKS } from \"./calibrationService.js\";\n\nconst SECTIONS = [\"abstract\", \"introduction\", \"methodology\", \"results\", \"discussion\", \"conclusion\", \"references\"];\n\n// ── Load keys with rotation ─────────────────────────────────────────────────\n\nfunction loadKeys(envPrefix, maxKeys = 10) {\n const keys = [];\n for (let i = 1; i <= maxKeys; i++) {\n const k = process.env[`${envPrefix}_${i}`] || process.env[`${envPrefix}${i}`];\n if (k) keys.push(k);\n }\n // Also check single-key env vars\n const single = process.env[envPrefix];\n if (single && !keys.includes(single)) keys.unshift(single);\n return keys;\n}\n\nconst keyIndices = {};\nfunction nextKey(providerId, keys) {\n if (!keys.length) return null;\n const idx = keyIndices[providerId] || 0;\n const key = keys[idx % keys.length];\n keyIndices[providerId] = (idx + 1) % keys.length;\n return key;\n}\n\n// ── Provider definitions ────────────────────────────────────────────────────\n\nconst PROVIDERS = [\n // --- Cerebras: 3 keys x 2 models = up to 6 independent judges ---\n {\n id: \"cerebras-qwen\",\n name: \"Cerebras-Qwen235B\",\n url: \"https://api.cerebras.ai/v1/chat/completions\",\n model: \"qwen-3-235b-a22b-instruct-2507\",\n keys: loadKeys(\"CEREBRAS_API_KEY\").concat(loadKeys(\"CEREBRAS_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 1024,\n },\n {\n id: \"cerebras-llama\",\n name: \"Cerebras-Llama8B\",\n url: \"https://api.cerebras.ai/v1/chat/completions\",\n model: \"llama3.1-8b\",\n keys: loadKeys(\"CEREBRAS_API_KEY\").concat(loadKeys(\"CEREBRAS_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n // --- Mistral: 2 keys, reliable ---\n {\n id: \"mistral\",\n name: \"Mistral\",\n url: \"https://api.mistral.ai/v1/chat/completions\",\n model: \"mistral-small-latest\",\n keys: loadKeys(\"MISTRAL_API_KEY\").concat(loadKeys(\"MISTRAL_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n // --- Sarvam (Indian AI): sarvam-m, uses <think> tags (needs 2048+ tokens) ---\n {\n id: \"sarvam\",\n name: \"Sarvam\",\n url: \"https://api.sarvam.ai/v1/chat/completions\",\n model: \"sarvam-m\",\n keys: loadKeys(\"SARVAM_KEY\").concat(loadKeys(\"SARVAM_API_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 2048,\n },\n // --- OpenRouter: free models ---\n {\n id: \"openrouter\",\n name: \"OpenRouter\",\n url: \"https://openrouter.ai/api/v1/chat/completions\",\n model: \"qwen/qwen3-coder:free\",\n keys: loadKeys(\"OPENROUTER_API_KEY\"),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n extraHeaders: { \"HTTP-Referer\": \"https://www.p2pclaw.com\", \"X-Title\": \"P2PCLAW Scoring\" },\n },\n // --- Groq: 7 keys but may be org-restricted ---\n {\n id: \"groq\",\n name: \"Groq\",\n url: \"https://api.groq.com/openai/v1/chat/completions\",\n model: \"llama-3.3-70b-versatile\",\n keys: loadKeys(\"GROQ_API_KEY\").concat(loadKeys(\"GROQ_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n // --- NVIDIA: 3 keys ---\n {\n id: \"nvidia\",\n name: \"NVIDIA\",\n url: \"https://integrate.api.nvidia.com/v1/chat/completions\",\n model: \"meta/llama-3.3-70b-instruct\",\n keys: loadKeys(\"NVAPI_KEY\").concat(loadKeys(\"NVIDIA_API_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n // --- Inception: mercury-2 ---\n {\n id: \"inception\",\n name: \"Inception\",\n url: \"https://api.inceptionlabs.ai/v1/chat/completions\",\n model: \"mercury-2\",\n keys: loadKeys(\"INCEPTION_API_KEY\").concat(loadKeys(\"INCEPTION_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n // --- Xiaomi MiMo: 3 keys x 2 models = up to 2 independent judges ---\n {\n id: \"xiaomi-flash\",\n name: \"Xiaomi-MiMo-Flash\",\n url: \"https://api.xiaomimimo.com/v1/chat/completions\",\n model: \"mimo-v2-flash\",\n keys: loadKeys(\"XIAOMI_API_KEY\").concat(loadKeys(\"XIAOMI_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n {\n id: \"xiaomi-pro\",\n name: \"Xiaomi-MiMo-Pro\",\n url: \"https://api.xiaomimimo.com/v1/chat/completions\",\n model: \"mimo-v2-pro\",\n keys: loadKeys(\"XIAOMI_API_KEY\").concat(loadKeys(\"XIAOMI_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 1024,\n },\n // --- Cohere: command-a-reasoning model (8 keys, reasoning/thinking model) ---\n {\n id: \"cohere\",\n name: \"Cohere-CommandA\",\n url: \"https://api.cohere.com/v2/chat\",\n model: \"command-a-reasoning-08-2025\",\n keys: loadKeys(\"COHERE_API_KEY\").concat(loadKeys(\"COHERE_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 4096, // reasoning model needs extra tokens for thinking + answer\n responseFormat: \"cohere\", // data.message.content[] array with {type:\"thinking\"} + {type:\"text\"}\n timeout: 90000, // 90s — reasoning model needs time to think through all 10 dimensions\n },\n];\n\n// Deduplicate keys within each provider\nfor (const p of PROVIDERS) {\n p.keys = [...new Set(p.keys)].filter(Boolean);\n}\n\n// Log available providers\nconst available = PROVIDERS.filter(p => p.keys.length > 0);\nconsole.log(`[SCORING] ${available.length} LLM providers available: ${available.map(p => `${p.name}(${p.keys.length})`).join(\", \")}`);\nif (available.length === 0) console.warn(\"[SCORING] No LLM providers — heuristic scoring only.\");\n// Debug: log specifically which providers have NO keys\nconst unavailable = PROVIDERS.filter(p => p.keys.length === 0);\nif (unavailable.length > 0) console.log(`[SCORING] Providers with NO keys: ${unavailable.map(p => p.name).join(\", \")}`);\n\nconst SCORING_PROMPT = `You are an academic paper quality evaluator for the P2PCLAW decentralized research network.\n\nScore this paper on a 0-10 scale for each criterion. For EACH score, include a brief \"why\" (1 sentence explaining the score).\n\nScoring criteria:\n- abstract (0-10): Quality, clarity, and completeness of the abstract. Clear problem statement, scope, and key results summarized.\n- introduction (0-10): Problem statement clarity, context, motivation. Should cite 2+ related works.\n- methodology (0-10): Rigor, reproducibility, appropriate methods. Must describe steps clearly enough to reproduce.\n- results (0-10): Quality of findings, data presentation, significance. Quantitative data strongly preferred.\n- discussion (0-10): Interpretation depth, limitations acknowledged, implications for the field.\n- conclusion (0-10): Summary of key findings (expected, NOT penalized as repetition), concrete future work directions, and connection back to the research question. A good conclusion SHOULD summarize findings — that is standard scientific practice.\n- references (0-10): Citation quality, relevance, proper formatting (0 if no real references). Real author names, titles, years, DOIs/URLs expected.\n- novelty (0-10): Original contribution to the field. Novel frameworks, terminology, or approaches.\n- reproducibility (0-10): Could another researcher reproduce this work? Code, equations, detailed parameters help.\n- citation_quality (0-10): Are references real, relevant, and properly cited? 8+ unique real citations expected for high scores.\n\nIMPORTANT RULES:\n- If a section is MISSING entirely, score it 0\n- If references are placeholder/fake (e.g. \"[1] Author, A. (2026). Placeholder\"), score references as 1\n- Trivial proofs or papers under 300 words should score below 3 overall\n- Papers that prove obvious things (like 0*0=0) should score novelty as 0-1\n- Be STRICT — a score of 8+ means genuinely publishable quality\n- Lean 4 verified papers with valid proof hashes deserve reproducibility bonus\n\nReturn ONLY this JSON format (numbers 0-10, nothing else):\n{\"abstract\":N,\"introduction\":N,\"methodology\":N,\"results\":N,\"discussion\":N,\"conclusion\":N,\"references\":N,\"novelty\":N,\"reproducibility\":N,\"citation_quality\":N}\n\nPaper content:\n`;\n\n/**\n * Call a single LLM provider to score a paper.\n * Tries up to 2 keys from the provider before giving up.\n */\nasync function callLLMForScoring(prompt, provider) {\n if (!provider.keys.length) return null;\n\n const maxAttempts = Math.min(provider.keys.length, 2);\n for (let attempt = 0; attempt < maxAttempts; attempt++) {\n const key = nextKey(provider.id, provider.keys);\n if (!key) return null;\n\n const headers = { \"Content-Type\": \"application/json\" };\n headers[provider.authHeader] = `${provider.authPrefix}${key}`;\n if (provider.extraHeaders) Object.assign(headers, provider.extraHeaders);\n\n try {\n const res = await fetch(provider.url, {\n method: \"POST\",\n headers,\n body: JSON.stringify({\n model: provider.model,\n messages: [{ role: \"user\", content: prompt }],\n max_tokens: provider.maxTokens || 512,\n temperature: 0.1,\n }),\n signal: AbortSignal.timeout(provider.timeout || 30000),\n });\n\n if (res.status === 429) {\n console.warn(`[SCORING] ${provider.name} 429 rate-limited`);\n await new Promise(r => setTimeout(r, 3000));\n continue;\n }\n if (res.status === 402) {\n console.warn(`[SCORING] ${provider.name} 402 credits exhausted — skipping provider`);\n return null;\n }\n if (!res.ok) {\n const errBody = await res.text().catch(() => \"\");\n console.warn(`[SCORING] ${provider.name} HTTP ${res.status}: ${errBody.substring(0, 150)}`);\n continue;\n }\n\n const data = await res.json();\n // Support both OpenAI format (choices[0].message.content) and Cohere v2 (message.content[] array)\n let text = \"\";\n if (provider.responseFormat === \"cohere\") {\n // Cohere v2 returns array of content blocks: [{type:\"thinking\",...}, {type:\"text\",text:\"...\"}]\n const blocks = data.message?.content || [];\n if (Array.isArray(blocks)) {\n const textBlock = blocks.find(b => b.type === \"text\");\n text = textBlock?.text || blocks[blocks.length - 1]?.text || \"\";\n } else {\n text = typeof blocks === \"string\" ? blocks : \"\";\n }\n } else {\n text = data.choices?.[0]?.message?.content || \"\";\n }\n\n // Strip <think>...</think> tags (Sarvam, Qwen with thinking mode)\n if (provider.stripThinkTags || text.includes(\"<think>\")) {\n // Strip closed think blocks\n text = text.replace(/<think>[\\s\\S]*?<\\/think>/g, \"\").trim();\n // Strip unclosed think blocks (model ran out of tokens mid-think)\n if (text.startsWith(\"<think>\")) {\n text = text.replace(/<think>[\\s\\S]*/g, \"\").trim();\n }\n }\n\n // Extract JSON — use balanced braces to handle nested objects like {\"score\":8,\"why\":\"...\"}\n let jsonMatch = null;\n const firstBrace = text.indexOf('{');\n if (firstBrace !== -1) {\n let depth = 0, end = -1;\n for (let i = firstBrace; i < text.length; i++) {\n if (text[i] === '{') depth++;\n else if (text[i] === '}') { depth--; if (depth === 0) { end = i; break; } }\n }\n if (end !== -1) jsonMatch = [text.substring(firstBrace, end + 1)];\n }\n if (!jsonMatch) {\n console.warn(`[SCORING] ${provider.name} — no JSON in response`);\n continue;\n }\n\n const parsed = JSON.parse(jsonMatch[0]);\n const fields = [...SECTIONS, \"novelty\", \"reproducibility\", \"citation_quality\"];\n const feedback = {};\n for (const field of fields) {\n let val = parsed[field];\n // Support both plain number and {score, why} object format\n if (val && typeof val === \"object\" && typeof val.score === \"number\") {\n feedback[field] = val.why || null;\n val = val.score;\n }\n if (typeof val !== \"number\" || val < 0 || val > 10) {\n val = typeof val === \"number\" ? Math.max(0, Math.min(10, Math.round(val))) : 5;\n }\n parsed[field] = val;\n }\n\n // Clean parsed: only keep recognized score fields (remove stray \"why\" etc.)\n const cleanScores = {};\n for (const field of fields) cleanScores[field] = parsed[field];\n\n console.log(`[SCORING] ${provider.name} scored successfully`);\n return { scores: cleanScores, provider: provider.name, feedback: Object.keys(feedback).length > 0 ? feedback : null };\n } catch (e) {\n console.warn(`[SCORING] ${provider.name} error: ${e.message}`);\n }\n }\n return null;\n}\n\n/**\n * Deterministic heuristic scoring — used when all LLMs fail.\n */\nfunction heuristicScore(content) {\n const text = content || \"\";\n const words = text.split(/\\s+/).filter(w => w.length > 0);\n const wordCount = words.length;\n const lowerText = text.toLowerCase();\n\n const sectionScores = {};\n for (const section of SECTIONS) {\n const hasSection = lowerText.includes(`## ${section}`) || lowerText.includes(`# ${section}`);\n if (!hasSection) {\n sectionScores[section] = 0;\n continue;\n }\n const sectionRegex = new RegExp(`##?\\\\s*${section}[\\\\s\\\\S]*?(?=##?\\\\s|$)`, \"i\");\n const match = text.match(sectionRegex);\n const sectionWords = match ? match[0].split(/\\s+/).length : 0;\n if (sectionWords < 20) sectionScores[section] = 2;\n else if (sectionWords < 50) sectionScores[section] = 4;\n else if (sectionWords < 100) sectionScores[section] = 5;\n else if (sectionWords < 200) sectionScores[section] = 6;\n else sectionScores[section] = 7;\n }\n\n const refMatches = text.match(/\\[\\d+\\]/g) || [];\n const uniqueRefs = new Set(refMatches).size;\n const hasPlaceholderRefs = /placeholder|author,?\\s*a\\.\\s*\\(\\d{4}\\)/i.test(text);\n const hasRealAuthors = /[A-Z][a-z]+,\\s*[A-Z]\\.\\s*(?:&|,|et al)/g.test(text);\n const hasDOI = /doi\\.org|arxiv\\.org|10\\.\\d{4}/i.test(text);\n let refScore = hasPlaceholderRefs ? 1 : Math.min(7, uniqueRefs);\n if (hasRealAuthors) refScore = Math.min(10, refScore + 1);\n if (hasDOI) refScore = Math.min(10, refScore + 1);\n sectionScores.references = refScore;\n\n const technicalTerms = (text.match(/\\b(algorithm|theorem|proof|complexity|O\\([^)]+\\)|convergence|optimal|novel|framework)\\b/gi) || []).length;\n const hasFigures = /figure \\d|fig\\.\\s*\\d|table \\d/i.test(text);\n let novelty = wordCount > 2000 ? 4 : wordCount > 1000 ? 3 : 2;\n novelty += Math.min(3, Math.floor(technicalTerms / 5));\n if (hasFigures) novelty += 1;\n\n const hasCode = /```[\\s\\S]*?```/.test(text);\n const hasEquations = /\\$[^$]+\\$/.test(text) || /\\\\begin\\{/.test(text);\n const hasNumbers = /\\d+\\.\\d+%|\\d+\\.\\d+x|p\\s*[<>]\\s*0\\.\\d/i.test(text);\n let reproducibility = (hasCode ? 5 : 3) + (hasEquations ? 1 : 0) + (wordCount > 2000 ? 1 : 0);\n if (hasNumbers) reproducibility += 1;\n\n const citation_quality = hasPlaceholderRefs ? 1 : Math.min(6, uniqueRefs);\n\n return {\n scores: {\n ...sectionScores,\n novelty: Math.min(10, novelty),\n reproducibility: Math.min(10, reproducibility),\n citation_quality: Math.min(10, citation_quality),\n },\n provider: \"heuristic\"\n };\n}\n\n/**\n * Score a paper using heterogeneous multi-LLM swarm.\n * Tries to get 2 independent judges, falls back through provider chain.\n */\nexport async function scoreGranular(content, paperType = \"research\") {\n if (!content || content.trim().length < 50) {\n return {\n sections: Object.fromEntries(SECTIONS.map(s => [s, 0])),\n overall: 0,\n novelty: 0,\n reproducibility: 0,\n citation_quality: 0,\n judges: [],\n judge_count: 0,\n error: \"Content too short to score\"\n };\n }\n\n // All LLM judges support 32k+ context. Send enough content to include ALL 7 mandatory sections.\n // Previous limit of 4000 chars truncated papers before methodology/results/conclusion, causing\n // strict judges (Cohere, Xiaomi) to correctly score missing sections as 0.\n const truncated = content.length > 16000 ? content.substring(0, 16000) + \"\\n\\n[... truncated for scoring ...]\" : content;\n const prompt = SCORING_PROMPT + truncated;\n\n // ALL available judges score independently for maximum consensus diversity.\n // Each model evaluates each section independently, then we average.\n // Run all judges in parallel for speed (each has its own timeout).\n const judgePromises = available.map(provider =>\n callLLMForScoring(prompt, provider).catch(() => null)\n );\n const judgeResults = await Promise.all(judgePromises);\n const judges = judgeResults.filter(Boolean);\n\n // If no LLM judges succeeded, use heuristic\n if (judges.length === 0) {\n const heuristic = heuristicScore(content);\n judges.push(heuristic);\n }\n\n // Average scores across all judges\n const allFields = [...SECTIONS, \"novelty\", \"reproducibility\", \"citation_quality\"];\n const averaged = {};\n for (const field of allFields) {\n const values = judges.map(j => j.scores[field]).filter(v => typeof v === \"number\");\n averaged[field] = values.length > 0\n ? Math.round((values.reduce((a, b) => a + b, 0) / values.length) * 10) / 10\n : 0;\n }\n\n // ── CALIBRATION PASS — adjust raw averages against reference benchmarks ──\n // This is the core fix for inflated scoring. Raw LLM averages are compared\n // against quality signals extracted from the paper content and calibrated\n // against recognized reference paper fingerprints.\n let calibration = null;\n try {\n const fieldResult = detectField(content);\n const signals = extractSignals(content);\n const benchmarks = REFERENCE_BENCHMARKS[fieldResult.field] || null;\n const { calibrated, adjustments } = calibrateScores(averaged, signals, benchmarks);\n\n // Apply calibrated scores over raw averages\n const adjustmentCount = Object.keys(adjustments).length;\n if (adjustmentCount > 0) {\n for (const [field, val] of Object.entries(calibrated)) {\n if (typeof val === \"number\" && averaged[field] !== undefined) {\n averaged[field] = val;\n }\n }\n console.log(`[SCORING] Calibration applied: ${adjustmentCount} adjustments, field=${fieldResult.field}, ` +\n `red_flags=${signals.red_flag_count}, depth=${signals.depth_score}`);\n }\n\n calibration = {\n field: fieldResult.field,\n field_confidence: fieldResult.confidence,\n signals_summary: {\n word_count: signals.word_count,\n sections_present: signals.sections_present.length,\n sections_missing: signals.sections_missing,\n red_flags: signals.red_flags,\n red_flag_count: signals.red_flag_count,\n has_formal_proofs: signals.has_formal_proofs,\n has_equations: signals.has_equations,\n has_code: signals.has_code,\n unique_refs: signals.unique_refs,\n has_placeholder_refs: signals.has_placeholder_refs,\n depth_score: signals.depth_score,\n evidence_markers: signals.evidence_markers,\n deception_count: signals.deception_count || 0,\n deception_matches: (signals.deception_matches || []).map(d => ({\n id: d.id, name: d.name, severity: d.severity,\n })),\n },\n adjustments,\n adjustment_count: adjustmentCount,\n reference_papers: benchmarks ? benchmarks.references.map(r => r.title) : [],\n };\n } catch (calErr) {\n console.warn(`[SCORING] Calibration error (non-fatal): ${calErr.message}`);\n }\n\n const sectionValues = SECTIONS.map(s => averaged[s]);\n const overall = Math.round((sectionValues.reduce((a, b) => a + b, 0) / SECTIONS.length) * 10) / 10;\n\n // Per-judge detail breakdown (individual scores + feedback)\n const judge_details = judges.map(j => ({\n judge: j.provider,\n scores: j.scores,\n feedback: j.feedback || null,\n }));\n\n // Consensus score per dimension (1 = all judges agree, 0 = total disagreement)\n const consensus = {};\n for (const field of allFields) {\n const values = judges.map(j => j.scores[field]).filter(v => typeof v === \"number\");\n if (values.length < 2) { consensus[field] = 1.0; continue; }\n const mean = values.reduce((a, b) => a + b, 0) / values.length;\n const variance = values.reduce((sum, v) => sum + (v - mean) ** 2, 0) / values.length;\n const stddev = Math.sqrt(variance);\n consensus[field] = Math.round(Math.max(0, 1 - stddev / 5) * 100) / 100;\n }\n const overall_consensus = Math.round(\n (allFields.reduce((sum, f) => sum + consensus[f], 0) / allFields.length) * 100\n ) / 100;\n\n // Aggregate feedback per dimension (from all judges that provided it)\n const aggregated_feedback = {};\n for (const field of allFields) {\n const comments = judges\n .filter(j => j.feedback && j.feedback[field])\n .map(j => ({ judge: j.provider, comment: j.feedback[field] }));\n if (comments.length > 0) aggregated_feedback[field] = comments;\n }\n\n const result = {\n sections: Object.fromEntries(SECTIONS.map(s => [s, averaged[s]])),\n overall,\n novelty: averaged.novelty,\n reproducibility: averaged.reproducibility,\n citation_quality: averaged.citation_quality,\n judges: judges.map(j => j.provider),\n judge_count: judges.length,\n judge_details,\n consensus,\n overall_consensus,\n feedback: Object.keys(aggregated_feedback).length > 0 ? aggregated_feedback : null,\n scored_at: new Date().toISOString(),\n paper_type: paperType,\n calibration,\n };\n\n console.log(`[SCORING] Granular score: overall=${overall}, consensus=${overall_consensus}, judges=${result.judges.join(\",\")}`);\n return result;\n}\n"}, "6b99cb3e1147919e8706fb5b4c33f516849247ef": {"t": 1785753944.1878805, "n": 24025, "h": "dd355ec8d25c439e", "c": "/**\n * P2PCLAW Granular Scoring Service\n * =================================\n * Heterogeneous multi-LLM scoring engine that evaluates papers section-by-section.\n *\n * Provider chain (updated 2026-04-01):\n * 1. Cerebras — qwen-3-235b (3 keys, free, ultra-fast)\n * 2. Cerebras — llama3.1-8b (3 keys, free, ultra-fast)\n * 3. Mistral — mistral-small-latest (2 keys, free)\n * 4. Sarvam — sarvam-m (Indian AI, free)\n * 5. OpenRouter — qwen3-coder:free (free)\n * 6. Groq — llama-3.3-70b-versatile (7 keys, may be restricted)\n * 7. NVIDIA — meta/llama-3.3-70b-instruct (3 keys, free)\n * 8. Inception — mercury-2 (5 keys, free)\n * 9. Xiaomi MiMo — mimo-v2-flash (3 keys, free)\n * 10. Xiaomi MiMo — mimo-v2-pro (3 keys, free, reasoning)\n * 11. Cohere — command-a-reasoning (8 keys, reasoning model)\n * 12. Deterministic heuristic fallback (never blocks)\n *\n * ALL available judges score independently. Final score = average across all judges.\n * Each model evaluates each section independently for maximum consensus diversity.\n */\n\nimport { detectField, extractSignals, calibrateScores, REFERENCE_BENCHMARKS } from \"./calibrationService.js\";\n\nconst SECTIONS = [\"abstract\", \"introduction\", \"methodology\", \"results\", \"discussion\", \"conclusion\", \"references\"];\n\n// ── Load keys with rotation ─────────────────────────────────────────────────\n\nfunction loadKeys(envPrefix, maxKeys = 10) {\n const keys = [];\n for (let i = 1; i <= maxKeys; i++) {\n const k = process.env[`${envPrefix}_${i}`] || process.env[`${envPrefix}${i}`];\n if (k) keys.push(k);\n }\n // Also check single-key env vars\n const single = process.env[envPrefix];\n if (single && !keys.includes(single)) keys.unshift(single);\n return keys;\n}\n\nconst keyIndices = {};\nfunction nextKey(providerId, keys) {\n if (!keys.length) return null;\n const idx = keyIndices[providerId] || 0;\n const key = keys[idx % keys.length];\n keyIndices[providerId] = (idx + 1) % keys.length;\n return key;\n}\n\n// ── Provider definitions ────────────────────────────────────────────────────\n\nconst PROVIDERS = [\n // --- Cerebras: 3 keys x 2 models = up to 6 independent judges ---\n {\n id: \"cerebras-qwen\",\n name: \"Cerebras-Qwen235B\",\n url: \"https://api.cerebras.ai/v1/chat/completions\",\n model: \"qwen-3-235b-a22b-instruct-2507\",\n keys: loadKeys(\"CEREBRAS_API_KEY\").concat(loadKeys(\"CEREBRAS_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 1024,\n },\n {\n id: \"cerebras-llama\",\n name: \"Cerebras-Llama8B\",\n url: \"https://api.cerebras.ai/v1/chat/completions\",\n model: \"llama3.1-8b\",\n keys: loadKeys(\"CEREBRAS_API_KEY\").concat(loadKeys(\"CEREBRAS_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n // --- Mistral: 2 keys, reliable ---\n {\n id: \"mistral\",\n name: \"Mistral\",\n url: \"https://api.mistral.ai/v1/chat/completions\",\n model: \"mistral-small-latest\",\n keys: loadKeys(\"MISTRAL_API_KEY\").concat(loadKeys(\"MISTRAL_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n // --- Sarvam (Indian AI): sarvam-m, uses <think> tags (needs 2048+ tokens) ---\n {\n id: \"sarvam\",\n name: \"Sarvam\",\n url: \"https://api.sarvam.ai/v1/chat/completions\",\n model: \"sarvam-m\",\n keys: loadKeys(\"SARVAM_KEY\").concat(loadKeys(\"SARVAM_API_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 2048,\n },\n // --- OpenRouter: free models ---\n {\n id: \"openrouter\",\n name: \"OpenRouter\",\n url: \"https://openrouter.ai/api/v1/chat/completions\",\n model: \"qwen/qwen3-coder:free\",\n keys: loadKeys(\"OPENROUTER_API_KEY\"),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n extraHeaders: { \"HTTP-Referer\": \"https://www.p2pclaw.com\", \"X-Title\": \"P2PCLAW Scoring\" },\n },\n // --- Groq: 7 keys but may be org-restricted ---\n {\n id: \"groq\",\n name: \"Groq\",\n url: \"https://api.groq.com/openai/v1/chat/completions\",\n model: \"llama-3.3-70b-versatile\",\n keys: loadKeys(\"GROQ_API_KEY\").concat(loadKeys(\"GROQ_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n // --- NVIDIA: 3 keys ---\n {\n id: \"nvidia\",\n name: \"NVIDIA\",\n url: \"https://integrate.api.nvidia.com/v1/chat/completions\",\n model: \"meta/llama-3.3-70b-instruct\",\n keys: loadKeys(\"NVAPI_KEY\").concat(loadKeys(\"NVIDIA_API_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n // --- Inception: mercury-2 ---\n {\n id: \"inception\",\n name: \"Inception\",\n url: \"https://api.inceptionlabs.ai/v1/chat/completions\",\n model: \"mercury-2\",\n keys: loadKeys(\"INCEPTION_API_KEY\").concat(loadKeys(\"INCEPTION_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n // --- Xiaomi MiMo: 3 keys x 2 models = up to 2 independent judges ---\n {\n id: \"xiaomi-flash\",\n name: \"Xiaomi-MiMo-Flash\",\n url: \"https://api.xiaomimimo.com/v1/chat/completions\",\n model: \"mimo-v2-flash\",\n keys: loadKeys(\"XIAOMI_API_KEY\").concat(loadKeys(\"XIAOMI_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n {\n id: \"xiaomi-pro\",\n name: \"Xiaomi-MiMo-Pro\",\n url: \"https://api.xiaomimimo.com/v1/chat/completions\",\n model: \"mimo-v2-pro\",\n keys: loadKeys(\"XIAOMI_API_KEY\").concat(loadKeys(\"XIAOMI_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 1024,\n },\n // --- Cohere: command-a-reasoning model (8 keys, reasoning/thinking model) ---\n {\n id: \"cohere\",\n name: \"Cohere-CommandA\",\n url: \"https://api.cohere.com/v2/chat\",\n model: \"command-a-reasoning-08-2025\",\n keys: loadKeys(\"COHERE_API_KEY\").concat(loadKeys(\"COHERE_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 4096, // reasoning model needs extra tokens for thinking + answer\n responseFormat: \"cohere\", // data.message.content[] array with {type:\"thinking\"} + {type:\"text\"}\n timeout: 90000, // 90s — reasoning model needs time to think through all 10 dimensions\n },\n];\n\n// Deduplicate keys within each provider\nfor (const p of PROVIDERS) {\n p.keys = [...new Set(p.keys)].filter(Boolean);\n}\n\n// Log available providers\nconst available = PROVIDERS.filter(p => p.keys.length > 0);\nconsole.log(`[SCORING] ${available.length} LLM providers available: ${available.map(p => `${p.name}(${p.keys.length})`).join(\", \")}`);\nif (available.length === 0) console.warn(\"[SCORING] No LLM providers — heuristic scoring only.\");\n// Debug: log specifically which providers have NO keys\nconst unavailable = PROVIDERS.filter(p => p.keys.length === 0);\nif (unavailable.length > 0) console.log(`[SCORING] Providers with NO keys: ${unavailable.map(p => p.name).join(\", \")}`);\n\nconst SCORING_PROMPT = `You are an academic paper quality evaluator for the P2PCLAW decentralized research network.\n\nScore this paper on a 0-10 scale for each criterion. For EACH score, include a brief \"why\" (1 sentence explaining the score).\n\nScoring criteria:\n- abstract (0-10): Quality, clarity, and completeness of the abstract. Clear problem statement, scope, and key results summarized.\n- introduction (0-10): Problem statement clarity, context, motivation. Should cite 2+ related works.\n- methodology (0-10): Rigor, reproducibility, appropriate methods. Must describe steps clearly enough to reproduce.\n- results (0-10): Quality of findings, data presentation, significance. Quantitative data strongly preferred.\n- discussion (0-10): Interpretation depth, limitations acknowledged, implications for the field.\n- conclusion (0-10): Summary of key findings (expected, NOT penalized as repetition), concrete future work directions, and connection back to the research question. A good conclusion SHOULD summarize findings — that is standard scientific practice.\n- references (0-10): Citation quality, relevance, proper formatting (0 if no real references). Real author names, titles, years, DOIs/URLs expected.\n- novelty (0-10): Original contribution to the field. Novel frameworks, terminology, or approaches.\n- reproducibility (0-10): Could another researcher reproduce this work? Code, equations, detailed parameters help.\n- citation_quality (0-10): Are references real, relevant, and properly cited? 8+ unique real citations expected for high scores.\n\nIMPORTANT RULES:\n- If a section is MISSING entirely, score it 0\n- If references are placeholder/fake (e.g. \"[1] Author, A. (2026). Placeholder\"), score references as 1\n- Trivial proofs or papers under 300 words should score below 3 overall\n- Papers that prove obvious things (like 0*0=0) should score novelty as 0-1\n- Be STRICT — a score of 8+ means genuinely publishable quality\n- Lean 4 verified papers with valid proof hashes deserve reproducibility bonus\n\nReturn ONLY this JSON format (numbers 0-10, nothing else):\n{\"abstract\":N,\"introduction\":N,\"methodology\":N,\"results\":N,\"discussion\":N,\"conclusion\":N,\"references\":N,\"novelty\":N,\"reproducibility\":N,\"citation_quality\":N}\n\nPaper content:\n`;\n\n/**\n * Call a single LLM provider to score a paper.\n * Tries up to 2 keys from the provider before giving up.\n */\nasync function callLLMForScoring(prompt, provider) {\n if (!provider.keys.length) return null;\n\n const maxAttempts = Math.min(provider.keys.length, 2);\n for (let attempt = 0; attempt < maxAttempts; attempt++) {\n const key = nextKey(provider.id, provider.keys);\n if (!key) return null;\n\n const headers = { \"Content-Type\": \"application/json\" };\n headers[provider.authHeader] = `${provider.authPrefix}${key}`;\n if (provider.extraHeaders) Object.assign(headers, provider.extraHeaders);\n\n try {\n const res = await fetch(provider.url, {\n method: \"POST\",\n headers,\n body: JSON.stringify({\n model: provider.model,\n messages: [{ role: \"user\", content: prompt }],\n max_tokens: provider.maxTokens || 512,\n temperature: 0.1,\n }),\n signal: AbortSignal.timeout(provider.timeout || 30000),\n });\n\n if (res.status === 429) {\n console.warn(`[SCORING] ${provider.name} 429 rate-limited`);\n await new Promise(r => setTimeout(r, 3000));\n continue;\n }\n if (res.status === 402) {\n console.warn(`[SCORING] ${provider.name} 402 credits exhausted — skipping provider`);\n return null;\n }\n if (!res.ok) {\n const errBody = await res.text().catch(() => \"\");\n console.warn(`[SCORING] ${provider.name} HTTP ${res.status}: ${errBody.substring(0, 150)}`);\n continue;\n }\n\n const data = await res.json();\n // Support both OpenAI format (choices[0].message.content) and Cohere v2 (message.content[] array)\n let text = \"\";\n if (provider.responseFormat === \"cohere\") {\n // Cohere v2 returns array of content blocks: [{type:\"thinking\",...}, {type:\"text\",text:\"...\"}]\n const blocks = data.message?.content || [];\n if (Array.isArray(blocks)) {\n const textBlock = blocks.find(b => b.type === \"text\");\n text = textBlock?.text || blocks[blocks.length - 1]?.text || \"\";\n } else {\n text = typeof blocks === \"string\" ? blocks : \"\";\n }\n } else {\n text = data.choices?.[0]?.message?.content || \"\";\n }\n\n // Strip <think>...</think> tags (Sarvam, Qwen with thinking mode)\n if (provider.stripThinkTags || text.includes(\"<think>\")) {\n // Strip closed think blocks\n text = text.replace(/<think>[\\s\\S]*?<\\/think>/g, \"\").trim();\n // Strip unclosed think blocks (model ran out of tokens mid-think)\n if (text.startsWith(\"<think>\")) {\n text = text.replace(/<think>[\\s\\S]*/g, \"\").trim();\n }\n }\n\n // Extract JSON — use balanced braces to handle nested objects like {\"score\":8,\"why\":\"...\"}\n let jsonMatch = null;\n const firstBrace = text.indexOf('{');\n if (firstBrace !== -1) {\n let depth = 0, end = -1;\n for (let i = firstBrace; i < text.length; i++) {\n if (text[i] === '{') depth++;\n else if (text[i] === '}') { depth--; if (depth === 0) { end = i; break; } }\n }\n if (end !== -1) jsonMatch = [text.substring(firstBrace, end + 1)];\n }\n if (!jsonMatch) {\n console.warn(`[SCORING] ${provider.name} — no JSON in response`);\n continue;\n }\n\n const parsed = JSON.parse(jsonMatch[0]);\n const fields = [...SECTIONS, \"novelty\", \"reproducibility\", \"citation_quality\"];\n const feedback = {};\n for (const field of fields) {\n let val = parsed[field];\n // Support both plain number and {score, why} object format\n if (val && typeof val === \"object\" && typeof val.score === \"number\") {\n feedback[field] = val.why || null;\n val = val.score;\n }\n if (typeof val !== \"number\" || val < 0 || val > 10) {\n val = typeof val === \"number\" ? Math.max(0, Math.min(10, Math.round(val))) : 5;\n }\n parsed[field] = val;\n }\n\n // Clean parsed: only keep recognized score fields (remove stray \"why\" etc.)\n const cleanScores = {};\n for (const field of fields) cleanScores[field] = parsed[field];\n\n console.log(`[SCORING] ${provider.name} scored successfully`);\n return { scores: cleanScores, provider: provider.name, feedback: Object.keys(feedback).length > 0 ? feedback : null };\n } catch (e) {\n console.warn(`[SCORING] ${provider.name} error: ${e.message}`);\n }\n }\n return null;\n}\n\n/**\n * Deterministic heuristic scoring — used when all LLMs fail.\n */\nfunction heuristicScore(content) {\n const text = content || \"\";\n const words = text.split(/\\s+/).filter(w => w.length > 0);\n const wordCount = words.length;\n const lowerText = text.toLowerCase();\n\n const sectionScores = {};\n for (const section of SECTIONS) {\n const hasSection = lowerText.includes(`## ${section}`) || lowerText.includes(`# ${section}`);\n if (!hasSection) {\n sectionScores[section] = 0;\n continue;\n }\n const sectionRegex = new RegExp(`##?\\\\s*${section}[\\\\s\\\\S]*?(?=##?\\\\s|$)`, \"i\");\n const match = text.match(sectionRegex);\n const sectionWords = match ? match[0].split(/\\s+/).length : 0;\n if (sectionWords < 20) sectionScores[section] = 2;\n else if (sectionWords < 50) sectionScores[section] = 4;\n else if (sectionWords < 100) sectionScores[section] = 5;\n else if (sectionWords < 200) sectionScores[section] = 6;\n else sectionScores[section] = 7;\n }\n\n const refMatches = text.match(/\\[\\d+\\]/g) || [];\n const uniqueRefs = new Set(refMatches).size;\n const hasPlaceholderRefs = /placeholder|author,?\\s*a\\.\\s*\\(\\d{4}\\)/i.test(text);\n const hasRealAuthors = /[A-Z][a-z]+,\\s*[A-Z]\\.\\s*(?:&|,|et al)/g.test(text);\n const hasDOI = /doi\\.org|arxiv\\.org|10\\.\\d{4}/i.test(text);\n let refScore = hasPlaceholderRefs ? 1 : Math.min(7, uniqueRefs);\n if (hasRealAuthors) refScore = Math.min(10, refScore + 1);\n if (hasDOI) refScore = Math.min(10, refScore + 1);\n sectionScores.references = refScore;\n\n const technicalTerms = (text.match(/\\b(algorithm|theorem|proof|complexity|O\\([^)]+\\)|convergence|optimal|novel|framework)\\b/gi) || []).length;\n const hasFigures = /figure \\d|fig\\.\\s*\\d|table \\d/i.test(text);\n let novelty = wordCount > 2000 ? 4 : wordCount > 1000 ? 3 : 2;\n novelty += Math.min(3, Math.floor(technicalTerms / 5));\n if (hasFigures) novelty += 1;\n\n const hasCode = /```[\\s\\S]*?```/.test(text);\n const hasEquations = /\\$[^$]+\\$/.test(text) || /\\\\begin\\{/.test(text);\n const hasNumbers = /\\d+\\.\\d+%|\\d+\\.\\d+x|p\\s*[<>]\\s*0\\.\\d/i.test(text);\n let reproducibility = (hasCode ? 5 : 3) + (hasEquations ? 1 : 0) + (wordCount > 2000 ? 1 : 0);\n if (hasNumbers) reproducibility += 1;\n\n const citation_quality = hasPlaceholderRefs ? 1 : Math.min(6, uniqueRefs);\n\n return {\n scores: {\n ...sectionScores,\n novelty: Math.min(10, novelty),\n reproducibility: Math.min(10, reproducibility),\n citation_quality: Math.min(10, citation_quality),\n },\n provider: \"heuristic\"\n };\n}\n\n/**\n * Score a paper using heterogeneous multi-LLM swarm.\n * Tries to get 2 independent judges, falls back through provider chain.\n */\nexport async function scoreGranular(content, paperType = \"research\") {\n if (!content || content.trim().length < 50) {\n return {\n sections: Object.fromEntries(SECTIONS.map(s => [s, 0])),\n overall: 0,\n novelty: 0,\n reproducibility: 0,\n citation_quality: 0,\n judges: [],\n judge_count: 0,\n error: \"Content too short to score\"\n };\n }\n\n // All LLM judges support 32k+ context. Send enough content to include ALL 7 mandatory sections.\n // Previous limit of 4000 chars truncated papers before methodology/results/conclusion, causing\n // strict judges (Cohere, Xiaomi) to correctly score missing sections as 0.\n const truncated = content.length > 16000 ? content.substring(0, 16000) + \"\\n\\n[... truncated for scoring ...]\" : content;\n const prompt = SCORING_PROMPT + truncated;\n\n // ALL available judges score independently for maximum consensus diversity.\n // Each model evaluates each section independently, then we average.\n // Run all judges in parallel for speed (each has its own timeout).\n const judgePromises = available.map(provider =>\n callLLMForScoring(prompt, provider).catch(() => null)\n );\n const judgeResults = await Promise.all(judgePromises);\n const judges = judgeResults.filter(Boolean);\n\n // If no LLM judges succeeded, use heuristic\n if (judges.length === 0) {\n const heuristic = heuristicScore(content);\n judges.push(heuristic);\n }\n\n // Average scores across all judges\n const allFields = [...SECTIONS, \"novelty\", \"reproducibility\", \"citation_quality\"];\n const averaged = {};\n for (const field of allFields) {\n const values = judges.map(j => j.scores[field]).filter(v => typeof v === \"number\");\n averaged[field] = values.length > 0\n ? Math.round((values.reduce((a, b) => a + b, 0) / values.length) * 10) / 10\n : 0;\n }\n\n // ── CALIBRATION PASS — adjust raw averages against reference benchmarks ──\n // This is the core fix for inflated scoring. Raw LLM averages are compared\n // against quality signals extracted from the paper content and calibrated\n // against recognized reference paper fingerprints.\n let calibration = null;\n try {\n const fieldResult = detectField(content);\n const signals = extractSignals(content);\n const benchmarks = REFERENCE_BENCHMARKS[fieldResult.field] || null;\n const { calibrated, adjustments } = calibrateScores(averaged, signals, benchmarks);\n\n // Apply calibrated scores over raw averages\n const adjustmentCount = Object.keys(adjustments).length;\n if (adjustmentCount > 0) {\n for (const [field, val] of Object.entries(calibrated)) {\n if (typeof val === \"number\" && averaged[field] !== undefined) {\n averaged[field] = val;\n }\n }\n console.log(`[SCORING] Calibration applied: ${adjustmentCount} adjustments, field=${fieldResult.field}, ` +\n `red_flags=${signals.red_flag_count}, depth=${signals.depth_score}`);\n }\n\n calibration = {\n field: fieldResult.field,\n field_confidence: fieldResult.confidence,\n signals_summary: {\n word_count: signals.word_count,\n sections_present: signals.sections_present.length,\n sections_missing: signals.sections_missing,\n red_flags: signals.red_flags,\n red_flag_count: signals.red_flag_count,\n has_formal_proofs: signals.has_formal_proofs,\n has_equations: signals.has_equations,\n has_code: signals.has_code,\n unique_refs: signals.unique_refs,\n has_placeholder_refs: signals.has_placeholder_refs,\n depth_score: signals.depth_score,\n evidence_markers: signals.evidence_markers,\n },\n adjustments,\n adjustment_count: adjustmentCount,\n reference_papers: benchmarks ? benchmarks.references.map(r => r.title) : [],\n };\n } catch (calErr) {\n console.warn(`[SCORING] Calibration error (non-fatal): ${calErr.message}`);\n }\n\n const sectionValues = SECTIONS.map(s => averaged[s]);\n const overall = Math.round((sectionValues.reduce((a, b) => a + b, 0) / SECTIONS.length) * 10) / 10;\n\n // Per-judge detail breakdown (individual scores + feedback)\n const judge_details = judges.map(j => ({\n judge: j.provider,\n scores: j.scores,\n feedback: j.feedback || null,\n }));\n\n // Consensus score per dimension (1 = all judges agree, 0 = total disagreement)\n const consensus = {};\n for (const field of allFields) {\n const values = judges.map(j => j.scores[field]).filter(v => typeof v === \"number\");\n if (values.length < 2) { consensus[field] = 1.0; continue; }\n const mean = values.reduce((a, b) => a + b, 0) / values.length;\n const variance = values.reduce((sum, v) => sum + (v - mean) ** 2, 0) / values.length;\n const stddev = Math.sqrt(variance);\n consensus[field] = Math.round(Math.max(0, 1 - stddev / 5) * 100) / 100;\n }\n const overall_consensus = Math.round(\n (allFields.reduce((sum, f) => sum + consensus[f], 0) / allFields.length) * 100\n ) / 100;\n\n // Aggregate feedback per dimension (from all judges that provided it)\n const aggregated_feedback = {};\n for (const field of allFields) {\n const comments = judges\n .filter(j => j.feedback && j.feedback[field])\n .map(j => ({ judge: j.provider, comment: j.feedback[field] }));\n if (comments.length > 0) aggregated_feedback[field] = comments;\n }\n\n const result = {\n sections: Object.fromEntries(SECTIONS.map(s => [s, averaged[s]])),\n overall,\n novelty: averaged.novelty,\n reproducibility: averaged.reproducibility,\n citation_quality: averaged.citation_quality,\n judges: judges.map(j => j.provider),\n judge_count: judges.length,\n judge_details,\n consensus,\n overall_consensus,\n feedback: Object.keys(aggregated_feedback).length > 0 ? aggregated_feedback : null,\n scored_at: new Date().toISOString(),\n paper_type: paperType,\n calibration,\n };\n\n console.log(`[SCORING] Granular score: overall=${overall}, consensus=${overall_consensus}, judges=${result.judges.join(\",\")}`);\n return result;\n}\n"}, "d7dd9bb3cfeaba8a314c87f640666275b78b7bd6": {"t": 1785753946.3827088, "n": 21584, "h": "1a635ac974021168", "c": "/**\n * P2PCLAW Granular Scoring Service\n * =================================\n * Heterogeneous multi-LLM scoring engine that evaluates papers section-by-section.\n *\n * Provider chain (updated 2026-04-01):\n * 1. Cerebras — qwen-3-235b (3 keys, free, ultra-fast)\n * 2. Cerebras — llama3.1-8b (3 keys, free, ultra-fast)\n * 3. Mistral — mistral-small-latest (2 keys, free)\n * 4. Sarvam — sarvam-m (Indian AI, free)\n * 5. OpenRouter — qwen3-coder:free (free)\n * 6. Groq — llama-3.3-70b-versatile (7 keys, may be restricted)\n * 7. NVIDIA — meta/llama-3.3-70b-instruct (3 keys, free)\n * 8. Inception — mercury-2 (5 keys, free)\n * 9. Xiaomi MiMo — mimo-v2-flash (3 keys, free)\n * 10. Xiaomi MiMo — mimo-v2-pro (3 keys, free, reasoning)\n * 11. Cohere — command-a-reasoning (8 keys, reasoning model)\n * 12. Deterministic heuristic fallback (never blocks)\n *\n * ALL available judges score independently. Final score = average across all judges.\n * Each model evaluates each section independently for maximum consensus diversity.\n */\n\nconst SECTIONS = [\"abstract\", \"introduction\", \"methodology\", \"results\", \"discussion\", \"conclusion\", \"references\"];\n\n// ── Load keys with rotation ─────────────────────────────────────────────────\n\nfunction loadKeys(envPrefix, maxKeys = 10) {\n const keys = [];\n for (let i = 1; i <= maxKeys; i++) {\n const k = process.env[`${envPrefix}_${i}`] || process.env[`${envPrefix}${i}`];\n if (k) keys.push(k);\n }\n // Also check single-key env vars\n const single = process.env[envPrefix];\n if (single && !keys.includes(single)) keys.unshift(single);\n return keys;\n}\n\nconst keyIndices = {};\nfunction nextKey(providerId, keys) {\n if (!keys.length) return null;\n const idx = keyIndices[providerId] || 0;\n const key = keys[idx % keys.length];\n keyIndices[providerId] = (idx + 1) % keys.length;\n return key;\n}\n\n// ── Provider definitions ────────────────────────────────────────────────────\n\nconst PROVIDERS = [\n // --- Cerebras: 3 keys x 2 models = up to 6 independent judges ---\n {\n id: \"cerebras-qwen\",\n name: \"Cerebras-Qwen235B\",\n url: \"https://api.cerebras.ai/v1/chat/completions\",\n model: \"qwen-3-235b-a22b-instruct-2507\",\n keys: loadKeys(\"CEREBRAS_API_KEY\").concat(loadKeys(\"CEREBRAS_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 1024,\n },\n {\n id: \"cerebras-llama\",\n name: \"Cerebras-Llama8B\",\n url: \"https://api.cerebras.ai/v1/chat/completions\",\n model: \"llama3.1-8b\",\n keys: loadKeys(\"CEREBRAS_API_KEY\").concat(loadKeys(\"CEREBRAS_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n // --- Mistral: 2 keys, reliable ---\n {\n id: \"mistral\",\n name: \"Mistral\",\n url: \"https://api.mistral.ai/v1/chat/completions\",\n model: \"mistral-small-latest\",\n keys: loadKeys(\"MISTRAL_API_KEY\").concat(loadKeys(\"MISTRAL_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n // --- Sarvam (Indian AI): sarvam-m, uses <think> tags (needs 2048+ tokens) ---\n {\n id: \"sarvam\",\n name: \"Sarvam\",\n url: \"https://api.sarvam.ai/v1/chat/completions\",\n model: \"sarvam-m\",\n keys: loadKeys(\"SARVAM_KEY\").concat(loadKeys(\"SARVAM_API_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 2048,\n },\n // --- OpenRouter: free models ---\n {\n id: \"openrouter\",\n name: \"OpenRouter\",\n url: \"https://openrouter.ai/api/v1/chat/completions\",\n model: \"qwen/qwen3-coder:free\",\n keys: loadKeys(\"OPENROUTER_API_KEY\"),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n extraHeaders: { \"HTTP-Referer\": \"https://www.p2pclaw.com\", \"X-Title\": \"P2PCLAW Scoring\" },\n },\n // --- Groq: 7 keys but may be org-restricted ---\n {\n id: \"groq\",\n name: \"Groq\",\n url: \"https://api.groq.com/openai/v1/chat/completions\",\n model: \"llama-3.3-70b-versatile\",\n keys: loadKeys(\"GROQ_API_KEY\").concat(loadKeys(\"GROQ_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n // --- NVIDIA: 3 keys ---\n {\n id: \"nvidia\",\n name: \"NVIDIA\",\n url: \"https://integrate.api.nvidia.com/v1/chat/completions\",\n model: \"meta/llama-3.3-70b-instruct\",\n keys: loadKeys(\"NVAPI_KEY\").concat(loadKeys(\"NVIDIA_API_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n // --- Inception: mercury-2 ---\n {\n id: \"inception\",\n name: \"Inception\",\n url: \"https://api.inceptionlabs.ai/v1/chat/completions\",\n model: \"mercury-2\",\n keys: loadKeys(\"INCEPTION_API_KEY\").concat(loadKeys(\"INCEPTION_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n // --- Xiaomi MiMo: 3 keys x 2 models = up to 2 independent judges ---\n {\n id: \"xiaomi-flash\",\n name: \"Xiaomi-MiMo-Flash\",\n url: \"https://api.xiaomimimo.com/v1/chat/completions\",\n model: \"mimo-v2-flash\",\n keys: loadKeys(\"XIAOMI_API_KEY\").concat(loadKeys(\"XIAOMI_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n {\n id: \"xiaomi-pro\",\n name: \"Xiaomi-MiMo-Pro\",\n url: \"https://api.xiaomimimo.com/v1/chat/completions\",\n model: \"mimo-v2-pro\",\n keys: loadKeys(\"XIAOMI_API_KEY\").concat(loadKeys(\"XIAOMI_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 1024,\n },\n // --- Cohere: command-a-reasoning model (8 keys, reasoning/thinking model) ---\n {\n id: \"cohere\",\n name: \"Cohere-CommandA\",\n url: \"https://api.cohere.com/v2/chat\",\n model: \"command-a-reasoning-08-2025\",\n keys: loadKeys(\"COHERE_API_KEY\").concat(loadKeys(\"COHERE_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 4096, // reasoning model needs extra tokens for thinking + answer\n responseFormat: \"cohere\", // data.message.content[] array with {type:\"thinking\"} + {type:\"text\"}\n timeout: 90000, // 90s — reasoning model needs time to think through all 10 dimensions\n },\n];\n\n// Deduplicate keys within each provider\nfor (const p of PROVIDERS) {\n p.keys = [...new Set(p.keys)].filter(Boolean);\n}\n\n// Log available providers\nconst available = PROVIDERS.filter(p => p.keys.length > 0);\nconsole.log(`[SCORING] ${available.length} LLM providers available: ${available.map(p => `${p.name}(${p.keys.length})`).join(\", \")}`);\nif (available.length === 0) console.warn(\"[SCORING] No LLM providers — heuristic scoring only.\");\n// Debug: log specifically which providers have NO keys\nconst unavailable = PROVIDERS.filter(p => p.keys.length === 0);\nif (unavailable.length > 0) console.log(`[SCORING] Providers with NO keys: ${unavailable.map(p => p.name).join(\", \")}`);\n\nconst SCORING_PROMPT = `You are an academic paper quality evaluator for the P2PCLAW decentralized research network.\n\nScore this paper on a 0-10 scale for each criterion. For EACH score, include a brief \"why\" (1 sentence explaining the score).\n\nScoring criteria:\n- abstract (0-10): Quality, clarity, and completeness of the abstract. Clear problem statement, scope, and key results summarized.\n- introduction (0-10): Problem statement clarity, context, motivation. Should cite 2+ related works.\n- methodology (0-10): Rigor, reproducibility, appropriate methods. Must describe steps clearly enough to reproduce.\n- results (0-10): Quality of findings, data presentation, significance. Quantitative data strongly preferred.\n- discussion (0-10): Interpretation depth, limitations acknowledged, implications for the field.\n- conclusion (0-10): Summary of key findings (expected, NOT penalized as repetition), concrete future work directions, and connection back to the research question. A good conclusion SHOULD summarize findings — that is standard scientific practice.\n- references (0-10): Citation quality, relevance, proper formatting (0 if no real references). Real author names, titles, years, DOIs/URLs expected.\n- novelty (0-10): Original contribution to the field. Novel frameworks, terminology, or approaches.\n- reproducibility (0-10): Could another researcher reproduce this work? Code, equations, detailed parameters help.\n- citation_quality (0-10): Are references real, relevant, and properly cited? 8+ unique real citations expected for high scores.\n\nIMPORTANT RULES:\n- If a section is MISSING entirely, score it 0\n- If references are placeholder/fake (e.g. \"[1] Author, A. (2026). Placeholder\"), score references as 1\n- Trivial proofs or papers under 300 words should score below 3 overall\n- Papers that prove obvious things (like 0*0=0) should score novelty as 0-1\n- Be STRICT — a score of 8+ means genuinely publishable quality\n- Lean 4 verified papers with valid proof hashes deserve reproducibility bonus\n\nReturn ONLY this JSON format (numbers 0-10, nothing else):\n{\"abstract\":N,\"introduction\":N,\"methodology\":N,\"results\":N,\"discussion\":N,\"conclusion\":N,\"references\":N,\"novelty\":N,\"reproducibility\":N,\"citation_quality\":N}\n\nPaper content:\n`;\n\n/**\n * Call a single LLM provider to score a paper.\n * Tries up to 2 keys from the provider before giving up.\n */\nasync function callLLMForScoring(prompt, provider) {\n if (!provider.keys.length) return null;\n\n const maxAttempts = Math.min(provider.keys.length, 2);\n for (let attempt = 0; attempt < maxAttempts; attempt++) {\n const key = nextKey(provider.id, provider.keys);\n if (!key) return null;\n\n const headers = { \"Content-Type\": \"application/json\" };\n headers[provider.authHeader] = `${provider.authPrefix}${key}`;\n if (provider.extraHeaders) Object.assign(headers, provider.extraHeaders);\n\n try {\n const res = await fetch(provider.url, {\n method: \"POST\",\n headers,\n body: JSON.stringify({\n model: provider.model,\n messages: [{ role: \"user\", content: prompt }],\n max_tokens: provider.maxTokens || 512,\n temperature: 0.1,\n }),\n signal: AbortSignal.timeout(provider.timeout || 30000),\n });\n\n if (res.status === 429) {\n console.warn(`[SCORING] ${provider.name} 429 rate-limited`);\n await new Promise(r => setTimeout(r, 3000));\n continue;\n }\n if (res.status === 402) {\n console.warn(`[SCORING] ${provider.name} 402 credits exhausted — skipping provider`);\n return null;\n }\n if (!res.ok) {\n const errBody = await res.text().catch(() => \"\");\n console.warn(`[SCORING] ${provider.name} HTTP ${res.status}: ${errBody.substring(0, 150)}`);\n continue;\n }\n\n const data = await res.json();\n // Support both OpenAI format (choices[0].message.content) and Cohere v2 (message.content[] array)\n let text = \"\";\n if (provider.responseFormat === \"cohere\") {\n // Cohere v2 returns array of content blocks: [{type:\"thinking\",...}, {type:\"text\",text:\"...\"}]\n const blocks = data.message?.content || [];\n if (Array.isArray(blocks)) {\n const textBlock = blocks.find(b => b.type === \"text\");\n text = textBlock?.text || blocks[blocks.length - 1]?.text || \"\";\n } else {\n text = typeof blocks === \"string\" ? blocks : \"\";\n }\n } else {\n text = data.choices?.[0]?.message?.content || \"\";\n }\n\n // Strip <think>...</think> tags (Sarvam, Qwen with thinking mode)\n if (provider.stripThinkTags || text.includes(\"<think>\")) {\n // Strip closed think blocks\n text = text.replace(/<think>[\\s\\S]*?<\\/think>/g, \"\").trim();\n // Strip unclosed think blocks (model ran out of tokens mid-think)\n if (text.startsWith(\"<think>\")) {\n text = text.replace(/<think>[\\s\\S]*/g, \"\").trim();\n }\n }\n\n // Extract JSON — use balanced braces to handle nested objects like {\"score\":8,\"why\":\"...\"}\n let jsonMatch = null;\n const firstBrace = text.indexOf('{');\n if (firstBrace !== -1) {\n let depth = 0, end = -1;\n for (let i = firstBrace; i < text.length; i++) {\n if (text[i] === '{') depth++;\n else if (text[i] === '}') { depth--; if (depth === 0) { end = i; break; } }\n }\n if (end !== -1) jsonMatch = [text.substring(firstBrace, end + 1)];\n }\n if (!jsonMatch) {\n console.warn(`[SCORING] ${provider.name} — no JSON in response`);\n continue;\n }\n\n const parsed = JSON.parse(jsonMatch[0]);\n const fields = [...SECTIONS, \"novelty\", \"reproducibility\", \"citation_quality\"];\n const feedback = {};\n for (const field of fields) {\n let val = parsed[field];\n // Support both plain number and {score, why} object format\n if (val && typeof val === \"object\" && typeof val.score === \"number\") {\n feedback[field] = val.why || null;\n val = val.score;\n }\n if (typeof val !== \"number\" || val < 0 || val > 10) {\n val = typeof val === \"number\" ? Math.max(0, Math.min(10, Math.round(val))) : 5;\n }\n parsed[field] = val;\n }\n\n // Clean parsed: only keep recognized score fields (remove stray \"why\" etc.)\n const cleanScores = {};\n for (const field of fields) cleanScores[field] = parsed[field];\n\n console.log(`[SCORING] ${provider.name} scored successfully`);\n return { scores: cleanScores, provider: provider.name, feedback: Object.keys(feedback).length > 0 ? feedback : null };\n } catch (e) {\n console.warn(`[SCORING] ${provider.name} error: ${e.message}`);\n }\n }\n return null;\n}\n\n/**\n * Deterministic heuristic scoring — used when all LLMs fail.\n */\nfunction heuristicScore(content) {\n const text = content || \"\";\n const words = text.split(/\\s+/).filter(w => w.length > 0);\n const wordCount = words.length;\n const lowerText = text.toLowerCase();\n\n const sectionScores = {};\n for (const section of SECTIONS) {\n const hasSection = lowerText.includes(`## ${section}`) || lowerText.includes(`# ${section}`);\n if (!hasSection) {\n sectionScores[section] = 0;\n continue;\n }\n const sectionRegex = new RegExp(`##?\\\\s*${section}[\\\\s\\\\S]*?(?=##?\\\\s|$)`, \"i\");\n const match = text.match(sectionRegex);\n const sectionWords = match ? match[0].split(/\\s+/).length : 0;\n if (sectionWords < 20) sectionScores[section] = 2;\n else if (sectionWords < 50) sectionScores[section] = 4;\n else if (sectionWords < 100) sectionScores[section] = 5;\n else if (sectionWords < 200) sectionScores[section] = 6;\n else sectionScores[section] = 7;\n }\n\n const refMatches = text.match(/\\[\\d+\\]/g) || [];\n const uniqueRefs = new Set(refMatches).size;\n const hasPlaceholderRefs = /placeholder|author,?\\s*a\\.\\s*\\(\\d{4}\\)/i.test(text);\n const hasRealAuthors = /[A-Z][a-z]+,\\s*[A-Z]\\.\\s*(?:&|,|et al)/g.test(text);\n const hasDOI = /doi\\.org|arxiv\\.org|10\\.\\d{4}/i.test(text);\n let refScore = hasPlaceholderRefs ? 1 : Math.min(7, uniqueRefs);\n if (hasRealAuthors) refScore = Math.min(10, refScore + 1);\n if (hasDOI) refScore = Math.min(10, refScore + 1);\n sectionScores.references = refScore;\n\n const technicalTerms = (text.match(/\\b(algorithm|theorem|proof|complexity|O\\([^)]+\\)|convergence|optimal|novel|framework)\\b/gi) || []).length;\n const hasFigures = /figure \\d|fig\\.\\s*\\d|table \\d/i.test(text);\n let novelty = wordCount > 2000 ? 4 : wordCount > 1000 ? 3 : 2;\n novelty += Math.min(3, Math.floor(technicalTerms / 5));\n if (hasFigures) novelty += 1;\n\n const hasCode = /```[\\s\\S]*?```/.test(text);\n const hasEquations = /\\$[^$]+\\$/.test(text) || /\\\\begin\\{/.test(text);\n const hasNumbers = /\\d+\\.\\d+%|\\d+\\.\\d+x|p\\s*[<>]\\s*0\\.\\d/i.test(text);\n let reproducibility = (hasCode ? 5 : 3) + (hasEquations ? 1 : 0) + (wordCount > 2000 ? 1 : 0);\n if (hasNumbers) reproducibility += 1;\n\n const citation_quality = hasPlaceholderRefs ? 1 : Math.min(6, uniqueRefs);\n\n return {\n scores: {\n ...sectionScores,\n novelty: Math.min(10, novelty),\n reproducibility: Math.min(10, reproducibility),\n citation_quality: Math.min(10, citation_quality),\n },\n provider: \"heuristic\"\n };\n}\n\n/**\n * Score a paper using heterogeneous multi-LLM swarm.\n * Tries to get 2 independent judges, falls back through provider chain.\n */\nexport async function scoreGranular(content, paperType = \"research\") {\n if (!content || content.trim().length < 50) {\n return {\n sections: Object.fromEntries(SECTIONS.map(s => [s, 0])),\n overall: 0,\n novelty: 0,\n reproducibility: 0,\n citation_quality: 0,\n judges: [],\n judge_count: 0,\n error: \"Content too short to score\"\n };\n }\n\n // All LLM judges support 32k+ context. Send enough content to include ALL 7 mandatory sections.\n // Previous limit of 4000 chars truncated papers before methodology/results/conclusion, causing\n // strict judges (Cohere, Xiaomi) to correctly score missing sections as 0.\n const truncated = content.length > 16000 ? content.substring(0, 16000) + \"\\n\\n[... truncated for scoring ...]\" : content;\n const prompt = SCORING_PROMPT + truncated;\n\n // ALL available judges score independently for maximum consensus diversity.\n // Each model evaluates each section independently, then we average.\n // Run all judges in parallel for speed (each has its own timeout).\n const judgePromises = available.map(provider =>\n callLLMForScoring(prompt, provider).catch(() => null)\n );\n const judgeResults = await Promise.all(judgePromises);\n const judges = judgeResults.filter(Boolean);\n\n // If no LLM judges succeeded, use heuristic\n if (judges.length === 0) {\n const heuristic = heuristicScore(content);\n judges.push(heuristic);\n }\n\n // Average scores across all judges\n const allFields = [...SECTIONS, \"novelty\", \"reproducibility\", \"citation_quality\"];\n const averaged = {};\n for (const field of allFields) {\n const values = judges.map(j => j.scores[field]).filter(v => typeof v === \"number\");\n averaged[field] = values.length > 0\n ? Math.round((values.reduce((a, b) => a + b, 0) / values.length) * 10) / 10\n : 0;\n }\n\n const sectionValues = SECTIONS.map(s => averaged[s]);\n const overall = Math.round((sectionValues.reduce((a, b) => a + b, 0) / SECTIONS.length) * 10) / 10;\n\n // Per-judge detail breakdown (individual scores + feedback)\n const judge_details = judges.map(j => ({\n judge: j.provider,\n scores: j.scores,\n feedback: j.feedback || null,\n }));\n\n // Consensus score per dimension (1 = all judges agree, 0 = total disagreement)\n const consensus = {};\n for (const field of allFields) {\n const values = judges.map(j => j.scores[field]).filter(v => typeof v === \"number\");\n if (values.length < 2) { consensus[field] = 1.0; continue; }\n const mean = values.reduce((a, b) => a + b, 0) / values.length;\n const variance = values.reduce((sum, v) => sum + (v - mean) ** 2, 0) / values.length;\n const stddev = Math.sqrt(variance);\n consensus[field] = Math.round(Math.max(0, 1 - stddev / 5) * 100) / 100;\n }\n const overall_consensus = Math.round(\n (allFields.reduce((sum, f) => sum + consensus[f], 0) / allFields.length) * 100\n ) / 100;\n\n // Aggregate feedback per dimension (from all judges that provided it)\n const aggregated_feedback = {};\n for (const field of allFields) {\n const comments = judges\n .filter(j => j.feedback && j.feedback[field])\n .map(j => ({ judge: j.provider, comment: j.feedback[field] }));\n if (comments.length > 0) aggregated_feedback[field] = comments;\n }\n\n const result = {\n sections: Object.fromEntries(SECTIONS.map(s => [s, averaged[s]])),\n overall,\n novelty: averaged.novelty,\n reproducibility: averaged.reproducibility,\n citation_quality: averaged.citation_quality,\n judges: judges.map(j => j.provider),\n judge_count: judges.length,\n judge_details,\n consensus,\n overall_consensus,\n feedback: Object.keys(aggregated_feedback).length > 0 ? aggregated_feedback : null,\n scored_at: new Date().toISOString(),\n paper_type: paperType,\n };\n\n console.log(`[SCORING] Granular score: overall=${overall}, consensus=${overall_consensus}, judges=${result.judges.join(\",\")}`);\n return result;\n}\n"}, "83c3b3da0f35a8c0fc36a78e76d5bed69eb23b8c": {"t": 1785753948.360381, "n": 21301, "h": "f19e312422c2016d", "c": "/**\n * P2PCLAW Granular Scoring Service\n * =================================\n * Heterogeneous multi-LLM scoring engine that evaluates papers section-by-section.\n *\n * Provider chain (updated 2026-04-01):\n * 1. Cerebras — qwen-3-235b (3 keys, free, ultra-fast)\n * 2. Cerebras — llama3.1-8b (3 keys, free, ultra-fast)\n * 3. Mistral — mistral-small-latest (2 keys, free)\n * 4. Sarvam — sarvam-m (Indian AI, free)\n * 5. OpenRouter — qwen3-coder:free (free)\n * 6. Groq — llama-3.3-70b-versatile (7 keys, may be restricted)\n * 7. NVIDIA — meta/llama-3.3-70b-instruct (3 keys, free)\n * 8. Inception — mercury-2 (5 keys, free)\n * 9. Xiaomi MiMo — mimo-v2-flash (3 keys, free)\n * 10. Xiaomi MiMo — mimo-v2-pro (3 keys, free, reasoning)\n * 11. Cohere — command-a-reasoning (8 keys, reasoning model)\n * 12. Deterministic heuristic fallback (never blocks)\n *\n * ALL available judges score independently. Final score = average across all judges.\n * Each model evaluates each section independently for maximum consensus diversity.\n */\n\nconst SECTIONS = [\"abstract\", \"introduction\", \"methodology\", \"results\", \"discussion\", \"conclusion\", \"references\"];\n\n// ── Load keys with rotation ─────────────────────────────────────────────────\n\nfunction loadKeys(envPrefix, maxKeys = 10) {\n const keys = [];\n for (let i = 1; i <= maxKeys; i++) {\n const k = process.env[`${envPrefix}_${i}`] || process.env[`${envPrefix}${i}`];\n if (k) keys.push(k);\n }\n // Also check single-key env vars\n const single = process.env[envPrefix];\n if (single && !keys.includes(single)) keys.unshift(single);\n return keys;\n}\n\nconst keyIndices = {};\nfunction nextKey(providerId, keys) {\n if (!keys.length) return null;\n const idx = keyIndices[providerId] || 0;\n const key = keys[idx % keys.length];\n keyIndices[providerId] = (idx + 1) % keys.length;\n return key;\n}\n\n// ── Provider definitions ────────────────────────────────────────────────────\n\nconst PROVIDERS = [\n // --- Cerebras: 3 keys x 2 models = up to 6 independent judges ---\n {\n id: \"cerebras-qwen\",\n name: \"Cerebras-Qwen235B\",\n url: \"https://api.cerebras.ai/v1/chat/completions\",\n model: \"qwen-3-235b-a22b-instruct-2507\",\n keys: loadKeys(\"CEREBRAS_API_KEY\").concat(loadKeys(\"CEREBRAS_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 1024,\n },\n {\n id: \"cerebras-llama\",\n name: \"Cerebras-Llama8B\",\n url: \"https://api.cerebras.ai/v1/chat/completions\",\n model: \"llama3.1-8b\",\n keys: loadKeys(\"CEREBRAS_API_KEY\").concat(loadKeys(\"CEREBRAS_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n // --- Mistral: 2 keys, reliable ---\n {\n id: \"mistral\",\n name: \"Mistral\",\n url: \"https://api.mistral.ai/v1/chat/completions\",\n model: \"mistral-small-latest\",\n keys: loadKeys(\"MISTRAL_API_KEY\").concat(loadKeys(\"MISTRAL_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n // --- Sarvam (Indian AI): sarvam-m, uses <think> tags (needs 2048+ tokens) ---\n {\n id: \"sarvam\",\n name: \"Sarvam\",\n url: \"https://api.sarvam.ai/v1/chat/completions\",\n model: \"sarvam-m\",\n keys: loadKeys(\"SARVAM_KEY\").concat(loadKeys(\"SARVAM_API_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 2048,\n },\n // --- OpenRouter: free models ---\n {\n id: \"openrouter\",\n name: \"OpenRouter\",\n url: \"https://openrouter.ai/api/v1/chat/completions\",\n model: \"qwen/qwen3-coder:free\",\n keys: loadKeys(\"OPENROUTER_API_KEY\"),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n extraHeaders: { \"HTTP-Referer\": \"https://www.p2pclaw.com\", \"X-Title\": \"P2PCLAW Scoring\" },\n },\n // --- Groq: 7 keys but may be org-restricted ---\n {\n id: \"groq\",\n name: \"Groq\",\n url: \"https://api.groq.com/openai/v1/chat/completions\",\n model: \"llama-3.3-70b-versatile\",\n keys: loadKeys(\"GROQ_API_KEY\").concat(loadKeys(\"GROQ_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n // --- NVIDIA: 3 keys ---\n {\n id: \"nvidia\",\n name: \"NVIDIA\",\n url: \"https://integrate.api.nvidia.com/v1/chat/completions\",\n model: \"meta/llama-3.3-70b-instruct\",\n keys: loadKeys(\"NVAPI_KEY\").concat(loadKeys(\"NVIDIA_API_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n // --- Inception: mercury-2 ---\n {\n id: \"inception\",\n name: \"Inception\",\n url: \"https://api.inceptionlabs.ai/v1/chat/completions\",\n model: \"mercury-2\",\n keys: loadKeys(\"INCEPTION_API_KEY\").concat(loadKeys(\"INCEPTION_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n // --- Xiaomi MiMo: 3 keys x 2 models = up to 2 independent judges ---\n {\n id: \"xiaomi-flash\",\n name: \"Xiaomi-MiMo-Flash\",\n url: \"https://api.xiaomimimo.com/v1/chat/completions\",\n model: \"mimo-v2-flash\",\n keys: loadKeys(\"XIAOMI_API_KEY\").concat(loadKeys(\"XIAOMI_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n {\n id: \"xiaomi-pro\",\n name: \"Xiaomi-MiMo-Pro\",\n url: \"https://api.xiaomimimo.com/v1/chat/completions\",\n model: \"mimo-v2-pro\",\n keys: loadKeys(\"XIAOMI_API_KEY\").concat(loadKeys(\"XIAOMI_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 1024,\n },\n // --- Cohere: command-a-reasoning model (8 keys, reasoning/thinking model) ---\n {\n id: \"cohere\",\n name: \"Cohere-CommandA\",\n url: \"https://api.cohere.com/v2/chat\",\n model: \"command-a-reasoning-08-2025\",\n keys: loadKeys(\"COHERE_API_KEY\").concat(loadKeys(\"COHERE_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 4096, // reasoning model needs extra tokens for thinking + answer\n responseFormat: \"cohere\", // data.message.content[] array with {type:\"thinking\"} + {type:\"text\"}\n timeout: 90000, // 90s — reasoning model needs time to think through all 10 dimensions\n },\n];\n\n// Deduplicate keys within each provider\nfor (const p of PROVIDERS) {\n p.keys = [...new Set(p.keys)].filter(Boolean);\n}\n\n// Log available providers\nconst available = PROVIDERS.filter(p => p.keys.length > 0);\nconsole.log(`[SCORING] ${available.length} LLM providers available: ${available.map(p => `${p.name}(${p.keys.length})`).join(\", \")}`);\nif (available.length === 0) console.warn(\"[SCORING] No LLM providers — heuristic scoring only.\");\n// Debug: log specifically which providers have NO keys\nconst unavailable = PROVIDERS.filter(p => p.keys.length === 0);\nif (unavailable.length > 0) console.log(`[SCORING] Providers with NO keys: ${unavailable.map(p => p.name).join(\", \")}`);\n\nconst SCORING_PROMPT = `You are an academic paper quality evaluator for the P2PCLAW decentralized research network.\n\nScore this paper on a 0-10 scale for each criterion. For EACH score, include a brief \"why\" (1 sentence explaining the score).\n\nScoring criteria:\n- abstract (0-10): Quality, clarity, and completeness of the abstract. Clear problem statement, scope, and key results summarized.\n- introduction (0-10): Problem statement clarity, context, motivation. Should cite 2+ related works.\n- methodology (0-10): Rigor, reproducibility, appropriate methods. Must describe steps clearly enough to reproduce.\n- results (0-10): Quality of findings, data presentation, significance. Quantitative data strongly preferred.\n- discussion (0-10): Interpretation depth, limitations acknowledged, implications for the field.\n- conclusion (0-10): Summary of key findings (expected, NOT penalized as repetition), concrete future work directions, and connection back to the research question. A good conclusion SHOULD summarize findings — that is standard scientific practice.\n- references (0-10): Citation quality, relevance, proper formatting (0 if no real references). Real author names, titles, years, DOIs/URLs expected.\n- novelty (0-10): Original contribution to the field. Novel frameworks, terminology, or approaches.\n- reproducibility (0-10): Could another researcher reproduce this work? Code, equations, detailed parameters help.\n- citation_quality (0-10): Are references real, relevant, and properly cited? 8+ unique real citations expected for high scores.\n\nIMPORTANT RULES:\n- If a section is MISSING entirely, score it 0\n- If references are placeholder/fake (e.g. \"[1] Author, A. (2026). Placeholder\"), score references as 1\n- Trivial proofs or papers under 300 words should score below 3 overall\n- Papers that prove obvious things (like 0*0=0) should score novelty as 0-1\n- Be STRICT — a score of 8+ means genuinely publishable quality\n- Lean 4 verified papers with valid proof hashes deserve reproducibility bonus\n\nReturn ONLY this JSON format (numbers 0-10, nothing else):\n{\"abstract\":N,\"introduction\":N,\"methodology\":N,\"results\":N,\"discussion\":N,\"conclusion\":N,\"references\":N,\"novelty\":N,\"reproducibility\":N,\"citation_quality\":N}\n\nPaper content:\n`;\n\n/**\n * Call a single LLM provider to score a paper.\n * Tries up to 2 keys from the provider before giving up.\n */\nasync function callLLMForScoring(prompt, provider) {\n if (!provider.keys.length) return null;\n\n const maxAttempts = Math.min(provider.keys.length, 2);\n for (let attempt = 0; attempt < maxAttempts; attempt++) {\n const key = nextKey(provider.id, provider.keys);\n if (!key) return null;\n\n const headers = { \"Content-Type\": \"application/json\" };\n headers[provider.authHeader] = `${provider.authPrefix}${key}`;\n if (provider.extraHeaders) Object.assign(headers, provider.extraHeaders);\n\n try {\n const res = await fetch(provider.url, {\n method: \"POST\",\n headers,\n body: JSON.stringify({\n model: provider.model,\n messages: [{ role: \"user\", content: prompt }],\n max_tokens: provider.maxTokens || 512,\n temperature: 0.1,\n }),\n signal: AbortSignal.timeout(provider.timeout || 30000),\n });\n\n if (res.status === 429) {\n console.warn(`[SCORING] ${provider.name} 429 rate-limited`);\n await new Promise(r => setTimeout(r, 3000));\n continue;\n }\n if (res.status === 402) {\n console.warn(`[SCORING] ${provider.name} 402 credits exhausted — skipping provider`);\n return null;\n }\n if (!res.ok) {\n const errBody = await res.text().catch(() => \"\");\n console.warn(`[SCORING] ${provider.name} HTTP ${res.status}: ${errBody.substring(0, 150)}`);\n continue;\n }\n\n const data = await res.json();\n // Support both OpenAI format (choices[0].message.content) and Cohere v2 (message.content[] array)\n let text = \"\";\n if (provider.responseFormat === \"cohere\") {\n // Cohere v2 returns array of content blocks: [{type:\"thinking\",...}, {type:\"text\",text:\"...\"}]\n const blocks = data.message?.content || [];\n if (Array.isArray(blocks)) {\n const textBlock = blocks.find(b => b.type === \"text\");\n text = textBlock?.text || blocks[blocks.length - 1]?.text || \"\";\n } else {\n text = typeof blocks === \"string\" ? blocks : \"\";\n }\n } else {\n text = data.choices?.[0]?.message?.content || \"\";\n }\n\n // Strip <think>...</think> tags (Sarvam, Qwen with thinking mode)\n if (provider.stripThinkTags || text.includes(\"<think>\")) {\n // Strip closed think blocks\n text = text.replace(/<think>[\\s\\S]*?<\\/think>/g, \"\").trim();\n // Strip unclosed think blocks (model ran out of tokens mid-think)\n if (text.startsWith(\"<think>\")) {\n text = text.replace(/<think>[\\s\\S]*/g, \"\").trim();\n }\n }\n\n // Extract JSON — use balanced braces to handle nested objects like {\"score\":8,\"why\":\"...\"}\n let jsonMatch = null;\n const firstBrace = text.indexOf('{');\n if (firstBrace !== -1) {\n let depth = 0, end = -1;\n for (let i = firstBrace; i < text.length; i++) {\n if (text[i] === '{') depth++;\n else if (text[i] === '}') { depth--; if (depth === 0) { end = i; break; } }\n }\n if (end !== -1) jsonMatch = [text.substring(firstBrace, end + 1)];\n }\n if (!jsonMatch) {\n console.warn(`[SCORING] ${provider.name} — no JSON in response`);\n continue;\n }\n\n const parsed = JSON.parse(jsonMatch[0]);\n const fields = [...SECTIONS, \"novelty\", \"reproducibility\", \"citation_quality\"];\n const feedback = {};\n for (const field of fields) {\n let val = parsed[field];\n // Support both plain number and {score, why} object format\n if (val && typeof val === \"object\" && typeof val.score === \"number\") {\n feedback[field] = val.why || null;\n val = val.score;\n }\n if (typeof val !== \"number\" || val < 0 || val > 10) {\n val = typeof val === \"number\" ? Math.max(0, Math.min(10, Math.round(val))) : 5;\n }\n parsed[field] = val;\n }\n\n // Clean parsed: only keep recognized score fields (remove stray \"why\" etc.)\n const cleanScores = {};\n for (const field of fields) cleanScores[field] = parsed[field];\n\n console.log(`[SCORING] ${provider.name} scored successfully`);\n return { scores: cleanScores, provider: provider.name, feedback: Object.keys(feedback).length > 0 ? feedback : null };\n } catch (e) {\n console.warn(`[SCORING] ${provider.name} error: ${e.message}`);\n }\n }\n return null;\n}\n\n/**\n * Deterministic heuristic scoring — used when all LLMs fail.\n */\nfunction heuristicScore(content) {\n const text = content || \"\";\n const words = text.split(/\\s+/).filter(w => w.length > 0);\n const wordCount = words.length;\n const lowerText = text.toLowerCase();\n\n const sectionScores = {};\n for (const section of SECTIONS) {\n const hasSection = lowerText.includes(`## ${section}`) || lowerText.includes(`# ${section}`);\n if (!hasSection) {\n sectionScores[section] = 0;\n continue;\n }\n const sectionRegex = new RegExp(`##?\\\\s*${section}[\\\\s\\\\S]*?(?=##?\\\\s|$)`, \"i\");\n const match = text.match(sectionRegex);\n const sectionWords = match ? match[0].split(/\\s+/).length : 0;\n if (sectionWords < 20) sectionScores[section] = 2;\n else if (sectionWords < 50) sectionScores[section] = 4;\n else if (sectionWords < 100) sectionScores[section] = 5;\n else if (sectionWords < 200) sectionScores[section] = 6;\n else sectionScores[section] = 7;\n }\n\n const refMatches = text.match(/\\[\\d+\\]/g) || [];\n const uniqueRefs = new Set(refMatches).size;\n const hasPlaceholderRefs = /placeholder|author,?\\s*a\\.\\s*\\(\\d{4}\\)/i.test(text);\n const hasRealAuthors = /[A-Z][a-z]+,\\s*[A-Z]\\.\\s*(?:&|,|et al)/g.test(text);\n const hasDOI = /doi\\.org|arxiv\\.org|10\\.\\d{4}/i.test(text);\n let refScore = hasPlaceholderRefs ? 1 : Math.min(7, uniqueRefs);\n if (hasRealAuthors) refScore = Math.min(10, refScore + 1);\n if (hasDOI) refScore = Math.min(10, refScore + 1);\n sectionScores.references = refScore;\n\n const technicalTerms = (text.match(/\\b(algorithm|theorem|proof|complexity|O\\([^)]+\\)|convergence|optimal|novel|framework)\\b/gi) || []).length;\n const hasFigures = /figure \\d|fig\\.\\s*\\d|table \\d/i.test(text);\n let novelty = wordCount > 2000 ? 4 : wordCount > 1000 ? 3 : 2;\n novelty += Math.min(3, Math.floor(technicalTerms / 5));\n if (hasFigures) novelty += 1;\n\n const hasCode = /```[\\s\\S]*?```/.test(text);\n const hasEquations = /\\$[^$]+\\$/.test(text) || /\\\\begin\\{/.test(text);\n const hasNumbers = /\\d+\\.\\d+%|\\d+\\.\\d+x|p\\s*[<>]\\s*0\\.\\d/i.test(text);\n let reproducibility = (hasCode ? 5 : 3) + (hasEquations ? 1 : 0) + (wordCount > 2000 ? 1 : 0);\n if (hasNumbers) reproducibility += 1;\n\n const citation_quality = hasPlaceholderRefs ? 1 : Math.min(6, uniqueRefs);\n\n return {\n scores: {\n ...sectionScores,\n novelty: Math.min(10, novelty),\n reproducibility: Math.min(10, reproducibility),\n citation_quality: Math.min(10, citation_quality),\n },\n provider: \"heuristic\"\n };\n}\n\n/**\n * Score a paper using heterogeneous multi-LLM swarm.\n * Tries to get 2 independent judges, falls back through provider chain.\n */\nexport async function scoreGranular(content, paperType = \"research\") {\n if (!content || content.trim().length < 50) {\n return {\n sections: Object.fromEntries(SECTIONS.map(s => [s, 0])),\n overall: 0,\n novelty: 0,\n reproducibility: 0,\n citation_quality: 0,\n judges: [],\n judge_count: 0,\n error: \"Content too short to score\"\n };\n }\n\n const truncated = content.length > 4000 ? content.substring(0, 4000) + \"\\n\\n[... truncated for scoring ...]\" : content;\n const prompt = SCORING_PROMPT + truncated;\n\n // ALL available judges score independently for maximum consensus diversity.\n // Each model evaluates each section independently, then we average.\n // Run all judges in parallel for speed (each has its own timeout).\n const judgePromises = available.map(provider =>\n callLLMForScoring(prompt, provider).catch(() => null)\n );\n const judgeResults = await Promise.all(judgePromises);\n const judges = judgeResults.filter(Boolean);\n\n // If no LLM judges succeeded, use heuristic\n if (judges.length === 0) {\n const heuristic = heuristicScore(content);\n judges.push(heuristic);\n }\n\n // Average scores across all judges\n const allFields = [...SECTIONS, \"novelty\", \"reproducibility\", \"citation_quality\"];\n const averaged = {};\n for (const field of allFields) {\n const values = judges.map(j => j.scores[field]).filter(v => typeof v === \"number\");\n averaged[field] = values.length > 0\n ? Math.round((values.reduce((a, b) => a + b, 0) / values.length) * 10) / 10\n : 0;\n }\n\n const sectionValues = SECTIONS.map(s => averaged[s]);\n const overall = Math.round((sectionValues.reduce((a, b) => a + b, 0) / SECTIONS.length) * 10) / 10;\n\n // Per-judge detail breakdown (individual scores + feedback)\n const judge_details = judges.map(j => ({\n judge: j.provider,\n scores: j.scores,\n feedback: j.feedback || null,\n }));\n\n // Consensus score per dimension (1 = all judges agree, 0 = total disagreement)\n const consensus = {};\n for (const field of allFields) {\n const values = judges.map(j => j.scores[field]).filter(v => typeof v === \"number\");\n if (values.length < 2) { consensus[field] = 1.0; continue; }\n const mean = values.reduce((a, b) => a + b, 0) / values.length;\n const variance = values.reduce((sum, v) => sum + (v - mean) ** 2, 0) / values.length;\n const stddev = Math.sqrt(variance);\n consensus[field] = Math.round(Math.max(0, 1 - stddev / 5) * 100) / 100;\n }\n const overall_consensus = Math.round(\n (allFields.reduce((sum, f) => sum + consensus[f], 0) / allFields.length) * 100\n ) / 100;\n\n // Aggregate feedback per dimension (from all judges that provided it)\n const aggregated_feedback = {};\n for (const field of allFields) {\n const comments = judges\n .filter(j => j.feedback && j.feedback[field])\n .map(j => ({ judge: j.provider, comment: j.feedback[field] }));\n if (comments.length > 0) aggregated_feedback[field] = comments;\n }\n\n const result = {\n sections: Object.fromEntries(SECTIONS.map(s => [s, averaged[s]])),\n overall,\n novelty: averaged.novelty,\n reproducibility: averaged.reproducibility,\n citation_quality: averaged.citation_quality,\n judges: judges.map(j => j.provider),\n judge_count: judges.length,\n judge_details,\n consensus,\n overall_consensus,\n feedback: Object.keys(aggregated_feedback).length > 0 ? aggregated_feedback : null,\n scored_at: new Date().toISOString(),\n paper_type: paperType,\n };\n\n console.log(`[SCORING] Granular score: overall=${overall}, consensus=${overall_consensus}, judges=${result.judges.join(\",\")}`);\n return result;\n}\n"}, "759745dcd95928a9202a013af181759db026f47b": {"t": 1785753949.8049052, "n": 21060, "h": "2132279512da5212", "c": "/**\n * P2PCLAW Granular Scoring Service\n * =================================\n * Heterogeneous multi-LLM scoring engine that evaluates papers section-by-section.\n *\n * Provider chain (updated 2026-04-01):\n * 1. Cerebras — qwen-3-235b (3 keys, free, ultra-fast)\n * 2. Cerebras — llama3.1-8b (3 keys, free, ultra-fast)\n * 3. Mistral — mistral-small-latest (2 keys, free)\n * 4. Sarvam — sarvam-m (Indian AI, free)\n * 5. OpenRouter — qwen3-coder:free (free)\n * 6. Groq — llama-3.3-70b-versatile (7 keys, may be restricted)\n * 7. NVIDIA — meta/llama-3.3-70b-instruct (3 keys, free)\n * 8. Inception — mercury-2 (5 keys, free)\n * 9. Xiaomi MiMo — mimo-v2-flash (3 keys, free)\n * 10. Xiaomi MiMo — mimo-v2-pro (3 keys, free, reasoning)\n * 11. Cohere — command-a-reasoning (8 keys, reasoning model)\n * 12. Deterministic heuristic fallback (never blocks)\n *\n * ALL available judges score independently. Final score = average across all judges.\n * Each model evaluates each section independently for maximum consensus diversity.\n */\n\nconst SECTIONS = [\"abstract\", \"introduction\", \"methodology\", \"results\", \"discussion\", \"conclusion\", \"references\"];\n\n// ── Load keys with rotation ─────────────────────────────────────────────────\n\nfunction loadKeys(envPrefix, maxKeys = 10) {\n const keys = [];\n for (let i = 1; i <= maxKeys; i++) {\n const k = process.env[`${envPrefix}_${i}`] || process.env[`${envPrefix}${i}`];\n if (k) keys.push(k);\n }\n // Also check single-key env vars\n const single = process.env[envPrefix];\n if (single && !keys.includes(single)) keys.unshift(single);\n return keys;\n}\n\nconst keyIndices = {};\nfunction nextKey(providerId, keys) {\n if (!keys.length) return null;\n const idx = keyIndices[providerId] || 0;\n const key = keys[idx % keys.length];\n keyIndices[providerId] = (idx + 1) % keys.length;\n return key;\n}\n\n// ── Provider definitions ────────────────────────────────────────────────────\n\nconst PROVIDERS = [\n // --- Cerebras: 3 keys x 2 models = up to 6 independent judges ---\n {\n id: \"cerebras-qwen\",\n name: \"Cerebras-Qwen235B\",\n url: \"https://api.cerebras.ai/v1/chat/completions\",\n model: \"qwen-3-235b-a22b-instruct-2507\",\n keys: loadKeys(\"CEREBRAS_API_KEY\").concat(loadKeys(\"CEREBRAS_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 1024,\n },\n {\n id: \"cerebras-llama\",\n name: \"Cerebras-Llama8B\",\n url: \"https://api.cerebras.ai/v1/chat/completions\",\n model: \"llama3.1-8b\",\n keys: loadKeys(\"CEREBRAS_API_KEY\").concat(loadKeys(\"CEREBRAS_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n // --- Mistral: 2 keys, reliable ---\n {\n id: \"mistral\",\n name: \"Mistral\",\n url: \"https://api.mistral.ai/v1/chat/completions\",\n model: \"mistral-small-latest\",\n keys: loadKeys(\"MISTRAL_API_KEY\").concat(loadKeys(\"MISTRAL_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n // --- Sarvam (Indian AI): sarvam-m, uses <think> tags (needs 2048+ tokens) ---\n {\n id: \"sarvam\",\n name: \"Sarvam\",\n url: \"https://api.sarvam.ai/v1/chat/completions\",\n model: \"sarvam-m\",\n keys: loadKeys(\"SARVAM_KEY\").concat(loadKeys(\"SARVAM_API_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 2048,\n },\n // --- OpenRouter: free models ---\n {\n id: \"openrouter\",\n name: \"OpenRouter\",\n url: \"https://openrouter.ai/api/v1/chat/completions\",\n model: \"qwen/qwen3-coder:free\",\n keys: loadKeys(\"OPENROUTER_API_KEY\"),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n extraHeaders: { \"HTTP-Referer\": \"https://www.p2pclaw.com\", \"X-Title\": \"P2PCLAW Scoring\" },\n },\n // --- Groq: 7 keys but may be org-restricted ---\n {\n id: \"groq\",\n name: \"Groq\",\n url: \"https://api.groq.com/openai/v1/chat/completions\",\n model: \"llama-3.3-70b-versatile\",\n keys: loadKeys(\"GROQ_API_KEY\").concat(loadKeys(\"GROQ_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n // --- NVIDIA: 3 keys ---\n {\n id: \"nvidia\",\n name: \"NVIDIA\",\n url: \"https://integrate.api.nvidia.com/v1/chat/completions\",\n model: \"meta/llama-3.3-70b-instruct\",\n keys: loadKeys(\"NVAPI_KEY\").concat(loadKeys(\"NVIDIA_API_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n // --- Inception: mercury-2 ---\n {\n id: \"inception\",\n name: \"Inception\",\n url: \"https://api.inceptionlabs.ai/v1/chat/completions\",\n model: \"mercury-2\",\n keys: loadKeys(\"INCEPTION_API_KEY\").concat(loadKeys(\"INCEPTION_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n // --- Xiaomi MiMo: 3 keys x 2 models = up to 2 independent judges ---\n {\n id: \"xiaomi-flash\",\n name: \"Xiaomi-MiMo-Flash\",\n url: \"https://api.xiaomimimo.com/v1/chat/completions\",\n model: \"mimo-v2-flash\",\n keys: loadKeys(\"XIAOMI_API_KEY\").concat(loadKeys(\"XIAOMI_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n {\n id: \"xiaomi-pro\",\n name: \"Xiaomi-MiMo-Pro\",\n url: \"https://api.xiaomimimo.com/v1/chat/completions\",\n model: \"mimo-v2-pro\",\n keys: loadKeys(\"XIAOMI_API_KEY\").concat(loadKeys(\"XIAOMI_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 1024,\n },\n // --- Cohere: command-a-reasoning model (8 keys, reasoning/thinking model) ---\n {\n id: \"cohere\",\n name: \"Cohere-CommandA\",\n url: \"https://api.cohere.com/v2/chat\",\n model: \"command-a-reasoning-08-2025\",\n keys: loadKeys(\"COHERE_API_KEY\").concat(loadKeys(\"COHERE_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 4096, // reasoning model needs extra tokens for thinking + answer\n responseFormat: \"cohere\", // data.message.content[] array with {type:\"thinking\"} + {type:\"text\"}\n timeout: 90000, // 90s — reasoning model needs time to think through all 10 dimensions\n },\n];\n\n// Deduplicate keys within each provider\nfor (const p of PROVIDERS) {\n p.keys = [...new Set(p.keys)].filter(Boolean);\n}\n\n// Log available providers\nconst available = PROVIDERS.filter(p => p.keys.length > 0);\nconsole.log(`[SCORING] ${available.length} LLM providers available: ${available.map(p => `${p.name}(${p.keys.length})`).join(\", \")}`);\nif (available.length === 0) console.warn(\"[SCORING] No LLM providers — heuristic scoring only.\");\n\nconst SCORING_PROMPT = `You are an academic paper quality evaluator for the P2PCLAW decentralized research network.\n\nScore this paper on a 0-10 scale for each criterion. For EACH score, include a brief \"why\" (1 sentence explaining the score).\n\nScoring criteria:\n- abstract (0-10): Quality, clarity, and completeness of the abstract. Clear problem statement, scope, and key results summarized.\n- introduction (0-10): Problem statement clarity, context, motivation. Should cite 2+ related works.\n- methodology (0-10): Rigor, reproducibility, appropriate methods. Must describe steps clearly enough to reproduce.\n- results (0-10): Quality of findings, data presentation, significance. Quantitative data strongly preferred.\n- discussion (0-10): Interpretation depth, limitations acknowledged, implications for the field.\n- conclusion (0-10): Summary of key findings (expected, NOT penalized as repetition), concrete future work directions, and connection back to the research question. A good conclusion SHOULD summarize findings — that is standard scientific practice.\n- references (0-10): Citation quality, relevance, proper formatting (0 if no real references). Real author names, titles, years, DOIs/URLs expected.\n- novelty (0-10): Original contribution to the field. Novel frameworks, terminology, or approaches.\n- reproducibility (0-10): Could another researcher reproduce this work? Code, equations, detailed parameters help.\n- citation_quality (0-10): Are references real, relevant, and properly cited? 8+ unique real citations expected for high scores.\n\nIMPORTANT RULES:\n- If a section is MISSING entirely, score it 0\n- If references are placeholder/fake (e.g. \"[1] Author, A. (2026). Placeholder\"), score references as 1\n- Trivial proofs or papers under 300 words should score below 3 overall\n- Papers that prove obvious things (like 0*0=0) should score novelty as 0-1\n- Be STRICT — a score of 8+ means genuinely publishable quality\n- Lean 4 verified papers with valid proof hashes deserve reproducibility bonus\n\nReturn ONLY this JSON format (numbers 0-10, nothing else):\n{\"abstract\":N,\"introduction\":N,\"methodology\":N,\"results\":N,\"discussion\":N,\"conclusion\":N,\"references\":N,\"novelty\":N,\"reproducibility\":N,\"citation_quality\":N}\n\nPaper content:\n`;\n\n/**\n * Call a single LLM provider to score a paper.\n * Tries up to 2 keys from the provider before giving up.\n */\nasync function callLLMForScoring(prompt, provider) {\n if (!provider.keys.length) return null;\n\n const maxAttempts = Math.min(provider.keys.length, 2);\n for (let attempt = 0; attempt < maxAttempts; attempt++) {\n const key = nextKey(provider.id, provider.keys);\n if (!key) return null;\n\n const headers = { \"Content-Type\": \"application/json\" };\n headers[provider.authHeader] = `${provider.authPrefix}${key}`;\n if (provider.extraHeaders) Object.assign(headers, provider.extraHeaders);\n\n try {\n const res = await fetch(provider.url, {\n method: \"POST\",\n headers,\n body: JSON.stringify({\n model: provider.model,\n messages: [{ role: \"user\", content: prompt }],\n max_tokens: provider.maxTokens || 512,\n temperature: 0.1,\n }),\n signal: AbortSignal.timeout(provider.timeout || 30000),\n });\n\n if (res.status === 429) {\n console.warn(`[SCORING] ${provider.name} 429 rate-limited`);\n await new Promise(r => setTimeout(r, 3000));\n continue;\n }\n if (res.status === 402) {\n console.warn(`[SCORING] ${provider.name} 402 credits exhausted — skipping provider`);\n return null;\n }\n if (!res.ok) {\n const errBody = await res.text().catch(() => \"\");\n console.warn(`[SCORING] ${provider.name} HTTP ${res.status}: ${errBody.substring(0, 150)}`);\n continue;\n }\n\n const data = await res.json();\n // Support both OpenAI format (choices[0].message.content) and Cohere v2 (message.content[] array)\n let text = \"\";\n if (provider.responseFormat === \"cohere\") {\n // Cohere v2 returns array of content blocks: [{type:\"thinking\",...}, {type:\"text\",text:\"...\"}]\n const blocks = data.message?.content || [];\n if (Array.isArray(blocks)) {\n const textBlock = blocks.find(b => b.type === \"text\");\n text = textBlock?.text || blocks[blocks.length - 1]?.text || \"\";\n } else {\n text = typeof blocks === \"string\" ? blocks : \"\";\n }\n } else {\n text = data.choices?.[0]?.message?.content || \"\";\n }\n\n // Strip <think>...</think> tags (Sarvam, Qwen with thinking mode)\n if (provider.stripThinkTags || text.includes(\"<think>\")) {\n // Strip closed think blocks\n text = text.replace(/<think>[\\s\\S]*?<\\/think>/g, \"\").trim();\n // Strip unclosed think blocks (model ran out of tokens mid-think)\n if (text.startsWith(\"<think>\")) {\n text = text.replace(/<think>[\\s\\S]*/g, \"\").trim();\n }\n }\n\n // Extract JSON — use balanced braces to handle nested objects like {\"score\":8,\"why\":\"...\"}\n let jsonMatch = null;\n const firstBrace = text.indexOf('{');\n if (firstBrace !== -1) {\n let depth = 0, end = -1;\n for (let i = firstBrace; i < text.length; i++) {\n if (text[i] === '{') depth++;\n else if (text[i] === '}') { depth--; if (depth === 0) { end = i; break; } }\n }\n if (end !== -1) jsonMatch = [text.substring(firstBrace, end + 1)];\n }\n if (!jsonMatch) {\n console.warn(`[SCORING] ${provider.name} — no JSON in response`);\n continue;\n }\n\n const parsed = JSON.parse(jsonMatch[0]);\n const fields = [...SECTIONS, \"novelty\", \"reproducibility\", \"citation_quality\"];\n const feedback = {};\n for (const field of fields) {\n let val = parsed[field];\n // Support both plain number and {score, why} object format\n if (val && typeof val === \"object\" && typeof val.score === \"number\") {\n feedback[field] = val.why || null;\n val = val.score;\n }\n if (typeof val !== \"number\" || val < 0 || val > 10) {\n val = typeof val === \"number\" ? Math.max(0, Math.min(10, Math.round(val))) : 5;\n }\n parsed[field] = val;\n }\n\n // Clean parsed: only keep recognized score fields (remove stray \"why\" etc.)\n const cleanScores = {};\n for (const field of fields) cleanScores[field] = parsed[field];\n\n console.log(`[SCORING] ${provider.name} scored successfully`);\n return { scores: cleanScores, provider: provider.name, feedback: Object.keys(feedback).length > 0 ? feedback : null };\n } catch (e) {\n console.warn(`[SCORING] ${provider.name} error: ${e.message}`);\n }\n }\n return null;\n}\n\n/**\n * Deterministic heuristic scoring — used when all LLMs fail.\n */\nfunction heuristicScore(content) {\n const text = content || \"\";\n const words = text.split(/\\s+/).filter(w => w.length > 0);\n const wordCount = words.length;\n const lowerText = text.toLowerCase();\n\n const sectionScores = {};\n for (const section of SECTIONS) {\n const hasSection = lowerText.includes(`## ${section}`) || lowerText.includes(`# ${section}`);\n if (!hasSection) {\n sectionScores[section] = 0;\n continue;\n }\n const sectionRegex = new RegExp(`##?\\\\s*${section}[\\\\s\\\\S]*?(?=##?\\\\s|$)`, \"i\");\n const match = text.match(sectionRegex);\n const sectionWords = match ? match[0].split(/\\s+/).length : 0;\n if (sectionWords < 20) sectionScores[section] = 2;\n else if (sectionWords < 50) sectionScores[section] = 4;\n else if (sectionWords < 100) sectionScores[section] = 5;\n else if (sectionWords < 200) sectionScores[section] = 6;\n else sectionScores[section] = 7;\n }\n\n const refMatches = text.match(/\\[\\d+\\]/g) || [];\n const uniqueRefs = new Set(refMatches).size;\n const hasPlaceholderRefs = /placeholder|author,?\\s*a\\.\\s*\\(\\d{4}\\)/i.test(text);\n const hasRealAuthors = /[A-Z][a-z]+,\\s*[A-Z]\\.\\s*(?:&|,|et al)/g.test(text);\n const hasDOI = /doi\\.org|arxiv\\.org|10\\.\\d{4}/i.test(text);\n let refScore = hasPlaceholderRefs ? 1 : Math.min(7, uniqueRefs);\n if (hasRealAuthors) refScore = Math.min(10, refScore + 1);\n if (hasDOI) refScore = Math.min(10, refScore + 1);\n sectionScores.references = refScore;\n\n const technicalTerms = (text.match(/\\b(algorithm|theorem|proof|complexity|O\\([^)]+\\)|convergence|optimal|novel|framework)\\b/gi) || []).length;\n const hasFigures = /figure \\d|fig\\.\\s*\\d|table \\d/i.test(text);\n let novelty = wordCount > 2000 ? 4 : wordCount > 1000 ? 3 : 2;\n novelty += Math.min(3, Math.floor(technicalTerms / 5));\n if (hasFigures) novelty += 1;\n\n const hasCode = /```[\\s\\S]*?```/.test(text);\n const hasEquations = /\\$[^$]+\\$/.test(text) || /\\\\begin\\{/.test(text);\n const hasNumbers = /\\d+\\.\\d+%|\\d+\\.\\d+x|p\\s*[<>]\\s*0\\.\\d/i.test(text);\n let reproducibility = (hasCode ? 5 : 3) + (hasEquations ? 1 : 0) + (wordCount > 2000 ? 1 : 0);\n if (hasNumbers) reproducibility += 1;\n\n const citation_quality = hasPlaceholderRefs ? 1 : Math.min(6, uniqueRefs);\n\n return {\n scores: {\n ...sectionScores,\n novelty: Math.min(10, novelty),\n reproducibility: Math.min(10, reproducibility),\n citation_quality: Math.min(10, citation_quality),\n },\n provider: \"heuristic\"\n };\n}\n\n/**\n * Score a paper using heterogeneous multi-LLM swarm.\n * Tries to get 2 independent judges, falls back through provider chain.\n */\nexport async function scoreGranular(content, paperType = \"research\") {\n if (!content || content.trim().length < 50) {\n return {\n sections: Object.fromEntries(SECTIONS.map(s => [s, 0])),\n overall: 0,\n novelty: 0,\n reproducibility: 0,\n citation_quality: 0,\n judges: [],\n judge_count: 0,\n error: \"Content too short to score\"\n };\n }\n\n const truncated = content.length > 4000 ? content.substring(0, 4000) + \"\\n\\n[... truncated for scoring ...]\" : content;\n const prompt = SCORING_PROMPT + truncated;\n\n // ALL available judges score independently for maximum consensus diversity.\n // Each model evaluates each section independently, then we average.\n // Run all judges in parallel for speed (each has its own timeout).\n const judgePromises = available.map(provider =>\n callLLMForScoring(prompt, provider).catch(() => null)\n );\n const judgeResults = await Promise.all(judgePromises);\n const judges = judgeResults.filter(Boolean);\n\n // If no LLM judges succeeded, use heuristic\n if (judges.length === 0) {\n const heuristic = heuristicScore(content);\n judges.push(heuristic);\n }\n\n // Average scores across all judges\n const allFields = [...SECTIONS, \"novelty\", \"reproducibility\", \"citation_quality\"];\n const averaged = {};\n for (const field of allFields) {\n const values = judges.map(j => j.scores[field]).filter(v => typeof v === \"number\");\n averaged[field] = values.length > 0\n ? Math.round((values.reduce((a, b) => a + b, 0) / values.length) * 10) / 10\n : 0;\n }\n\n const sectionValues = SECTIONS.map(s => averaged[s]);\n const overall = Math.round((sectionValues.reduce((a, b) => a + b, 0) / SECTIONS.length) * 10) / 10;\n\n // Per-judge detail breakdown (individual scores + feedback)\n const judge_details = judges.map(j => ({\n judge: j.provider,\n scores: j.scores,\n feedback: j.feedback || null,\n }));\n\n // Consensus score per dimension (1 = all judges agree, 0 = total disagreement)\n const consensus = {};\n for (const field of allFields) {\n const values = judges.map(j => j.scores[field]).filter(v => typeof v === \"number\");\n if (values.length < 2) { consensus[field] = 1.0; continue; }\n const mean = values.reduce((a, b) => a + b, 0) / values.length;\n const variance = values.reduce((sum, v) => sum + (v - mean) ** 2, 0) / values.length;\n const stddev = Math.sqrt(variance);\n consensus[field] = Math.round(Math.max(0, 1 - stddev / 5) * 100) / 100;\n }\n const overall_consensus = Math.round(\n (allFields.reduce((sum, f) => sum + consensus[f], 0) / allFields.length) * 100\n ) / 100;\n\n // Aggregate feedback per dimension (from all judges that provided it)\n const aggregated_feedback = {};\n for (const field of allFields) {\n const comments = judges\n .filter(j => j.feedback && j.feedback[field])\n .map(j => ({ judge: j.provider, comment: j.feedback[field] }));\n if (comments.length > 0) aggregated_feedback[field] = comments;\n }\n\n const result = {\n sections: Object.fromEntries(SECTIONS.map(s => [s, averaged[s]])),\n overall,\n novelty: averaged.novelty,\n reproducibility: averaged.reproducibility,\n citation_quality: averaged.citation_quality,\n judges: judges.map(j => j.provider),\n judge_count: judges.length,\n judge_details,\n consensus,\n overall_consensus,\n feedback: Object.keys(aggregated_feedback).length > 0 ? aggregated_feedback : null,\n scored_at: new Date().toISOString(),\n paper_type: paperType,\n };\n\n console.log(`[SCORING] Granular score: overall=${overall}, consensus=${overall_consensus}, judges=${result.judges.join(\",\")}`);\n return result;\n}\n"}, "d21d8628540258cac445182de62db64407574c27": {"t": 1785753951.020373, "n": 20945, "h": "2c8974d33a6348cf", "c": "/**\n * P2PCLAW Granular Scoring Service\n * =================================\n * Heterogeneous multi-LLM scoring engine that evaluates papers section-by-section.\n *\n * Provider chain (updated 2026-04-01):\n * 1. Cerebras — qwen-3-235b (3 keys, free, ultra-fast)\n * 2. Cerebras — llama3.1-8b (3 keys, free, ultra-fast)\n * 3. Mistral — mistral-small-latest (2 keys, free)\n * 4. Sarvam — sarvam-m (Indian AI, free)\n * 5. OpenRouter — qwen3-coder:free (free)\n * 6. Groq — llama-3.3-70b-versatile (7 keys, may be restricted)\n * 7. NVIDIA — meta/llama-3.3-70b-instruct (3 keys, free)\n * 8. Inception — mercury-2 (5 keys, free)\n * 9. Xiaomi MiMo — mimo-v2-flash (3 keys, free)\n * 10. Xiaomi MiMo — mimo-v2-pro (3 keys, free, reasoning)\n * 11. Cohere — command-a-reasoning (8 keys, reasoning model)\n * 12. Deterministic heuristic fallback (never blocks)\n *\n * ALL available judges score independently. Final score = average across all judges.\n * Each model evaluates each section independently for maximum consensus diversity.\n */\n\nconst SECTIONS = [\"abstract\", \"introduction\", \"methodology\", \"results\", \"discussion\", \"conclusion\", \"references\"];\n\n// ── Load keys with rotation ─────────────────────────────────────────────────\n\nfunction loadKeys(envPrefix, maxKeys = 10) {\n const keys = [];\n for (let i = 1; i <= maxKeys; i++) {\n const k = process.env[`${envPrefix}_${i}`] || process.env[`${envPrefix}${i}`];\n if (k) keys.push(k);\n }\n // Also check single-key env vars\n const single = process.env[envPrefix];\n if (single && !keys.includes(single)) keys.unshift(single);\n return keys;\n}\n\nconst keyIndices = {};\nfunction nextKey(providerId, keys) {\n if (!keys.length) return null;\n const idx = keyIndices[providerId] || 0;\n const key = keys[idx % keys.length];\n keyIndices[providerId] = (idx + 1) % keys.length;\n return key;\n}\n\n// ── Provider definitions ────────────────────────────────────────────────────\n\nconst PROVIDERS = [\n // --- Cerebras: 3 keys x 2 models = up to 6 independent judges ---\n {\n id: \"cerebras-qwen\",\n name: \"Cerebras-Qwen235B\",\n url: \"https://api.cerebras.ai/v1/chat/completions\",\n model: \"qwen-3-235b-a22b-instruct-2507\",\n keys: loadKeys(\"CEREBRAS_API_KEY\").concat(loadKeys(\"CEREBRAS_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 1024,\n },\n {\n id: \"cerebras-llama\",\n name: \"Cerebras-Llama8B\",\n url: \"https://api.cerebras.ai/v1/chat/completions\",\n model: \"llama3.1-8b\",\n keys: loadKeys(\"CEREBRAS_API_KEY\").concat(loadKeys(\"CEREBRAS_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n // --- Mistral: 2 keys, reliable ---\n {\n id: \"mistral\",\n name: \"Mistral\",\n url: \"https://api.mistral.ai/v1/chat/completions\",\n model: \"mistral-small-latest\",\n keys: loadKeys(\"MISTRAL_API_KEY\").concat(loadKeys(\"MISTRAL_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n // --- Sarvam (Indian AI): sarvam-m, uses <think> tags (needs 2048+ tokens) ---\n {\n id: \"sarvam\",\n name: \"Sarvam\",\n url: \"https://api.sarvam.ai/v1/chat/completions\",\n model: \"sarvam-m\",\n keys: loadKeys(\"SARVAM_KEY\").concat(loadKeys(\"SARVAM_API_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 2048,\n },\n // --- OpenRouter: free models ---\n {\n id: \"openrouter\",\n name: \"OpenRouter\",\n url: \"https://openrouter.ai/api/v1/chat/completions\",\n model: \"qwen/qwen3-coder:free\",\n keys: loadKeys(\"OPENROUTER_API_KEY\"),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n extraHeaders: { \"HTTP-Referer\": \"https://www.p2pclaw.com\", \"X-Title\": \"P2PCLAW Scoring\" },\n },\n // --- Groq: 7 keys but may be org-restricted ---\n {\n id: \"groq\",\n name: \"Groq\",\n url: \"https://api.groq.com/openai/v1/chat/completions\",\n model: \"llama-3.3-70b-versatile\",\n keys: loadKeys(\"GROQ_API_KEY\").concat(loadKeys(\"GROQ_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n // --- NVIDIA: 3 keys ---\n {\n id: \"nvidia\",\n name: \"NVIDIA\",\n url: \"https://integrate.api.nvidia.com/v1/chat/completions\",\n model: \"meta/llama-3.3-70b-instruct\",\n keys: loadKeys(\"NVAPI_KEY\").concat(loadKeys(\"NVIDIA_API_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n // --- Inception: mercury-2 ---\n {\n id: \"inception\",\n name: \"Inception\",\n url: \"https://api.inceptionlabs.ai/v1/chat/completions\",\n model: \"mercury-2\",\n keys: loadKeys(\"INCEPTION_API_KEY\").concat(loadKeys(\"INCEPTION_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n // --- Xiaomi MiMo: 3 keys x 2 models = up to 2 independent judges ---\n {\n id: \"xiaomi-flash\",\n name: \"Xiaomi-MiMo-Flash\",\n url: \"https://api.xiaomimimo.com/v1/chat/completions\",\n model: \"mimo-v2-flash\",\n keys: loadKeys(\"XIAOMI_API_KEY\").concat(loadKeys(\"XIAOMI_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n {\n id: \"xiaomi-pro\",\n name: \"Xiaomi-MiMo-Pro\",\n url: \"https://api.xiaomimimo.com/v1/chat/completions\",\n model: \"mimo-v2-pro\",\n keys: loadKeys(\"XIAOMI_API_KEY\").concat(loadKeys(\"XIAOMI_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 1024,\n },\n // --- Cohere: command-a-reasoning model (8 keys, reasoning/thinking model) ---\n {\n id: \"cohere\",\n name: \"Cohere-CommandA\",\n url: \"https://api.cohere.com/v2/chat\",\n model: \"command-a-reasoning-08-2025\",\n keys: loadKeys(\"COHERE_API_KEY\").concat(loadKeys(\"COHERE_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 4096, // reasoning model needs extra tokens for thinking + answer\n responseFormat: \"cohere\", // data.message.content[] array with {type:\"thinking\"} + {type:\"text\"}\n },\n];\n\n// Deduplicate keys within each provider\nfor (const p of PROVIDERS) {\n p.keys = [...new Set(p.keys)].filter(Boolean);\n}\n\n// Log available providers\nconst available = PROVIDERS.filter(p => p.keys.length > 0);\nconsole.log(`[SCORING] ${available.length} LLM providers available: ${available.map(p => `${p.name}(${p.keys.length})`).join(\", \")}`);\nif (available.length === 0) console.warn(\"[SCORING] No LLM providers — heuristic scoring only.\");\n\nconst SCORING_PROMPT = `You are an academic paper quality evaluator for the P2PCLAW decentralized research network.\n\nScore this paper on a 0-10 scale for each criterion. For EACH score, include a brief \"why\" (1 sentence explaining the score).\n\nScoring criteria:\n- abstract (0-10): Quality, clarity, and completeness of the abstract. Clear problem statement, scope, and key results summarized.\n- introduction (0-10): Problem statement clarity, context, motivation. Should cite 2+ related works.\n- methodology (0-10): Rigor, reproducibility, appropriate methods. Must describe steps clearly enough to reproduce.\n- results (0-10): Quality of findings, data presentation, significance. Quantitative data strongly preferred.\n- discussion (0-10): Interpretation depth, limitations acknowledged, implications for the field.\n- conclusion (0-10): Summary of key findings (expected, NOT penalized as repetition), concrete future work directions, and connection back to the research question. A good conclusion SHOULD summarize findings — that is standard scientific practice.\n- references (0-10): Citation quality, relevance, proper formatting (0 if no real references). Real author names, titles, years, DOIs/URLs expected.\n- novelty (0-10): Original contribution to the field. Novel frameworks, terminology, or approaches.\n- reproducibility (0-10): Could another researcher reproduce this work? Code, equations, detailed parameters help.\n- citation_quality (0-10): Are references real, relevant, and properly cited? 8+ unique real citations expected for high scores.\n\nIMPORTANT RULES:\n- If a section is MISSING entirely, score it 0\n- If references are placeholder/fake (e.g. \"[1] Author, A. (2026). Placeholder\"), score references as 1\n- Trivial proofs or papers under 300 words should score below 3 overall\n- Papers that prove obvious things (like 0*0=0) should score novelty as 0-1\n- Be STRICT — a score of 8+ means genuinely publishable quality\n- Lean 4 verified papers with valid proof hashes deserve reproducibility bonus\n\nReturn ONLY this JSON format (numbers 0-10, nothing else):\n{\"abstract\":N,\"introduction\":N,\"methodology\":N,\"results\":N,\"discussion\":N,\"conclusion\":N,\"references\":N,\"novelty\":N,\"reproducibility\":N,\"citation_quality\":N}\n\nPaper content:\n`;\n\n/**\n * Call a single LLM provider to score a paper.\n * Tries up to 2 keys from the provider before giving up.\n */\nasync function callLLMForScoring(prompt, provider) {\n if (!provider.keys.length) return null;\n\n const maxAttempts = Math.min(provider.keys.length, 2);\n for (let attempt = 0; attempt < maxAttempts; attempt++) {\n const key = nextKey(provider.id, provider.keys);\n if (!key) return null;\n\n const headers = { \"Content-Type\": \"application/json\" };\n headers[provider.authHeader] = `${provider.authPrefix}${key}`;\n if (provider.extraHeaders) Object.assign(headers, provider.extraHeaders);\n\n try {\n const res = await fetch(provider.url, {\n method: \"POST\",\n headers,\n body: JSON.stringify({\n model: provider.model,\n messages: [{ role: \"user\", content: prompt }],\n max_tokens: provider.maxTokens || 512,\n temperature: 0.1,\n }),\n signal: AbortSignal.timeout(30000),\n });\n\n if (res.status === 429) {\n console.warn(`[SCORING] ${provider.name} 429 rate-limited`);\n await new Promise(r => setTimeout(r, 3000));\n continue;\n }\n if (res.status === 402) {\n console.warn(`[SCORING] ${provider.name} 402 credits exhausted — skipping provider`);\n return null;\n }\n if (!res.ok) {\n const errBody = await res.text().catch(() => \"\");\n console.warn(`[SCORING] ${provider.name} HTTP ${res.status}: ${errBody.substring(0, 150)}`);\n continue;\n }\n\n const data = await res.json();\n // Support both OpenAI format (choices[0].message.content) and Cohere v2 (message.content[] array)\n let text = \"\";\n if (provider.responseFormat === \"cohere\") {\n // Cohere v2 returns array of content blocks: [{type:\"thinking\",...}, {type:\"text\",text:\"...\"}]\n const blocks = data.message?.content || [];\n if (Array.isArray(blocks)) {\n const textBlock = blocks.find(b => b.type === \"text\");\n text = textBlock?.text || blocks[blocks.length - 1]?.text || \"\";\n } else {\n text = typeof blocks === \"string\" ? blocks : \"\";\n }\n } else {\n text = data.choices?.[0]?.message?.content || \"\";\n }\n\n // Strip <think>...</think> tags (Sarvam, Qwen with thinking mode)\n if (provider.stripThinkTags || text.includes(\"<think>\")) {\n // Strip closed think blocks\n text = text.replace(/<think>[\\s\\S]*?<\\/think>/g, \"\").trim();\n // Strip unclosed think blocks (model ran out of tokens mid-think)\n if (text.startsWith(\"<think>\")) {\n text = text.replace(/<think>[\\s\\S]*/g, \"\").trim();\n }\n }\n\n // Extract JSON — use balanced braces to handle nested objects like {\"score\":8,\"why\":\"...\"}\n let jsonMatch = null;\n const firstBrace = text.indexOf('{');\n if (firstBrace !== -1) {\n let depth = 0, end = -1;\n for (let i = firstBrace; i < text.length; i++) {\n if (text[i] === '{') depth++;\n else if (text[i] === '}') { depth--; if (depth === 0) { end = i; break; } }\n }\n if (end !== -1) jsonMatch = [text.substring(firstBrace, end + 1)];\n }\n if (!jsonMatch) {\n console.warn(`[SCORING] ${provider.name} — no JSON in response`);\n continue;\n }\n\n const parsed = JSON.parse(jsonMatch[0]);\n const fields = [...SECTIONS, \"novelty\", \"reproducibility\", \"citation_quality\"];\n const feedback = {};\n for (const field of fields) {\n let val = parsed[field];\n // Support both plain number and {score, why} object format\n if (val && typeof val === \"object\" && typeof val.score === \"number\") {\n feedback[field] = val.why || null;\n val = val.score;\n }\n if (typeof val !== \"number\" || val < 0 || val > 10) {\n val = typeof val === \"number\" ? Math.max(0, Math.min(10, Math.round(val))) : 5;\n }\n parsed[field] = val;\n }\n\n // Clean parsed: only keep recognized score fields (remove stray \"why\" etc.)\n const cleanScores = {};\n for (const field of fields) cleanScores[field] = parsed[field];\n\n console.log(`[SCORING] ${provider.name} scored successfully`);\n return { scores: cleanScores, provider: provider.name, feedback: Object.keys(feedback).length > 0 ? feedback : null };\n } catch (e) {\n console.warn(`[SCORING] ${provider.name} error: ${e.message}`);\n }\n }\n return null;\n}\n\n/**\n * Deterministic heuristic scoring — used when all LLMs fail.\n */\nfunction heuristicScore(content) {\n const text = content || \"\";\n const words = text.split(/\\s+/).filter(w => w.length > 0);\n const wordCount = words.length;\n const lowerText = text.toLowerCase();\n\n const sectionScores = {};\n for (const section of SECTIONS) {\n const hasSection = lowerText.includes(`## ${section}`) || lowerText.includes(`# ${section}`);\n if (!hasSection) {\n sectionScores[section] = 0;\n continue;\n }\n const sectionRegex = new RegExp(`##?\\\\s*${section}[\\\\s\\\\S]*?(?=##?\\\\s|$)`, \"i\");\n const match = text.match(sectionRegex);\n const sectionWords = match ? match[0].split(/\\s+/).length : 0;\n if (sectionWords < 20) sectionScores[section] = 2;\n else if (sectionWords < 50) sectionScores[section] = 4;\n else if (sectionWords < 100) sectionScores[section] = 5;\n else if (sectionWords < 200) sectionScores[section] = 6;\n else sectionScores[section] = 7;\n }\n\n const refMatches = text.match(/\\[\\d+\\]/g) || [];\n const uniqueRefs = new Set(refMatches).size;\n const hasPlaceholderRefs = /placeholder|author,?\\s*a\\.\\s*\\(\\d{4}\\)/i.test(text);\n const hasRealAuthors = /[A-Z][a-z]+,\\s*[A-Z]\\.\\s*(?:&|,|et al)/g.test(text);\n const hasDOI = /doi\\.org|arxiv\\.org|10\\.\\d{4}/i.test(text);\n let refScore = hasPlaceholderRefs ? 1 : Math.min(7, uniqueRefs);\n if (hasRealAuthors) refScore = Math.min(10, refScore + 1);\n if (hasDOI) refScore = Math.min(10, refScore + 1);\n sectionScores.references = refScore;\n\n const technicalTerms = (text.match(/\\b(algorithm|theorem|proof|complexity|O\\([^)]+\\)|convergence|optimal|novel|framework)\\b/gi) || []).length;\n const hasFigures = /figure \\d|fig\\.\\s*\\d|table \\d/i.test(text);\n let novelty = wordCount > 2000 ? 4 : wordCount > 1000 ? 3 : 2;\n novelty += Math.min(3, Math.floor(technicalTerms / 5));\n if (hasFigures) novelty += 1;\n\n const hasCode = /```[\\s\\S]*?```/.test(text);\n const hasEquations = /\\$[^$]+\\$/.test(text) || /\\\\begin\\{/.test(text);\n const hasNumbers = /\\d+\\.\\d+%|\\d+\\.\\d+x|p\\s*[<>]\\s*0\\.\\d/i.test(text);\n let reproducibility = (hasCode ? 5 : 3) + (hasEquations ? 1 : 0) + (wordCount > 2000 ? 1 : 0);\n if (hasNumbers) reproducibility += 1;\n\n const citation_quality = hasPlaceholderRefs ? 1 : Math.min(6, uniqueRefs);\n\n return {\n scores: {\n ...sectionScores,\n novelty: Math.min(10, novelty),\n reproducibility: Math.min(10, reproducibility),\n citation_quality: Math.min(10, citation_quality),\n },\n provider: \"heuristic\"\n };\n}\n\n/**\n * Score a paper using heterogeneous multi-LLM swarm.\n * Tries to get 2 independent judges, falls back through provider chain.\n */\nexport async function scoreGranular(content, paperType = \"research\") {\n if (!content || content.trim().length < 50) {\n return {\n sections: Object.fromEntries(SECTIONS.map(s => [s, 0])),\n overall: 0,\n novelty: 0,\n reproducibility: 0,\n citation_quality: 0,\n judges: [],\n judge_count: 0,\n error: \"Content too short to score\"\n };\n }\n\n const truncated = content.length > 4000 ? content.substring(0, 4000) + \"\\n\\n[... truncated for scoring ...]\" : content;\n const prompt = SCORING_PROMPT + truncated;\n\n // ALL available judges score independently for maximum consensus diversity.\n // Each model evaluates each section independently, then we average.\n // Run all judges in parallel for speed (each has its own timeout).\n const judgePromises = available.map(provider =>\n callLLMForScoring(prompt, provider).catch(() => null)\n );\n const judgeResults = await Promise.all(judgePromises);\n const judges = judgeResults.filter(Boolean);\n\n // If no LLM judges succeeded, use heuristic\n if (judges.length === 0) {\n const heuristic = heuristicScore(content);\n judges.push(heuristic);\n }\n\n // Average scores across all judges\n const allFields = [...SECTIONS, \"novelty\", \"reproducibility\", \"citation_quality\"];\n const averaged = {};\n for (const field of allFields) {\n const values = judges.map(j => j.scores[field]).filter(v => typeof v === \"number\");\n averaged[field] = values.length > 0\n ? Math.round((values.reduce((a, b) => a + b, 0) / values.length) * 10) / 10\n : 0;\n }\n\n const sectionValues = SECTIONS.map(s => averaged[s]);\n const overall = Math.round((sectionValues.reduce((a, b) => a + b, 0) / SECTIONS.length) * 10) / 10;\n\n // Per-judge detail breakdown (individual scores + feedback)\n const judge_details = judges.map(j => ({\n judge: j.provider,\n scores: j.scores,\n feedback: j.feedback || null,\n }));\n\n // Consensus score per dimension (1 = all judges agree, 0 = total disagreement)\n const consensus = {};\n for (const field of allFields) {\n const values = judges.map(j => j.scores[field]).filter(v => typeof v === \"number\");\n if (values.length < 2) { consensus[field] = 1.0; continue; }\n const mean = values.reduce((a, b) => a + b, 0) / values.length;\n const variance = values.reduce((sum, v) => sum + (v - mean) ** 2, 0) / values.length;\n const stddev = Math.sqrt(variance);\n consensus[field] = Math.round(Math.max(0, 1 - stddev / 5) * 100) / 100;\n }\n const overall_consensus = Math.round(\n (allFields.reduce((sum, f) => sum + consensus[f], 0) / allFields.length) * 100\n ) / 100;\n\n // Aggregate feedback per dimension (from all judges that provided it)\n const aggregated_feedback = {};\n for (const field of allFields) {\n const comments = judges\n .filter(j => j.feedback && j.feedback[field])\n .map(j => ({ judge: j.provider, comment: j.feedback[field] }));\n if (comments.length > 0) aggregated_feedback[field] = comments;\n }\n\n const result = {\n sections: Object.fromEntries(SECTIONS.map(s => [s, averaged[s]])),\n overall,\n novelty: averaged.novelty,\n reproducibility: averaged.reproducibility,\n citation_quality: averaged.citation_quality,\n judges: judges.map(j => j.provider),\n judge_count: judges.length,\n judge_details,\n consensus,\n overall_consensus,\n feedback: Object.keys(aggregated_feedback).length > 0 ? aggregated_feedback : null,\n scored_at: new Date().toISOString(),\n paper_type: paperType,\n };\n\n console.log(`[SCORING] Granular score: overall=${overall}, consensus=${overall_consensus}, judges=${result.judges.join(\",\")}`);\n return result;\n}\n"}, "59d9781786f721f39ad311514195c38194fd7e94": {"t": 1785753953.0094159, "n": 20488, "h": "5597b257c2f30c55", "c": "/**\n * P2PCLAW Granular Scoring Service\n * =================================\n * Heterogeneous multi-LLM scoring engine that evaluates papers section-by-section.\n *\n * Provider chain (updated 2026-04-01):\n * 1. Cerebras — qwen-3-235b (3 keys, free, ultra-fast)\n * 2. Cerebras — llama3.1-8b (3 keys, free, ultra-fast)\n * 3. Mistral — mistral-small-latest (2 keys, free)\n * 4. Sarvam — sarvam-m (Indian AI, free)\n * 5. OpenRouter — qwen3-coder:free (free)\n * 6. Groq — llama-3.3-70b-versatile (7 keys, may be restricted)\n * 7. NVIDIA — meta/llama-3.3-70b-instruct (3 keys, free)\n * 8. Inception — mercury-2 (5 keys, free)\n * 9. Xiaomi MiMo — mimo-v2-flash (3 keys, free)\n * 10. Xiaomi MiMo — mimo-v2-pro (3 keys, free, reasoning)\n * 11. Cohere — command-a-reasoning (8 keys, reasoning model)\n * 12. Deterministic heuristic fallback (never blocks)\n *\n * ALL available judges score independently. Final score = average across all judges.\n * Each model evaluates each section independently for maximum consensus diversity.\n */\n\nconst SECTIONS = [\"abstract\", \"introduction\", \"methodology\", \"results\", \"discussion\", \"conclusion\", \"references\"];\n\n// ── Load keys with rotation ─────────────────────────────────────────────────\n\nfunction loadKeys(envPrefix, maxKeys = 10) {\n const keys = [];\n for (let i = 1; i <= maxKeys; i++) {\n const k = process.env[`${envPrefix}_${i}`] || process.env[`${envPrefix}${i}`];\n if (k) keys.push(k);\n }\n // Also check single-key env vars\n const single = process.env[envPrefix];\n if (single && !keys.includes(single)) keys.unshift(single);\n return keys;\n}\n\nconst keyIndices = {};\nfunction nextKey(providerId, keys) {\n if (!keys.length) return null;\n const idx = keyIndices[providerId] || 0;\n const key = keys[idx % keys.length];\n keyIndices[providerId] = (idx + 1) % keys.length;\n return key;\n}\n\n// ── Provider definitions ────────────────────────────────────────────────────\n\nconst PROVIDERS = [\n // --- Cerebras: 3 keys x 2 models = up to 6 independent judges ---\n {\n id: \"cerebras-qwen\",\n name: \"Cerebras-Qwen235B\",\n url: \"https://api.cerebras.ai/v1/chat/completions\",\n model: \"qwen-3-235b-a22b-instruct-2507\",\n keys: loadKeys(\"CEREBRAS_API_KEY\").concat(loadKeys(\"CEREBRAS_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 1024,\n },\n {\n id: \"cerebras-llama\",\n name: \"Cerebras-Llama8B\",\n url: \"https://api.cerebras.ai/v1/chat/completions\",\n model: \"llama3.1-8b\",\n keys: loadKeys(\"CEREBRAS_API_KEY\").concat(loadKeys(\"CEREBRAS_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n // --- Mistral: 2 keys, reliable ---\n {\n id: \"mistral\",\n name: \"Mistral\",\n url: \"https://api.mistral.ai/v1/chat/completions\",\n model: \"mistral-small-latest\",\n keys: loadKeys(\"MISTRAL_API_KEY\").concat(loadKeys(\"MISTRAL_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n // --- Sarvam (Indian AI): sarvam-m, uses <think> tags (needs 2048+ tokens) ---\n {\n id: \"sarvam\",\n name: \"Sarvam\",\n url: \"https://api.sarvam.ai/v1/chat/completions\",\n model: \"sarvam-m\",\n keys: loadKeys(\"SARVAM_KEY\").concat(loadKeys(\"SARVAM_API_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 2048,\n },\n // --- OpenRouter: free models ---\n {\n id: \"openrouter\",\n name: \"OpenRouter\",\n url: \"https://openrouter.ai/api/v1/chat/completions\",\n model: \"qwen/qwen3-coder:free\",\n keys: loadKeys(\"OPENROUTER_API_KEY\"),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n extraHeaders: { \"HTTP-Referer\": \"https://www.p2pclaw.com\", \"X-Title\": \"P2PCLAW Scoring\" },\n },\n // --- Groq: 7 keys but may be org-restricted ---\n {\n id: \"groq\",\n name: \"Groq\",\n url: \"https://api.groq.com/openai/v1/chat/completions\",\n model: \"llama-3.3-70b-versatile\",\n keys: loadKeys(\"GROQ_API_KEY\").concat(loadKeys(\"GROQ_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n // --- NVIDIA: 3 keys ---\n {\n id: \"nvidia\",\n name: \"NVIDIA\",\n url: \"https://integrate.api.nvidia.com/v1/chat/completions\",\n model: \"meta/llama-3.3-70b-instruct\",\n keys: loadKeys(\"NVAPI_KEY\").concat(loadKeys(\"NVIDIA_API_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n // --- Inception: mercury-2 ---\n {\n id: \"inception\",\n name: \"Inception\",\n url: \"https://api.inceptionlabs.ai/v1/chat/completions\",\n model: \"mercury-2\",\n keys: loadKeys(\"INCEPTION_API_KEY\").concat(loadKeys(\"INCEPTION_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n // --- Xiaomi MiMo: 3 keys x 2 models = up to 2 independent judges ---\n {\n id: \"xiaomi-flash\",\n name: \"Xiaomi-MiMo-Flash\",\n url: \"https://api.xiaomimimo.com/v1/chat/completions\",\n model: \"mimo-v2-flash\",\n keys: loadKeys(\"XIAOMI_API_KEY\").concat(loadKeys(\"XIAOMI_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n {\n id: \"xiaomi-pro\",\n name: \"Xiaomi-MiMo-Pro\",\n url: \"https://api.xiaomimimo.com/v1/chat/completions\",\n model: \"mimo-v2-pro\",\n keys: loadKeys(\"XIAOMI_API_KEY\").concat(loadKeys(\"XIAOMI_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 1024,\n },\n // --- Cohere: command-a-reasoning model (8 keys, reasoning/thinking model) ---\n {\n id: \"cohere\",\n name: \"Cohere-CommandA\",\n url: \"https://api.cohere.com/v2/chat\",\n model: \"command-a-reasoning-08-2025\",\n keys: loadKeys(\"COHERE_API_KEY\").concat(loadKeys(\"COHERE_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 1024,\n responseFormat: \"cohere\", // data.message.content[0].text instead of data.choices[0].message.content\n },\n];\n\n// Deduplicate keys within each provider\nfor (const p of PROVIDERS) {\n p.keys = [...new Set(p.keys)].filter(Boolean);\n}\n\n// Log available providers\nconst available = PROVIDERS.filter(p => p.keys.length > 0);\nconsole.log(`[SCORING] ${available.length} LLM providers available: ${available.map(p => `${p.name}(${p.keys.length})`).join(\", \")}`);\nif (available.length === 0) console.warn(\"[SCORING] No LLM providers — heuristic scoring only.\");\n\nconst SCORING_PROMPT = `You are an academic paper quality evaluator for the P2PCLAW decentralized research network.\n\nScore this paper on a 0-10 scale for each criterion. For EACH score, include a brief \"why\" (1 sentence explaining the score).\n\nScoring criteria:\n- abstract (0-10): Quality, clarity, and completeness of the abstract. Clear problem statement, scope, and key results summarized.\n- introduction (0-10): Problem statement clarity, context, motivation. Should cite 2+ related works.\n- methodology (0-10): Rigor, reproducibility, appropriate methods. Must describe steps clearly enough to reproduce.\n- results (0-10): Quality of findings, data presentation, significance. Quantitative data strongly preferred.\n- discussion (0-10): Interpretation depth, limitations acknowledged, implications for the field.\n- conclusion (0-10): Summary of key findings (expected, NOT penalized as repetition), concrete future work directions, and connection back to the research question. A good conclusion SHOULD summarize findings — that is standard scientific practice.\n- references (0-10): Citation quality, relevance, proper formatting (0 if no real references). Real author names, titles, years, DOIs/URLs expected.\n- novelty (0-10): Original contribution to the field. Novel frameworks, terminology, or approaches.\n- reproducibility (0-10): Could another researcher reproduce this work? Code, equations, detailed parameters help.\n- citation_quality (0-10): Are references real, relevant, and properly cited? 8+ unique real citations expected for high scores.\n\nIMPORTANT RULES:\n- If a section is MISSING entirely, score it 0\n- If references are placeholder/fake (e.g. \"[1] Author, A. (2026). Placeholder\"), score references as 1\n- Trivial proofs or papers under 300 words should score below 3 overall\n- Papers that prove obvious things (like 0*0=0) should score novelty as 0-1\n- Be STRICT — a score of 8+ means genuinely publishable quality\n- Lean 4 verified papers with valid proof hashes deserve reproducibility bonus\n\nReturn ONLY this JSON format (numbers 0-10, nothing else):\n{\"abstract\":N,\"introduction\":N,\"methodology\":N,\"results\":N,\"discussion\":N,\"conclusion\":N,\"references\":N,\"novelty\":N,\"reproducibility\":N,\"citation_quality\":N}\n\nPaper content:\n`;\n\n/**\n * Call a single LLM provider to score a paper.\n * Tries up to 2 keys from the provider before giving up.\n */\nasync function callLLMForScoring(prompt, provider) {\n if (!provider.keys.length) return null;\n\n const maxAttempts = Math.min(provider.keys.length, 2);\n for (let attempt = 0; attempt < maxAttempts; attempt++) {\n const key = nextKey(provider.id, provider.keys);\n if (!key) return null;\n\n const headers = { \"Content-Type\": \"application/json\" };\n headers[provider.authHeader] = `${provider.authPrefix}${key}`;\n if (provider.extraHeaders) Object.assign(headers, provider.extraHeaders);\n\n try {\n const res = await fetch(provider.url, {\n method: \"POST\",\n headers,\n body: JSON.stringify({\n model: provider.model,\n messages: [{ role: \"user\", content: prompt }],\n max_tokens: provider.maxTokens || 512,\n temperature: 0.1,\n }),\n signal: AbortSignal.timeout(30000),\n });\n\n if (res.status === 429) {\n console.warn(`[SCORING] ${provider.name} 429 rate-limited`);\n await new Promise(r => setTimeout(r, 3000));\n continue;\n }\n if (res.status === 402) {\n console.warn(`[SCORING] ${provider.name} 402 credits exhausted — skipping provider`);\n return null;\n }\n if (!res.ok) {\n const errBody = await res.text().catch(() => \"\");\n console.warn(`[SCORING] ${provider.name} HTTP ${res.status}: ${errBody.substring(0, 150)}`);\n continue;\n }\n\n const data = await res.json();\n // Support both OpenAI format (choices[0].message.content) and Cohere v2 (message.content[0].text)\n let text = \"\";\n if (provider.responseFormat === \"cohere\") {\n text = data.message?.content?.[0]?.text || data.message?.content || \"\";\n } else {\n text = data.choices?.[0]?.message?.content || \"\";\n }\n\n // Strip <think>...</think> tags (Sarvam, Qwen with thinking mode)\n if (provider.stripThinkTags || text.includes(\"<think>\")) {\n // Strip closed think blocks\n text = text.replace(/<think>[\\s\\S]*?<\\/think>/g, \"\").trim();\n // Strip unclosed think blocks (model ran out of tokens mid-think)\n if (text.startsWith(\"<think>\")) {\n text = text.replace(/<think>[\\s\\S]*/g, \"\").trim();\n }\n }\n\n // Extract JSON — use balanced braces to handle nested objects like {\"score\":8,\"why\":\"...\"}\n let jsonMatch = null;\n const firstBrace = text.indexOf('{');\n if (firstBrace !== -1) {\n let depth = 0, end = -1;\n for (let i = firstBrace; i < text.length; i++) {\n if (text[i] === '{') depth++;\n else if (text[i] === '}') { depth--; if (depth === 0) { end = i; break; } }\n }\n if (end !== -1) jsonMatch = [text.substring(firstBrace, end + 1)];\n }\n if (!jsonMatch) {\n console.warn(`[SCORING] ${provider.name} — no JSON in response`);\n continue;\n }\n\n const parsed = JSON.parse(jsonMatch[0]);\n const fields = [...SECTIONS, \"novelty\", \"reproducibility\", \"citation_quality\"];\n const feedback = {};\n for (const field of fields) {\n let val = parsed[field];\n // Support both plain number and {score, why} object format\n if (val && typeof val === \"object\" && typeof val.score === \"number\") {\n feedback[field] = val.why || null;\n val = val.score;\n }\n if (typeof val !== \"number\" || val < 0 || val > 10) {\n val = typeof val === \"number\" ? Math.max(0, Math.min(10, Math.round(val))) : 5;\n }\n parsed[field] = val;\n }\n\n // Clean parsed: only keep recognized score fields (remove stray \"why\" etc.)\n const cleanScores = {};\n for (const field of fields) cleanScores[field] = parsed[field];\n\n console.log(`[SCORING] ${provider.name} scored successfully`);\n return { scores: cleanScores, provider: provider.name, feedback: Object.keys(feedback).length > 0 ? feedback : null };\n } catch (e) {\n console.warn(`[SCORING] ${provider.name} error: ${e.message}`);\n }\n }\n return null;\n}\n\n/**\n * Deterministic heuristic scoring — used when all LLMs fail.\n */\nfunction heuristicScore(content) {\n const text = content || \"\";\n const words = text.split(/\\s+/).filter(w => w.length > 0);\n const wordCount = words.length;\n const lowerText = text.toLowerCase();\n\n const sectionScores = {};\n for (const section of SECTIONS) {\n const hasSection = lowerText.includes(`## ${section}`) || lowerText.includes(`# ${section}`);\n if (!hasSection) {\n sectionScores[section] = 0;\n continue;\n }\n const sectionRegex = new RegExp(`##?\\\\s*${section}[\\\\s\\\\S]*?(?=##?\\\\s|$)`, \"i\");\n const match = text.match(sectionRegex);\n const sectionWords = match ? match[0].split(/\\s+/).length : 0;\n if (sectionWords < 20) sectionScores[section] = 2;\n else if (sectionWords < 50) sectionScores[section] = 4;\n else if (sectionWords < 100) sectionScores[section] = 5;\n else if (sectionWords < 200) sectionScores[section] = 6;\n else sectionScores[section] = 7;\n }\n\n const refMatches = text.match(/\\[\\d+\\]/g) || [];\n const uniqueRefs = new Set(refMatches).size;\n const hasPlaceholderRefs = /placeholder|author,?\\s*a\\.\\s*\\(\\d{4}\\)/i.test(text);\n const hasRealAuthors = /[A-Z][a-z]+,\\s*[A-Z]\\.\\s*(?:&|,|et al)/g.test(text);\n const hasDOI = /doi\\.org|arxiv\\.org|10\\.\\d{4}/i.test(text);\n let refScore = hasPlaceholderRefs ? 1 : Math.min(7, uniqueRefs);\n if (hasRealAuthors) refScore = Math.min(10, refScore + 1);\n if (hasDOI) refScore = Math.min(10, refScore + 1);\n sectionScores.references = refScore;\n\n const technicalTerms = (text.match(/\\b(algorithm|theorem|proof|complexity|O\\([^)]+\\)|convergence|optimal|novel|framework)\\b/gi) || []).length;\n const hasFigures = /figure \\d|fig\\.\\s*\\d|table \\d/i.test(text);\n let novelty = wordCount > 2000 ? 4 : wordCount > 1000 ? 3 : 2;\n novelty += Math.min(3, Math.floor(technicalTerms / 5));\n if (hasFigures) novelty += 1;\n\n const hasCode = /```[\\s\\S]*?```/.test(text);\n const hasEquations = /\\$[^$]+\\$/.test(text) || /\\\\begin\\{/.test(text);\n const hasNumbers = /\\d+\\.\\d+%|\\d+\\.\\d+x|p\\s*[<>]\\s*0\\.\\d/i.test(text);\n let reproducibility = (hasCode ? 5 : 3) + (hasEquations ? 1 : 0) + (wordCount > 2000 ? 1 : 0);\n if (hasNumbers) reproducibility += 1;\n\n const citation_quality = hasPlaceholderRefs ? 1 : Math.min(6, uniqueRefs);\n\n return {\n scores: {\n ...sectionScores,\n novelty: Math.min(10, novelty),\n reproducibility: Math.min(10, reproducibility),\n citation_quality: Math.min(10, citation_quality),\n },\n provider: \"heuristic\"\n };\n}\n\n/**\n * Score a paper using heterogeneous multi-LLM swarm.\n * Tries to get 2 independent judges, falls back through provider chain.\n */\nexport async function scoreGranular(content, paperType = \"research\") {\n if (!content || content.trim().length < 50) {\n return {\n sections: Object.fromEntries(SECTIONS.map(s => [s, 0])),\n overall: 0,\n novelty: 0,\n reproducibility: 0,\n citation_quality: 0,\n judges: [],\n judge_count: 0,\n error: \"Content too short to score\"\n };\n }\n\n const truncated = content.length > 4000 ? content.substring(0, 4000) + \"\\n\\n[... truncated for scoring ...]\" : content;\n const prompt = SCORING_PROMPT + truncated;\n\n // ALL available judges score independently for maximum consensus diversity.\n // Each model evaluates each section independently, then we average.\n // Run all judges in parallel for speed (each has its own timeout).\n const judgePromises = available.map(provider =>\n callLLMForScoring(prompt, provider).catch(() => null)\n );\n const judgeResults = await Promise.all(judgePromises);\n const judges = judgeResults.filter(Boolean);\n\n // If no LLM judges succeeded, use heuristic\n if (judges.length === 0) {\n const heuristic = heuristicScore(content);\n judges.push(heuristic);\n }\n\n // Average scores across all judges\n const allFields = [...SECTIONS, \"novelty\", \"reproducibility\", \"citation_quality\"];\n const averaged = {};\n for (const field of allFields) {\n const values = judges.map(j => j.scores[field]).filter(v => typeof v === \"number\");\n averaged[field] = values.length > 0\n ? Math.round((values.reduce((a, b) => a + b, 0) / values.length) * 10) / 10\n : 0;\n }\n\n const sectionValues = SECTIONS.map(s => averaged[s]);\n const overall = Math.round((sectionValues.reduce((a, b) => a + b, 0) / SECTIONS.length) * 10) / 10;\n\n // Per-judge detail breakdown (individual scores + feedback)\n const judge_details = judges.map(j => ({\n judge: j.provider,\n scores: j.scores,\n feedback: j.feedback || null,\n }));\n\n // Consensus score per dimension (1 = all judges agree, 0 = total disagreement)\n const consensus = {};\n for (const field of allFields) {\n const values = judges.map(j => j.scores[field]).filter(v => typeof v === \"number\");\n if (values.length < 2) { consensus[field] = 1.0; continue; }\n const mean = values.reduce((a, b) => a + b, 0) / values.length;\n const variance = values.reduce((sum, v) => sum + (v - mean) ** 2, 0) / values.length;\n const stddev = Math.sqrt(variance);\n consensus[field] = Math.round(Math.max(0, 1 - stddev / 5) * 100) / 100;\n }\n const overall_consensus = Math.round(\n (allFields.reduce((sum, f) => sum + consensus[f], 0) / allFields.length) * 100\n ) / 100;\n\n // Aggregate feedback per dimension (from all judges that provided it)\n const aggregated_feedback = {};\n for (const field of allFields) {\n const comments = judges\n .filter(j => j.feedback && j.feedback[field])\n .map(j => ({ judge: j.provider, comment: j.feedback[field] }));\n if (comments.length > 0) aggregated_feedback[field] = comments;\n }\n\n const result = {\n sections: Object.fromEntries(SECTIONS.map(s => [s, averaged[s]])),\n overall,\n novelty: averaged.novelty,\n reproducibility: averaged.reproducibility,\n citation_quality: averaged.citation_quality,\n judges: judges.map(j => j.provider),\n judge_count: judges.length,\n judge_details,\n consensus,\n overall_consensus,\n feedback: Object.keys(aggregated_feedback).length > 0 ? aggregated_feedback : null,\n scored_at: new Date().toISOString(),\n paper_type: paperType,\n };\n\n console.log(`[SCORING] Granular score: overall=${overall}, consensus=${overall_consensus}, judges=${result.judges.join(\",\")}`);\n return result;\n}\n"}, "15a330b5b5e73649c9ce22279a04cabc6fc9c442": {"t": 1785753954.3638241, "n": 19777, "h": "6b94a7fa7201a1db", "c": "/**\n * P2PCLAW Granular Scoring Service\n * =================================\n * Heterogeneous multi-LLM scoring engine that evaluates papers section-by-section.\n *\n * Provider chain (updated 2026-04-01):\n * 1. Cerebras — qwen-3-235b (3 keys, free, ultra-fast)\n * 2. Cerebras — llama3.1-8b (3 keys, free, ultra-fast)\n * 3. Mistral — mistral-small-latest (2 keys, free)\n * 4. Sarvam — sarvam-m (Indian AI, free)\n * 5. OpenRouter — qwen3-coder:free (free)\n * 6. Groq — llama-3.3-70b-versatile (7 keys, may be restricted)\n * 7. NVIDIA — meta/llama-3.3-70b-instruct (3 keys, free)\n * 8. Inception — mercury-2 (5 keys, free)\n * 9. Xiaomi MiMo — mimo-v2-flash (3 keys, free)\n * 10. Xiaomi MiMo — mimo-v2-pro (3 keys, free, reasoning)\n * 11. Cohere — command-a-reasoning (8 keys, reasoning model)\n * 12. Deterministic heuristic fallback (never blocks)\n *\n * ALL available judges score independently. Final score = average across all judges.\n * Each model evaluates each section independently for maximum consensus diversity.\n */\n\nconst SECTIONS = [\"abstract\", \"introduction\", \"methodology\", \"results\", \"discussion\", \"conclusion\", \"references\"];\n\n// ── Load keys with rotation ─────────────────────────────────────────────────\n\nfunction loadKeys(envPrefix, maxKeys = 10) {\n const keys = [];\n for (let i = 1; i <= maxKeys; i++) {\n const k = process.env[`${envPrefix}_${i}`] || process.env[`${envPrefix}${i}`];\n if (k) keys.push(k);\n }\n // Also check single-key env vars\n const single = process.env[envPrefix];\n if (single && !keys.includes(single)) keys.unshift(single);\n return keys;\n}\n\nconst keyIndices = {};\nfunction nextKey(providerId, keys) {\n if (!keys.length) return null;\n const idx = keyIndices[providerId] || 0;\n const key = keys[idx % keys.length];\n keyIndices[providerId] = (idx + 1) % keys.length;\n return key;\n}\n\n// ── Provider definitions ────────────────────────────────────────────────────\n\nconst PROVIDERS = [\n // --- Cerebras: 3 keys x 2 models = up to 6 independent judges ---\n {\n id: \"cerebras-qwen\",\n name: \"Cerebras-Qwen235B\",\n url: \"https://api.cerebras.ai/v1/chat/completions\",\n model: \"qwen-3-235b-a22b-instruct-2507\",\n keys: loadKeys(\"CEREBRAS_API_KEY\").concat(loadKeys(\"CEREBRAS_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 1024,\n },\n {\n id: \"cerebras-llama\",\n name: \"Cerebras-Llama8B\",\n url: \"https://api.cerebras.ai/v1/chat/completions\",\n model: \"llama3.1-8b\",\n keys: loadKeys(\"CEREBRAS_API_KEY\").concat(loadKeys(\"CEREBRAS_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n // --- Mistral: 2 keys, reliable ---\n {\n id: \"mistral\",\n name: \"Mistral\",\n url: \"https://api.mistral.ai/v1/chat/completions\",\n model: \"mistral-small-latest\",\n keys: loadKeys(\"MISTRAL_API_KEY\").concat(loadKeys(\"MISTRAL_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n // --- Sarvam (Indian AI): sarvam-m, uses <think> tags (needs 2048+ tokens) ---\n {\n id: \"sarvam\",\n name: \"Sarvam\",\n url: \"https://api.sarvam.ai/v1/chat/completions\",\n model: \"sarvam-m\",\n keys: loadKeys(\"SARVAM_KEY\").concat(loadKeys(\"SARVAM_API_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 2048,\n },\n // --- OpenRouter: free models ---\n {\n id: \"openrouter\",\n name: \"OpenRouter\",\n url: \"https://openrouter.ai/api/v1/chat/completions\",\n model: \"qwen/qwen3-coder:free\",\n keys: loadKeys(\"OPENROUTER_API_KEY\"),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n extraHeaders: { \"HTTP-Referer\": \"https://www.p2pclaw.com\", \"X-Title\": \"P2PCLAW Scoring\" },\n },\n // --- Groq: 7 keys but may be org-restricted ---\n {\n id: \"groq\",\n name: \"Groq\",\n url: \"https://api.groq.com/openai/v1/chat/completions\",\n model: \"llama-3.3-70b-versatile\",\n keys: loadKeys(\"GROQ_API_KEY\").concat(loadKeys(\"GROQ_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n // --- NVIDIA: 3 keys ---\n {\n id: \"nvidia\",\n name: \"NVIDIA\",\n url: \"https://integrate.api.nvidia.com/v1/chat/completions\",\n model: \"meta/llama-3.3-70b-instruct\",\n keys: loadKeys(\"NVAPI_KEY\").concat(loadKeys(\"NVIDIA_API_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n // --- Inception: mercury-2 ---\n {\n id: \"inception\",\n name: \"Inception\",\n url: \"https://api.inceptionlabs.ai/v1/chat/completions\",\n model: \"mercury-2\",\n keys: loadKeys(\"INCEPTION_API_KEY\").concat(loadKeys(\"INCEPTION_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n // --- Xiaomi MiMo: 3 keys x 2 models = up to 2 independent judges ---\n {\n id: \"xiaomi-flash\",\n name: \"Xiaomi-MiMo-Flash\",\n url: \"https://api.xiaomimimo.com/v1/chat/completions\",\n model: \"mimo-v2-flash\",\n keys: loadKeys(\"XIAOMI_API_KEY\").concat(loadKeys(\"XIAOMI_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n {\n id: \"xiaomi-pro\",\n name: \"Xiaomi-MiMo-Pro\",\n url: \"https://api.xiaomimimo.com/v1/chat/completions\",\n model: \"mimo-v2-pro\",\n keys: loadKeys(\"XIAOMI_API_KEY\").concat(loadKeys(\"XIAOMI_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 1024,\n },\n // --- Cohere: command-a-reasoning model (8 keys, reasoning/thinking model) ---\n {\n id: \"cohere\",\n name: \"Cohere-CommandA\",\n url: \"https://api.cohere.com/v2/chat\",\n model: \"command-a-reasoning-08-2025\",\n keys: loadKeys(\"COHERE_API_KEY\").concat(loadKeys(\"COHERE_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 1024,\n responseFormat: \"cohere\", // data.message.content[0].text instead of data.choices[0].message.content\n },\n];\n\n// Deduplicate keys within each provider\nfor (const p of PROVIDERS) {\n p.keys = [...new Set(p.keys)].filter(Boolean);\n}\n\n// Log available providers\nconst available = PROVIDERS.filter(p => p.keys.length > 0);\nconsole.log(`[SCORING] ${available.length} LLM providers available: ${available.map(p => `${p.name}(${p.keys.length})`).join(\", \")}`);\nif (available.length === 0) console.warn(\"[SCORING] No LLM providers — heuristic scoring only.\");\n\nconst SCORING_PROMPT = `You are an academic paper quality evaluator for the P2PCLAW decentralized research network.\n\nScore this paper on a 0-10 scale for each criterion. For EACH score, include a brief \"why\" (1 sentence explaining the score).\n\nScoring criteria:\n- abstract (0-10): Quality, clarity, and completeness of the abstract. Clear problem statement, scope, and key results summarized.\n- introduction (0-10): Problem statement clarity, context, motivation. Should cite 2+ related works.\n- methodology (0-10): Rigor, reproducibility, appropriate methods. Must describe steps clearly enough to reproduce.\n- results (0-10): Quality of findings, data presentation, significance. Quantitative data strongly preferred.\n- discussion (0-10): Interpretation depth, limitations acknowledged, implications for the field.\n- conclusion (0-10): Summary of key findings (expected, NOT penalized as repetition), concrete future work directions, and connection back to the research question. A good conclusion SHOULD summarize findings — that is standard scientific practice.\n- references (0-10): Citation quality, relevance, proper formatting (0 if no real references). Real author names, titles, years, DOIs/URLs expected.\n- novelty (0-10): Original contribution to the field. Novel frameworks, terminology, or approaches.\n- reproducibility (0-10): Could another researcher reproduce this work? Code, equations, detailed parameters help.\n- citation_quality (0-10): Are references real, relevant, and properly cited? 8+ unique real citations expected for high scores.\n\nIMPORTANT RULES:\n- If a section is MISSING entirely, score it 0\n- If references are placeholder/fake (e.g. \"[1] Author, A. (2026). Placeholder\"), score references as 1\n- Trivial proofs or papers under 300 words should score below 3 overall\n- Papers that prove obvious things (like 0*0=0) should score novelty as 0-1\n- Be STRICT — a score of 8+ means genuinely publishable quality\n- Lean 4 verified papers with valid proof hashes deserve reproducibility bonus\n\nReturn ONLY this JSON format (each field can be a number OR {\"score\":N,\"why\":\"reason\"}):\n{\"abstract\":N,\"introduction\":N,\"methodology\":N,\"results\":N,\"discussion\":N,\"conclusion\":N,\"references\":N,\"novelty\":N,\"reproducibility\":N,\"citation_quality\":N}\n\nPaper content:\n`;\n\n/**\n * Call a single LLM provider to score a paper.\n * Tries up to 2 keys from the provider before giving up.\n */\nasync function callLLMForScoring(prompt, provider) {\n if (!provider.keys.length) return null;\n\n const maxAttempts = Math.min(provider.keys.length, 2);\n for (let attempt = 0; attempt < maxAttempts; attempt++) {\n const key = nextKey(provider.id, provider.keys);\n if (!key) return null;\n\n const headers = { \"Content-Type\": \"application/json\" };\n headers[provider.authHeader] = `${provider.authPrefix}${key}`;\n if (provider.extraHeaders) Object.assign(headers, provider.extraHeaders);\n\n try {\n const res = await fetch(provider.url, {\n method: \"POST\",\n headers,\n body: JSON.stringify({\n model: provider.model,\n messages: [{ role: \"user\", content: prompt }],\n max_tokens: provider.maxTokens || 512,\n temperature: 0.1,\n }),\n signal: AbortSignal.timeout(30000),\n });\n\n if (res.status === 429) {\n console.warn(`[SCORING] ${provider.name} 429 rate-limited`);\n await new Promise(r => setTimeout(r, 3000));\n continue;\n }\n if (res.status === 402) {\n console.warn(`[SCORING] ${provider.name} 402 credits exhausted — skipping provider`);\n return null;\n }\n if (!res.ok) {\n const errBody = await res.text().catch(() => \"\");\n console.warn(`[SCORING] ${provider.name} HTTP ${res.status}: ${errBody.substring(0, 150)}`);\n continue;\n }\n\n const data = await res.json();\n // Support both OpenAI format (choices[0].message.content) and Cohere v2 (message.content[0].text)\n let text = \"\";\n if (provider.responseFormat === \"cohere\") {\n text = data.message?.content?.[0]?.text || data.message?.content || \"\";\n } else {\n text = data.choices?.[0]?.message?.content || \"\";\n }\n\n // Strip <think>...</think> tags (Sarvam, Qwen with thinking mode)\n if (provider.stripThinkTags || text.includes(\"<think>\")) {\n // Strip closed think blocks\n text = text.replace(/<think>[\\s\\S]*?<\\/think>/g, \"\").trim();\n // Strip unclosed think blocks (model ran out of tokens mid-think)\n if (text.startsWith(\"<think>\")) {\n text = text.replace(/<think>[\\s\\S]*/g, \"\").trim();\n }\n }\n\n const jsonMatch = text.match(/\\{[\\s\\S]*?\\}/);\n if (!jsonMatch) {\n console.warn(`[SCORING] ${provider.name} — no JSON in response`);\n continue;\n }\n\n const parsed = JSON.parse(jsonMatch[0]);\n const fields = [...SECTIONS, \"novelty\", \"reproducibility\", \"citation_quality\"];\n const feedback = {};\n for (const field of fields) {\n let val = parsed[field];\n // Support both plain number and {score, why} object format\n if (val && typeof val === \"object\" && typeof val.score === \"number\") {\n feedback[field] = val.why || null;\n val = val.score;\n }\n if (typeof val !== \"number\" || val < 0 || val > 10) {\n val = typeof val === \"number\" ? Math.max(0, Math.min(10, Math.round(val))) : 5;\n }\n parsed[field] = val;\n }\n\n console.log(`[SCORING] ${provider.name} scored successfully`);\n return { scores: parsed, provider: provider.name, feedback: Object.keys(feedback).length > 0 ? feedback : null };\n } catch (e) {\n console.warn(`[SCORING] ${provider.name} error: ${e.message}`);\n }\n }\n return null;\n}\n\n/**\n * Deterministic heuristic scoring — used when all LLMs fail.\n */\nfunction heuristicScore(content) {\n const text = content || \"\";\n const words = text.split(/\\s+/).filter(w => w.length > 0);\n const wordCount = words.length;\n const lowerText = text.toLowerCase();\n\n const sectionScores = {};\n for (const section of SECTIONS) {\n const hasSection = lowerText.includes(`## ${section}`) || lowerText.includes(`# ${section}`);\n if (!hasSection) {\n sectionScores[section] = 0;\n continue;\n }\n const sectionRegex = new RegExp(`##?\\\\s*${section}[\\\\s\\\\S]*?(?=##?\\\\s|$)`, \"i\");\n const match = text.match(sectionRegex);\n const sectionWords = match ? match[0].split(/\\s+/).length : 0;\n if (sectionWords < 20) sectionScores[section] = 2;\n else if (sectionWords < 50) sectionScores[section] = 4;\n else if (sectionWords < 100) sectionScores[section] = 5;\n else if (sectionWords < 200) sectionScores[section] = 6;\n else sectionScores[section] = 7;\n }\n\n const refMatches = text.match(/\\[\\d+\\]/g) || [];\n const uniqueRefs = new Set(refMatches).size;\n const hasPlaceholderRefs = /placeholder|author,?\\s*a\\.\\s*\\(\\d{4}\\)/i.test(text);\n const hasRealAuthors = /[A-Z][a-z]+,\\s*[A-Z]\\.\\s*(?:&|,|et al)/g.test(text);\n const hasDOI = /doi\\.org|arxiv\\.org|10\\.\\d{4}/i.test(text);\n let refScore = hasPlaceholderRefs ? 1 : Math.min(7, uniqueRefs);\n if (hasRealAuthors) refScore = Math.min(10, refScore + 1);\n if (hasDOI) refScore = Math.min(10, refScore + 1);\n sectionScores.references = refScore;\n\n const technicalTerms = (text.match(/\\b(algorithm|theorem|proof|complexity|O\\([^)]+\\)|convergence|optimal|novel|framework)\\b/gi) || []).length;\n const hasFigures = /figure \\d|fig\\.\\s*\\d|table \\d/i.test(text);\n let novelty = wordCount > 2000 ? 4 : wordCount > 1000 ? 3 : 2;\n novelty += Math.min(3, Math.floor(technicalTerms / 5));\n if (hasFigures) novelty += 1;\n\n const hasCode = /```[\\s\\S]*?```/.test(text);\n const hasEquations = /\\$[^$]+\\$/.test(text) || /\\\\begin\\{/.test(text);\n const hasNumbers = /\\d+\\.\\d+%|\\d+\\.\\d+x|p\\s*[<>]\\s*0\\.\\d/i.test(text);\n let reproducibility = (hasCode ? 5 : 3) + (hasEquations ? 1 : 0) + (wordCount > 2000 ? 1 : 0);\n if (hasNumbers) reproducibility += 1;\n\n const citation_quality = hasPlaceholderRefs ? 1 : Math.min(6, uniqueRefs);\n\n return {\n scores: {\n ...sectionScores,\n novelty: Math.min(10, novelty),\n reproducibility: Math.min(10, reproducibility),\n citation_quality: Math.min(10, citation_quality),\n },\n provider: \"heuristic\"\n };\n}\n\n/**\n * Score a paper using heterogeneous multi-LLM swarm.\n * Tries to get 2 independent judges, falls back through provider chain.\n */\nexport async function scoreGranular(content, paperType = \"research\") {\n if (!content || content.trim().length < 50) {\n return {\n sections: Object.fromEntries(SECTIONS.map(s => [s, 0])),\n overall: 0,\n novelty: 0,\n reproducibility: 0,\n citation_quality: 0,\n judges: [],\n judge_count: 0,\n error: \"Content too short to score\"\n };\n }\n\n const truncated = content.length > 4000 ? content.substring(0, 4000) + \"\\n\\n[... truncated for scoring ...]\" : content;\n const prompt = SCORING_PROMPT + truncated;\n\n // ALL available judges score independently for maximum consensus diversity.\n // Each model evaluates each section independently, then we average.\n // Run all judges in parallel for speed (each has its own timeout).\n const judgePromises = available.map(provider =>\n callLLMForScoring(prompt, provider).catch(() => null)\n );\n const judgeResults = await Promise.all(judgePromises);\n const judges = judgeResults.filter(Boolean);\n\n // If no LLM judges succeeded, use heuristic\n if (judges.length === 0) {\n const heuristic = heuristicScore(content);\n judges.push(heuristic);\n }\n\n // Average scores across all judges\n const allFields = [...SECTIONS, \"novelty\", \"reproducibility\", \"citation_quality\"];\n const averaged = {};\n for (const field of allFields) {\n const values = judges.map(j => j.scores[field]).filter(v => typeof v === \"number\");\n averaged[field] = values.length > 0\n ? Math.round((values.reduce((a, b) => a + b, 0) / values.length) * 10) / 10\n : 0;\n }\n\n const sectionValues = SECTIONS.map(s => averaged[s]);\n const overall = Math.round((sectionValues.reduce((a, b) => a + b, 0) / SECTIONS.length) * 10) / 10;\n\n // Per-judge detail breakdown (individual scores + feedback)\n const judge_details = judges.map(j => ({\n judge: j.provider,\n scores: j.scores,\n feedback: j.feedback || null,\n }));\n\n // Consensus score per dimension (1 = all judges agree, 0 = total disagreement)\n const consensus = {};\n for (const field of allFields) {\n const values = judges.map(j => j.scores[field]).filter(v => typeof v === \"number\");\n if (values.length < 2) { consensus[field] = 1.0; continue; }\n const mean = values.reduce((a, b) => a + b, 0) / values.length;\n const variance = values.reduce((sum, v) => sum + (v - mean) ** 2, 0) / values.length;\n const stddev = Math.sqrt(variance);\n consensus[field] = Math.round(Math.max(0, 1 - stddev / 5) * 100) / 100;\n }\n const overall_consensus = Math.round(\n (allFields.reduce((sum, f) => sum + consensus[f], 0) / allFields.length) * 100\n ) / 100;\n\n // Aggregate feedback per dimension (from all judges that provided it)\n const aggregated_feedback = {};\n for (const field of allFields) {\n const comments = judges\n .filter(j => j.feedback && j.feedback[field])\n .map(j => ({ judge: j.provider, comment: j.feedback[field] }));\n if (comments.length > 0) aggregated_feedback[field] = comments;\n }\n\n const result = {\n sections: Object.fromEntries(SECTIONS.map(s => [s, averaged[s]])),\n overall,\n novelty: averaged.novelty,\n reproducibility: averaged.reproducibility,\n citation_quality: averaged.citation_quality,\n judges: judges.map(j => j.provider),\n judge_count: judges.length,\n judge_details,\n consensus,\n overall_consensus,\n feedback: Object.keys(aggregated_feedback).length > 0 ? aggregated_feedback : null,\n scored_at: new Date().toISOString(),\n paper_type: paperType,\n };\n\n console.log(`[SCORING] Granular score: overall=${overall}, consensus=${overall_consensus}, judges=${result.judges.join(\",\")}`);\n return result;\n}\n"}, "e5c9444dbac8757b21fedb96e386b6ede5b20504": {"t": 1785753955.9794543, "n": 18841, "h": "34b3fe48e0a4cced", "c": "/**\n * P2PCLAW Granular Scoring Service\n * =================================\n * Heterogeneous multi-LLM scoring engine that evaluates papers section-by-section.\n *\n * Provider chain (updated 2026-04-01):\n * 1. Cerebras — qwen-3-235b (3 keys, free, ultra-fast)\n * 2. Cerebras — llama3.1-8b (3 keys, free, ultra-fast)\n * 3. Mistral — mistral-small-latest (2 keys, free)\n * 4. Sarvam — sarvam-m (Indian AI, free)\n * 5. OpenRouter — qwen3-coder:free (free)\n * 6. Groq — llama-3.3-70b-versatile (7 keys, may be restricted)\n * 7. NVIDIA — meta/llama-3.3-70b-instruct (3 keys, free)\n * 8. Inception — mercury-2 (5 keys, free)\n * 9. Xiaomi MiMo — mimo-v2-flash (3 keys, free)\n * 10. Xiaomi MiMo — mimo-v2-pro (3 keys, free, reasoning)\n * 11. Deterministic heuristic fallback (never blocks)\n *\n * ALL available judges score independently. Final score = average across all judges.\n * Each model evaluates each section independently for maximum consensus diversity.\n */\n\nconst SECTIONS = [\"abstract\", \"introduction\", \"methodology\", \"results\", \"discussion\", \"conclusion\", \"references\"];\n\n// ── Load keys with rotation ─────────────────────────────────────────────────\n\nfunction loadKeys(envPrefix, maxKeys = 10) {\n const keys = [];\n for (let i = 1; i <= maxKeys; i++) {\n const k = process.env[`${envPrefix}_${i}`] || process.env[`${envPrefix}${i}`];\n if (k) keys.push(k);\n }\n // Also check single-key env vars\n const single = process.env[envPrefix];\n if (single && !keys.includes(single)) keys.unshift(single);\n return keys;\n}\n\nconst keyIndices = {};\nfunction nextKey(providerId, keys) {\n if (!keys.length) return null;\n const idx = keyIndices[providerId] || 0;\n const key = keys[idx % keys.length];\n keyIndices[providerId] = (idx + 1) % keys.length;\n return key;\n}\n\n// ── Provider definitions ────────────────────────────────────────────────────\n\nconst PROVIDERS = [\n // --- Cerebras: 3 keys x 2 models = up to 6 independent judges ---\n {\n id: \"cerebras-qwen\",\n name: \"Cerebras-Qwen235B\",\n url: \"https://api.cerebras.ai/v1/chat/completions\",\n model: \"qwen-3-235b-a22b-instruct-2507\",\n keys: loadKeys(\"CEREBRAS_API_KEY\").concat(loadKeys(\"CEREBRAS_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 1024,\n },\n {\n id: \"cerebras-llama\",\n name: \"Cerebras-Llama8B\",\n url: \"https://api.cerebras.ai/v1/chat/completions\",\n model: \"llama3.1-8b\",\n keys: loadKeys(\"CEREBRAS_API_KEY\").concat(loadKeys(\"CEREBRAS_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n // --- Mistral: 2 keys, reliable ---\n {\n id: \"mistral\",\n name: \"Mistral\",\n url: \"https://api.mistral.ai/v1/chat/completions\",\n model: \"mistral-small-latest\",\n keys: loadKeys(\"MISTRAL_API_KEY\").concat(loadKeys(\"MISTRAL_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n // --- Sarvam (Indian AI): sarvam-m, uses <think> tags (needs 2048+ tokens) ---\n {\n id: \"sarvam\",\n name: \"Sarvam\",\n url: \"https://api.sarvam.ai/v1/chat/completions\",\n model: \"sarvam-m\",\n keys: loadKeys(\"SARVAM_KEY\").concat(loadKeys(\"SARVAM_API_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 2048,\n },\n // --- OpenRouter: free models ---\n {\n id: \"openrouter\",\n name: \"OpenRouter\",\n url: \"https://openrouter.ai/api/v1/chat/completions\",\n model: \"qwen/qwen3-coder:free\",\n keys: loadKeys(\"OPENROUTER_API_KEY\"),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n extraHeaders: { \"HTTP-Referer\": \"https://www.p2pclaw.com\", \"X-Title\": \"P2PCLAW Scoring\" },\n },\n // --- Groq: 7 keys but may be org-restricted ---\n {\n id: \"groq\",\n name: \"Groq\",\n url: \"https://api.groq.com/openai/v1/chat/completions\",\n model: \"llama-3.3-70b-versatile\",\n keys: loadKeys(\"GROQ_API_KEY\").concat(loadKeys(\"GROQ_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n // --- NVIDIA: 3 keys ---\n {\n id: \"nvidia\",\n name: \"NVIDIA\",\n url: \"https://integrate.api.nvidia.com/v1/chat/completions\",\n model: \"meta/llama-3.3-70b-instruct\",\n keys: loadKeys(\"NVAPI_KEY\").concat(loadKeys(\"NVIDIA_API_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n // --- Inception: mercury-2 ---\n {\n id: \"inception\",\n name: \"Inception\",\n url: \"https://api.inceptionlabs.ai/v1/chat/completions\",\n model: \"mercury-2\",\n keys: loadKeys(\"INCEPTION_API_KEY\").concat(loadKeys(\"INCEPTION_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n // --- Xiaomi MiMo: 3 keys x 2 models = up to 2 independent judges ---\n {\n id: \"xiaomi-flash\",\n name: \"Xiaomi-MiMo-Flash\",\n url: \"https://api.xiaomimimo.com/v1/chat/completions\",\n model: \"mimo-v2-flash\",\n keys: loadKeys(\"XIAOMI_API_KEY\").concat(loadKeys(\"XIAOMI_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n {\n id: \"xiaomi-pro\",\n name: \"Xiaomi-MiMo-Pro\",\n url: \"https://api.xiaomimimo.com/v1/chat/completions\",\n model: \"mimo-v2-pro\",\n keys: loadKeys(\"XIAOMI_API_KEY\").concat(loadKeys(\"XIAOMI_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 1024,\n },\n];\n\n// Deduplicate keys within each provider\nfor (const p of PROVIDERS) {\n p.keys = [...new Set(p.keys)].filter(Boolean);\n}\n\n// Log available providers\nconst available = PROVIDERS.filter(p => p.keys.length > 0);\nconsole.log(`[SCORING] ${available.length} LLM providers available: ${available.map(p => `${p.name}(${p.keys.length})`).join(\", \")}`);\nif (available.length === 0) console.warn(\"[SCORING] No LLM providers — heuristic scoring only.\");\n\nconst SCORING_PROMPT = `You are an academic paper quality evaluator for the P2PCLAW decentralized research network.\n\nScore this paper on a 0-10 scale for each criterion. For EACH score, include a brief \"why\" (1 sentence explaining the score).\n\nScoring criteria:\n- abstract (0-10): Quality, clarity, and completeness of the abstract. Clear problem statement, scope, and key results summarized.\n- introduction (0-10): Problem statement clarity, context, motivation. Should cite 2+ related works.\n- methodology (0-10): Rigor, reproducibility, appropriate methods. Must describe steps clearly enough to reproduce.\n- results (0-10): Quality of findings, data presentation, significance. Quantitative data strongly preferred.\n- discussion (0-10): Interpretation depth, limitations acknowledged, implications for the field.\n- conclusion (0-10): Summary of key findings (expected, NOT penalized as repetition), concrete future work directions, and connection back to the research question. A good conclusion SHOULD summarize findings — that is standard scientific practice.\n- references (0-10): Citation quality, relevance, proper formatting (0 if no real references). Real author names, titles, years, DOIs/URLs expected.\n- novelty (0-10): Original contribution to the field. Novel frameworks, terminology, or approaches.\n- reproducibility (0-10): Could another researcher reproduce this work? Code, equations, detailed parameters help.\n- citation_quality (0-10): Are references real, relevant, and properly cited? 8+ unique real citations expected for high scores.\n\nIMPORTANT RULES:\n- If a section is MISSING entirely, score it 0\n- If references are placeholder/fake (e.g. \"[1] Author, A. (2026). Placeholder\"), score references as 1\n- Trivial proofs or papers under 300 words should score below 3 overall\n- Papers that prove obvious things (like 0*0=0) should score novelty as 0-1\n- Be STRICT — a score of 8+ means genuinely publishable quality\n- Lean 4 verified papers with valid proof hashes deserve reproducibility bonus\n\nReturn ONLY this JSON format (each field can be a number OR {\"score\":N,\"why\":\"reason\"}):\n{\"abstract\":N,\"introduction\":N,\"methodology\":N,\"results\":N,\"discussion\":N,\"conclusion\":N,\"references\":N,\"novelty\":N,\"reproducibility\":N,\"citation_quality\":N}\n\nPaper content:\n`;\n\n/**\n * Call a single LLM provider to score a paper.\n * Tries up to 2 keys from the provider before giving up.\n */\nasync function callLLMForScoring(prompt, provider) {\n if (!provider.keys.length) return null;\n\n const maxAttempts = Math.min(provider.keys.length, 2);\n for (let attempt = 0; attempt < maxAttempts; attempt++) {\n const key = nextKey(provider.id, provider.keys);\n if (!key) return null;\n\n const headers = { \"Content-Type\": \"application/json\" };\n headers[provider.authHeader] = `${provider.authPrefix}${key}`;\n if (provider.extraHeaders) Object.assign(headers, provider.extraHeaders);\n\n try {\n const res = await fetch(provider.url, {\n method: \"POST\",\n headers,\n body: JSON.stringify({\n model: provider.model,\n messages: [{ role: \"user\", content: prompt }],\n max_tokens: provider.maxTokens || 512,\n temperature: 0.1,\n }),\n signal: AbortSignal.timeout(30000),\n });\n\n if (res.status === 429) {\n console.warn(`[SCORING] ${provider.name} 429 rate-limited`);\n await new Promise(r => setTimeout(r, 3000));\n continue;\n }\n if (res.status === 402) {\n console.warn(`[SCORING] ${provider.name} 402 credits exhausted — skipping provider`);\n return null;\n }\n if (!res.ok) {\n const errBody = await res.text().catch(() => \"\");\n console.warn(`[SCORING] ${provider.name} HTTP ${res.status}: ${errBody.substring(0, 150)}`);\n continue;\n }\n\n const data = await res.json();\n let text = data.choices?.[0]?.message?.content || \"\";\n\n // Strip <think>...</think> tags (Sarvam, Qwen with thinking mode)\n if (provider.stripThinkTags || text.includes(\"<think>\")) {\n // Strip closed think blocks\n text = text.replace(/<think>[\\s\\S]*?<\\/think>/g, \"\").trim();\n // Strip unclosed think blocks (model ran out of tokens mid-think)\n if (text.startsWith(\"<think>\")) {\n text = text.replace(/<think>[\\s\\S]*/g, \"\").trim();\n }\n }\n\n const jsonMatch = text.match(/\\{[\\s\\S]*?\\}/);\n if (!jsonMatch) {\n console.warn(`[SCORING] ${provider.name} — no JSON in response`);\n continue;\n }\n\n const parsed = JSON.parse(jsonMatch[0]);\n const fields = [...SECTIONS, \"novelty\", \"reproducibility\", \"citation_quality\"];\n const feedback = {};\n for (const field of fields) {\n let val = parsed[field];\n // Support both plain number and {score, why} object format\n if (val && typeof val === \"object\" && typeof val.score === \"number\") {\n feedback[field] = val.why || null;\n val = val.score;\n }\n if (typeof val !== \"number\" || val < 0 || val > 10) {\n val = typeof val === \"number\" ? Math.max(0, Math.min(10, Math.round(val))) : 5;\n }\n parsed[field] = val;\n }\n\n console.log(`[SCORING] ${provider.name} scored successfully`);\n return { scores: parsed, provider: provider.name, feedback: Object.keys(feedback).length > 0 ? feedback : null };\n } catch (e) {\n console.warn(`[SCORING] ${provider.name} error: ${e.message}`);\n }\n }\n return null;\n}\n\n/**\n * Deterministic heuristic scoring — used when all LLMs fail.\n */\nfunction heuristicScore(content) {\n const text = content || \"\";\n const words = text.split(/\\s+/).filter(w => w.length > 0);\n const wordCount = words.length;\n const lowerText = text.toLowerCase();\n\n const sectionScores = {};\n for (const section of SECTIONS) {\n const hasSection = lowerText.includes(`## ${section}`) || lowerText.includes(`# ${section}`);\n if (!hasSection) {\n sectionScores[section] = 0;\n continue;\n }\n const sectionRegex = new RegExp(`##?\\\\s*${section}[\\\\s\\\\S]*?(?=##?\\\\s|$)`, \"i\");\n const match = text.match(sectionRegex);\n const sectionWords = match ? match[0].split(/\\s+/).length : 0;\n if (sectionWords < 20) sectionScores[section] = 2;\n else if (sectionWords < 50) sectionScores[section] = 4;\n else if (sectionWords < 100) sectionScores[section] = 5;\n else if (sectionWords < 200) sectionScores[section] = 6;\n else sectionScores[section] = 7;\n }\n\n const refMatches = text.match(/\\[\\d+\\]/g) || [];\n const uniqueRefs = new Set(refMatches).size;\n const hasPlaceholderRefs = /placeholder|author,?\\s*a\\.\\s*\\(\\d{4}\\)/i.test(text);\n const hasRealAuthors = /[A-Z][a-z]+,\\s*[A-Z]\\.\\s*(?:&|,|et al)/g.test(text);\n const hasDOI = /doi\\.org|arxiv\\.org|10\\.\\d{4}/i.test(text);\n let refScore = hasPlaceholderRefs ? 1 : Math.min(7, uniqueRefs);\n if (hasRealAuthors) refScore = Math.min(10, refScore + 1);\n if (hasDOI) refScore = Math.min(10, refScore + 1);\n sectionScores.references = refScore;\n\n const technicalTerms = (text.match(/\\b(algorithm|theorem|proof|complexity|O\\([^)]+\\)|convergence|optimal|novel|framework)\\b/gi) || []).length;\n const hasFigures = /figure \\d|fig\\.\\s*\\d|table \\d/i.test(text);\n let novelty = wordCount > 2000 ? 4 : wordCount > 1000 ? 3 : 2;\n novelty += Math.min(3, Math.floor(technicalTerms / 5));\n if (hasFigures) novelty += 1;\n\n const hasCode = /```[\\s\\S]*?```/.test(text);\n const hasEquations = /\\$[^$]+\\$/.test(text) || /\\\\begin\\{/.test(text);\n const hasNumbers = /\\d+\\.\\d+%|\\d+\\.\\d+x|p\\s*[<>]\\s*0\\.\\d/i.test(text);\n let reproducibility = (hasCode ? 5 : 3) + (hasEquations ? 1 : 0) + (wordCount > 2000 ? 1 : 0);\n if (hasNumbers) reproducibility += 1;\n\n const citation_quality = hasPlaceholderRefs ? 1 : Math.min(6, uniqueRefs);\n\n return {\n scores: {\n ...sectionScores,\n novelty: Math.min(10, novelty),\n reproducibility: Math.min(10, reproducibility),\n citation_quality: Math.min(10, citation_quality),\n },\n provider: \"heuristic\"\n };\n}\n\n/**\n * Score a paper using heterogeneous multi-LLM swarm.\n * Tries to get 2 independent judges, falls back through provider chain.\n */\nexport async function scoreGranular(content, paperType = \"research\") {\n if (!content || content.trim().length < 50) {\n return {\n sections: Object.fromEntries(SECTIONS.map(s => [s, 0])),\n overall: 0,\n novelty: 0,\n reproducibility: 0,\n citation_quality: 0,\n judges: [],\n judge_count: 0,\n error: \"Content too short to score\"\n };\n }\n\n const truncated = content.length > 4000 ? content.substring(0, 4000) + \"\\n\\n[... truncated for scoring ...]\" : content;\n const prompt = SCORING_PROMPT + truncated;\n\n // ALL available judges score independently for maximum consensus diversity.\n // Each model evaluates each section independently, then we average.\n // Run all judges in parallel for speed (each has its own timeout).\n const judgePromises = available.map(provider =>\n callLLMForScoring(prompt, provider).catch(() => null)\n );\n const judgeResults = await Promise.all(judgePromises);\n const judges = judgeResults.filter(Boolean);\n\n // If no LLM judges succeeded, use heuristic\n if (judges.length === 0) {\n const heuristic = heuristicScore(content);\n judges.push(heuristic);\n }\n\n // Average scores across all judges\n const allFields = [...SECTIONS, \"novelty\", \"reproducibility\", \"citation_quality\"];\n const averaged = {};\n for (const field of allFields) {\n const values = judges.map(j => j.scores[field]).filter(v => typeof v === \"number\");\n averaged[field] = values.length > 0\n ? Math.round((values.reduce((a, b) => a + b, 0) / values.length) * 10) / 10\n : 0;\n }\n\n const sectionValues = SECTIONS.map(s => averaged[s]);\n const overall = Math.round((sectionValues.reduce((a, b) => a + b, 0) / SECTIONS.length) * 10) / 10;\n\n // Per-judge detail breakdown (individual scores + feedback)\n const judge_details = judges.map(j => ({\n judge: j.provider,\n scores: j.scores,\n feedback: j.feedback || null,\n }));\n\n // Consensus score per dimension (1 = all judges agree, 0 = total disagreement)\n const consensus = {};\n for (const field of allFields) {\n const values = judges.map(j => j.scores[field]).filter(v => typeof v === \"number\");\n if (values.length < 2) { consensus[field] = 1.0; continue; }\n const mean = values.reduce((a, b) => a + b, 0) / values.length;\n const variance = values.reduce((sum, v) => sum + (v - mean) ** 2, 0) / values.length;\n const stddev = Math.sqrt(variance);\n consensus[field] = Math.round(Math.max(0, 1 - stddev / 5) * 100) / 100;\n }\n const overall_consensus = Math.round(\n (allFields.reduce((sum, f) => sum + consensus[f], 0) / allFields.length) * 100\n ) / 100;\n\n // Aggregate feedback per dimension (from all judges that provided it)\n const aggregated_feedback = {};\n for (const field of allFields) {\n const comments = judges\n .filter(j => j.feedback && j.feedback[field])\n .map(j => ({ judge: j.provider, comment: j.feedback[field] }));\n if (comments.length > 0) aggregated_feedback[field] = comments;\n }\n\n const result = {\n sections: Object.fromEntries(SECTIONS.map(s => [s, averaged[s]])),\n overall,\n novelty: averaged.novelty,\n reproducibility: averaged.reproducibility,\n citation_quality: averaged.citation_quality,\n judges: judges.map(j => j.provider),\n judge_count: judges.length,\n judge_details,\n consensus,\n overall_consensus,\n feedback: Object.keys(aggregated_feedback).length > 0 ? aggregated_feedback : null,\n scored_at: new Date().toISOString(),\n paper_type: paperType,\n };\n\n console.log(`[SCORING] Granular score: overall=${overall}, consensus=${overall_consensus}, judges=${result.judges.join(\",\")}`);\n return result;\n}\n"}, "330ed8e7712fe8914d332676dc37a7f77605ac5b": {"t": 1785753957.337084, "n": 16148, "h": "720f0cb0b1032e61", "c": "/**\n * P2PCLAW Granular Scoring Service\n * =================================\n * Heterogeneous multi-LLM scoring engine that evaluates papers section-by-section.\n *\n * Provider chain (updated 2026-04-01):\n * 1. Cerebras — qwen-3-235b (3 keys, free, ultra-fast)\n * 2. Cerebras — llama3.1-8b (3 keys, free, ultra-fast)\n * 3. Mistral — mistral-small-latest (2 keys, free)\n * 4. Sarvam — sarvam-m (Indian AI, free)\n * 5. OpenRouter — qwen3-coder:free (free)\n * 6. Groq — llama-3.3-70b-versatile (7 keys, may be restricted)\n * 7. NVIDIA — meta/llama-3.3-70b-instruct (3 keys, free)\n * 8. Inception — mercury-2 (5 keys, free)\n * 9. Xiaomi MiMo — mimo-v2-flash (3 keys, free)\n * 10. Xiaomi MiMo — mimo-v2-pro (3 keys, free, reasoning)\n * 11. Deterministic heuristic fallback (never blocks)\n *\n * ALL available judges score independently. Final score = average across all judges.\n * Each model evaluates each section independently for maximum consensus diversity.\n */\n\nconst SECTIONS = [\"abstract\", \"introduction\", \"methodology\", \"results\", \"discussion\", \"conclusion\", \"references\"];\n\n// ── Load keys with rotation ─────────────────────────────────────────────────\n\nfunction loadKeys(envPrefix, maxKeys = 10) {\n const keys = [];\n for (let i = 1; i <= maxKeys; i++) {\n const k = process.env[`${envPrefix}_${i}`] || process.env[`${envPrefix}${i}`];\n if (k) keys.push(k);\n }\n // Also check single-key env vars\n const single = process.env[envPrefix];\n if (single && !keys.includes(single)) keys.unshift(single);\n return keys;\n}\n\nconst keyIndices = {};\nfunction nextKey(providerId, keys) {\n if (!keys.length) return null;\n const idx = keyIndices[providerId] || 0;\n const key = keys[idx % keys.length];\n keyIndices[providerId] = (idx + 1) % keys.length;\n return key;\n}\n\n// ── Provider definitions ────────────────────────────────────────────────────\n\nconst PROVIDERS = [\n // --- Cerebras: 3 keys x 2 models = up to 6 independent judges ---\n {\n id: \"cerebras-qwen\",\n name: \"Cerebras-Qwen235B\",\n url: \"https://api.cerebras.ai/v1/chat/completions\",\n model: \"qwen-3-235b-a22b-instruct-2507\",\n keys: loadKeys(\"CEREBRAS_API_KEY\").concat(loadKeys(\"CEREBRAS_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 1024,\n },\n {\n id: \"cerebras-llama\",\n name: \"Cerebras-Llama8B\",\n url: \"https://api.cerebras.ai/v1/chat/completions\",\n model: \"llama3.1-8b\",\n keys: loadKeys(\"CEREBRAS_API_KEY\").concat(loadKeys(\"CEREBRAS_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n // --- Mistral: 2 keys, reliable ---\n {\n id: \"mistral\",\n name: \"Mistral\",\n url: \"https://api.mistral.ai/v1/chat/completions\",\n model: \"mistral-small-latest\",\n keys: loadKeys(\"MISTRAL_API_KEY\").concat(loadKeys(\"MISTRAL_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n // --- Sarvam (Indian AI): sarvam-m, uses <think> tags (needs 2048+ tokens) ---\n {\n id: \"sarvam\",\n name: \"Sarvam\",\n url: \"https://api.sarvam.ai/v1/chat/completions\",\n model: \"sarvam-m\",\n keys: loadKeys(\"SARVAM_KEY\").concat(loadKeys(\"SARVAM_API_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 2048,\n },\n // --- OpenRouter: free models ---\n {\n id: \"openrouter\",\n name: \"OpenRouter\",\n url: \"https://openrouter.ai/api/v1/chat/completions\",\n model: \"qwen/qwen3-coder:free\",\n keys: loadKeys(\"OPENROUTER_API_KEY\"),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n extraHeaders: { \"HTTP-Referer\": \"https://www.p2pclaw.com\", \"X-Title\": \"P2PCLAW Scoring\" },\n },\n // --- Groq: 7 keys but may be org-restricted ---\n {\n id: \"groq\",\n name: \"Groq\",\n url: \"https://api.groq.com/openai/v1/chat/completions\",\n model: \"llama-3.3-70b-versatile\",\n keys: loadKeys(\"GROQ_API_KEY\").concat(loadKeys(\"GROQ_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n // --- NVIDIA: 3 keys ---\n {\n id: \"nvidia\",\n name: \"NVIDIA\",\n url: \"https://integrate.api.nvidia.com/v1/chat/completions\",\n model: \"meta/llama-3.3-70b-instruct\",\n keys: loadKeys(\"NVAPI_KEY\").concat(loadKeys(\"NVIDIA_API_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n // --- Inception: mercury-2 ---\n {\n id: \"inception\",\n name: \"Inception\",\n url: \"https://api.inceptionlabs.ai/v1/chat/completions\",\n model: \"mercury-2\",\n keys: loadKeys(\"INCEPTION_API_KEY\").concat(loadKeys(\"INCEPTION_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n // --- Xiaomi MiMo: 3 keys x 2 models = up to 2 independent judges ---\n {\n id: \"xiaomi-flash\",\n name: \"Xiaomi-MiMo-Flash\",\n url: \"https://api.xiaomimimo.com/v1/chat/completions\",\n model: \"mimo-v2-flash\",\n keys: loadKeys(\"XIAOMI_API_KEY\").concat(loadKeys(\"XIAOMI_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n {\n id: \"xiaomi-pro\",\n name: \"Xiaomi-MiMo-Pro\",\n url: \"https://api.xiaomimimo.com/v1/chat/completions\",\n model: \"mimo-v2-pro\",\n keys: loadKeys(\"XIAOMI_API_KEY\").concat(loadKeys(\"XIAOMI_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n maxTokens: 1024,\n },\n];\n\n// Deduplicate keys within each provider\nfor (const p of PROVIDERS) {\n p.keys = [...new Set(p.keys)].filter(Boolean);\n}\n\n// Log available providers\nconst available = PROVIDERS.filter(p => p.keys.length > 0);\nconsole.log(`[SCORING] ${available.length} LLM providers available: ${available.map(p => `${p.name}(${p.keys.length})`).join(\", \")}`);\nif (available.length === 0) console.warn(\"[SCORING] No LLM providers — heuristic scoring only.\");\n\nconst SCORING_PROMPT = `You are an academic paper quality evaluator for the P2PCLAW research network.\n\nScore this paper on a 0-10 scale for each criterion. Return ONLY valid JSON, no explanations.\n\nScoring criteria:\n- abstract (0-10): Quality, clarity, and completeness of the abstract\n- introduction (0-10): Problem statement clarity, context, motivation\n- methodology (0-10): Rigor, reproducibility, appropriate methods\n- results (0-10): Quality of findings, data presentation, significance\n- discussion (0-10): Interpretation depth, limitations acknowledged, implications\n- conclusion (0-10): Summary quality, future work suggestions\n- references (0-10): Citation quality, relevance, proper formatting (0 if no real references)\n- novelty (0-10): Original contribution to the field\n- reproducibility (0-10): Could another researcher reproduce this work?\n- citation_quality (0-10): Are references real, relevant, and properly cited?\n\nIMPORTANT RULES:\n- If a section is MISSING entirely, score it 0\n- If references are placeholder/fake (e.g. \"[1] Author, A. (2026). Placeholder\"), score references as 1\n- Trivial proofs or papers under 300 words should score below 3 overall\n- Papers that prove obvious things (like 0*0=0) should score novelty as 0-1\n- Be STRICT — a score of 8+ means genuinely publishable quality\n\nReturn ONLY this JSON format:\n{\"abstract\":N,\"introduction\":N,\"methodology\":N,\"results\":N,\"discussion\":N,\"conclusion\":N,\"references\":N,\"novelty\":N,\"reproducibility\":N,\"citation_quality\":N}\n\nPaper content:\n`;\n\n/**\n * Call a single LLM provider to score a paper.\n * Tries up to 2 keys from the provider before giving up.\n */\nasync function callLLMForScoring(prompt, provider) {\n if (!provider.keys.length) return null;\n\n const maxAttempts = Math.min(provider.keys.length, 2);\n for (let attempt = 0; attempt < maxAttempts; attempt++) {\n const key = nextKey(provider.id, provider.keys);\n if (!key) return null;\n\n const headers = { \"Content-Type\": \"application/json\" };\n headers[provider.authHeader] = `${provider.authPrefix}${key}`;\n if (provider.extraHeaders) Object.assign(headers, provider.extraHeaders);\n\n try {\n const res = await fetch(provider.url, {\n method: \"POST\",\n headers,\n body: JSON.stringify({\n model: provider.model,\n messages: [{ role: \"user\", content: prompt }],\n max_tokens: provider.maxTokens || 512,\n temperature: 0.1,\n }),\n signal: AbortSignal.timeout(30000),\n });\n\n if (res.status === 429) {\n console.warn(`[SCORING] ${provider.name} 429 rate-limited`);\n await new Promise(r => setTimeout(r, 3000));\n continue;\n }\n if (res.status === 402) {\n console.warn(`[SCORING] ${provider.name} 402 credits exhausted — skipping provider`);\n return null;\n }\n if (!res.ok) {\n const errBody = await res.text().catch(() => \"\");\n console.warn(`[SCORING] ${provider.name} HTTP ${res.status}: ${errBody.substring(0, 150)}`);\n continue;\n }\n\n const data = await res.json();\n let text = data.choices?.[0]?.message?.content || \"\";\n\n // Strip <think>...</think> tags (Sarvam, Qwen with thinking mode)\n if (provider.stripThinkTags || text.includes(\"<think>\")) {\n // Strip closed think blocks\n text = text.replace(/<think>[\\s\\S]*?<\\/think>/g, \"\").trim();\n // Strip unclosed think blocks (model ran out of tokens mid-think)\n if (text.startsWith(\"<think>\")) {\n text = text.replace(/<think>[\\s\\S]*/g, \"\").trim();\n }\n }\n\n const jsonMatch = text.match(/\\{[\\s\\S]*?\\}/);\n if (!jsonMatch) {\n console.warn(`[SCORING] ${provider.name} — no JSON in response`);\n continue;\n }\n\n const parsed = JSON.parse(jsonMatch[0]);\n const fields = [...SECTIONS, \"novelty\", \"reproducibility\", \"citation_quality\"];\n for (const field of fields) {\n if (typeof parsed[field] !== \"number\" || parsed[field] < 0 || parsed[field] > 10) {\n parsed[field] = typeof parsed[field] === \"number\" ? Math.max(0, Math.min(10, Math.round(parsed[field]))) : 5;\n }\n }\n\n console.log(`[SCORING] ${provider.name} scored successfully`);\n return { scores: parsed, provider: provider.name };\n } catch (e) {\n console.warn(`[SCORING] ${provider.name} error: ${e.message}`);\n }\n }\n return null;\n}\n\n/**\n * Deterministic heuristic scoring — used when all LLMs fail.\n */\nfunction heuristicScore(content) {\n const text = content || \"\";\n const words = text.split(/\\s+/).filter(w => w.length > 0);\n const wordCount = words.length;\n const lowerText = text.toLowerCase();\n\n const sectionScores = {};\n for (const section of SECTIONS) {\n const hasSection = lowerText.includes(`## ${section}`) || lowerText.includes(`# ${section}`);\n if (!hasSection) {\n sectionScores[section] = 0;\n continue;\n }\n const sectionRegex = new RegExp(`##?\\\\s*${section}[\\\\s\\\\S]*?(?=##?\\\\s|$)`, \"i\");\n const match = text.match(sectionRegex);\n const sectionWords = match ? match[0].split(/\\s+/).length : 0;\n if (sectionWords < 20) sectionScores[section] = 2;\n else if (sectionWords < 50) sectionScores[section] = 4;\n else if (sectionWords < 100) sectionScores[section] = 5;\n else if (sectionWords < 200) sectionScores[section] = 6;\n else sectionScores[section] = 7;\n }\n\n const refMatches = text.match(/\\[\\d+\\]/g) || [];\n const uniqueRefs = new Set(refMatches).size;\n const hasPlaceholderRefs = /placeholder|author,?\\s*a\\.\\s*\\(\\d{4}\\)/i.test(text);\n const hasRealAuthors = /[A-Z][a-z]+,\\s*[A-Z]\\.\\s*(?:&|,|et al)/g.test(text);\n const hasDOI = /doi\\.org|arxiv\\.org|10\\.\\d{4}/i.test(text);\n let refScore = hasPlaceholderRefs ? 1 : Math.min(7, uniqueRefs);\n if (hasRealAuthors) refScore = Math.min(10, refScore + 1);\n if (hasDOI) refScore = Math.min(10, refScore + 1);\n sectionScores.references = refScore;\n\n const technicalTerms = (text.match(/\\b(algorithm|theorem|proof|complexity|O\\([^)]+\\)|convergence|optimal|novel|framework)\\b/gi) || []).length;\n const hasFigures = /figure \\d|fig\\.\\s*\\d|table \\d/i.test(text);\n let novelty = wordCount > 2000 ? 4 : wordCount > 1000 ? 3 : 2;\n novelty += Math.min(3, Math.floor(technicalTerms / 5));\n if (hasFigures) novelty += 1;\n\n const hasCode = /```[\\s\\S]*?```/.test(text);\n const hasEquations = /\\$[^$]+\\$/.test(text) || /\\\\begin\\{/.test(text);\n const hasNumbers = /\\d+\\.\\d+%|\\d+\\.\\d+x|p\\s*[<>]\\s*0\\.\\d/i.test(text);\n let reproducibility = (hasCode ? 5 : 3) + (hasEquations ? 1 : 0) + (wordCount > 2000 ? 1 : 0);\n if (hasNumbers) reproducibility += 1;\n\n const citation_quality = hasPlaceholderRefs ? 1 : Math.min(6, uniqueRefs);\n\n return {\n scores: {\n ...sectionScores,\n novelty: Math.min(10, novelty),\n reproducibility: Math.min(10, reproducibility),\n citation_quality: Math.min(10, citation_quality),\n },\n provider: \"heuristic\"\n };\n}\n\n/**\n * Score a paper using heterogeneous multi-LLM swarm.\n * Tries to get 2 independent judges, falls back through provider chain.\n */\nexport async function scoreGranular(content, paperType = \"research\") {\n if (!content || content.trim().length < 50) {\n return {\n sections: Object.fromEntries(SECTIONS.map(s => [s, 0])),\n overall: 0,\n novelty: 0,\n reproducibility: 0,\n citation_quality: 0,\n judges: [],\n judge_count: 0,\n error: \"Content too short to score\"\n };\n }\n\n const truncated = content.length > 4000 ? content.substring(0, 4000) + \"\\n\\n[... truncated for scoring ...]\" : content;\n const prompt = SCORING_PROMPT + truncated;\n\n // ALL available judges score independently for maximum consensus diversity.\n // Each model evaluates each section independently, then we average.\n // Run all judges in parallel for speed (each has its own timeout).\n const judgePromises = available.map(provider =>\n callLLMForScoring(prompt, provider).catch(() => null)\n );\n const judgeResults = await Promise.all(judgePromises);\n const judges = judgeResults.filter(Boolean);\n\n // If no LLM judges succeeded, use heuristic\n if (judges.length === 0) {\n const heuristic = heuristicScore(content);\n judges.push(heuristic);\n }\n\n // Average scores across all judges\n const allFields = [...SECTIONS, \"novelty\", \"reproducibility\", \"citation_quality\"];\n const averaged = {};\n for (const field of allFields) {\n const values = judges.map(j => j.scores[field]).filter(v => typeof v === \"number\");\n averaged[field] = values.length > 0\n ? Math.round((values.reduce((a, b) => a + b, 0) / values.length) * 10) / 10\n : 0;\n }\n\n const sectionValues = SECTIONS.map(s => averaged[s]);\n const overall = Math.round((sectionValues.reduce((a, b) => a + b, 0) / SECTIONS.length) * 10) / 10;\n\n const result = {\n sections: Object.fromEntries(SECTIONS.map(s => [s, averaged[s]])),\n overall,\n novelty: averaged.novelty,\n reproducibility: averaged.reproducibility,\n citation_quality: averaged.citation_quality,\n judges: judges.map(j => j.provider),\n judge_count: judges.length,\n scored_at: new Date().toISOString(),\n paper_type: paperType,\n };\n\n console.log(`[SCORING] Granular score: overall=${overall}, judges=${result.judges.join(\",\")}`);\n return result;\n}\n"}, "1b5b5fcb6ae8f2dfb1f9457546637adf85a8698f": {"t": 1785753958.802135, "n": 15317, "h": "f8d31bbebefaec18", "c": "/**\n * P2PCLAW Granular Scoring Service\n * =================================\n * Heterogeneous multi-LLM scoring engine that evaluates papers section-by-section.\n *\n * Provider chain (updated 2026-04-01):\n * 1. Cerebras — qwen-3-235b (3 keys, free, ultra-fast)\n * 2. Cerebras — llama3.1-8b (3 keys, free, ultra-fast)\n * 3. Mistral — mistral-small-latest (2 keys, free)\n * 4. Sarvam — sarvam-m (Indian AI, free)\n * 5. OpenRouter — qwen3-coder:free (free)\n * 6. Groq — llama-3.3-70b-versatile (7 keys, may be restricted)\n * 7. NVIDIA — meta/llama-3.3-70b-instruct (3 keys, free)\n * 8. Inception — mercury-2 (5 keys, free)\n * 9. Deterministic heuristic fallback (never blocks)\n *\n * ALL available judges score independently. Final score = average across all judges.\n * Each model evaluates each section independently for maximum consensus diversity.\n */\n\nconst SECTIONS = [\"abstract\", \"introduction\", \"methodology\", \"results\", \"discussion\", \"conclusion\", \"references\"];\n\n// ── Load keys with rotation ─────────────────────────────────────────────────\n\nfunction loadKeys(envPrefix, maxKeys = 10) {\n const keys = [];\n for (let i = 1; i <= maxKeys; i++) {\n const k = process.env[`${envPrefix}_${i}`] || process.env[`${envPrefix}${i}`];\n if (k) keys.push(k);\n }\n // Also check single-key env vars\n const single = process.env[envPrefix];\n if (single && !keys.includes(single)) keys.unshift(single);\n return keys;\n}\n\nconst keyIndices = {};\nfunction nextKey(providerId, keys) {\n if (!keys.length) return null;\n const idx = keyIndices[providerId] || 0;\n const key = keys[idx % keys.length];\n keyIndices[providerId] = (idx + 1) % keys.length;\n return key;\n}\n\n// ── Provider definitions ────────────────────────────────────────────────────\n\nconst PROVIDERS = [\n // --- Cerebras: 3 keys x 2 models = up to 6 independent judges ---\n {\n id: \"cerebras-qwen\",\n name: \"Cerebras-Qwen235B\",\n url: \"https://api.cerebras.ai/v1/chat/completions\",\n model: \"qwen-3-235b-a22b-instruct-2507\",\n keys: loadKeys(\"CEREBRAS_API_KEY\").concat(loadKeys(\"CEREBRAS_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 1024,\n },\n {\n id: \"cerebras-llama\",\n name: \"Cerebras-Llama8B\",\n url: \"https://api.cerebras.ai/v1/chat/completions\",\n model: \"llama3.1-8b\",\n keys: loadKeys(\"CEREBRAS_API_KEY\").concat(loadKeys(\"CEREBRAS_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n // --- Mistral: 2 keys, reliable ---\n {\n id: \"mistral\",\n name: \"Mistral\",\n url: \"https://api.mistral.ai/v1/chat/completions\",\n model: \"mistral-small-latest\",\n keys: loadKeys(\"MISTRAL_API_KEY\").concat(loadKeys(\"MISTRAL_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n // --- Sarvam (Indian AI): sarvam-m, uses <think> tags (needs 2048+ tokens) ---\n {\n id: \"sarvam\",\n name: \"Sarvam\",\n url: \"https://api.sarvam.ai/v1/chat/completions\",\n model: \"sarvam-m\",\n keys: loadKeys(\"SARVAM_KEY\").concat(loadKeys(\"SARVAM_API_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 2048,\n },\n // --- OpenRouter: free models ---\n {\n id: \"openrouter\",\n name: \"OpenRouter\",\n url: \"https://openrouter.ai/api/v1/chat/completions\",\n model: \"qwen/qwen3-coder:free\",\n keys: loadKeys(\"OPENROUTER_API_KEY\"),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n extraHeaders: { \"HTTP-Referer\": \"https://www.p2pclaw.com\", \"X-Title\": \"P2PCLAW Scoring\" },\n },\n // --- Groq: 7 keys but may be org-restricted ---\n {\n id: \"groq\",\n name: \"Groq\",\n url: \"https://api.groq.com/openai/v1/chat/completions\",\n model: \"llama-3.3-70b-versatile\",\n keys: loadKeys(\"GROQ_API_KEY\").concat(loadKeys(\"GROQ_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n // --- NVIDIA: 3 keys ---\n {\n id: \"nvidia\",\n name: \"NVIDIA\",\n url: \"https://integrate.api.nvidia.com/v1/chat/completions\",\n model: \"meta/llama-3.3-70b-instruct\",\n keys: loadKeys(\"NVAPI_KEY\").concat(loadKeys(\"NVIDIA_API_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n // --- Inception: mercury-2 ---\n {\n id: \"inception\",\n name: \"Inception\",\n url: \"https://api.inceptionlabs.ai/v1/chat/completions\",\n model: \"mercury-2\",\n keys: loadKeys(\"INCEPTION_API_KEY\").concat(loadKeys(\"INCEPTION_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n];\n\n// Deduplicate keys within each provider\nfor (const p of PROVIDERS) {\n p.keys = [...new Set(p.keys)].filter(Boolean);\n}\n\n// Log available providers\nconst available = PROVIDERS.filter(p => p.keys.length > 0);\nconsole.log(`[SCORING] ${available.length} LLM providers available: ${available.map(p => `${p.name}(${p.keys.length})`).join(\", \")}`);\nif (available.length === 0) console.warn(\"[SCORING] No LLM providers — heuristic scoring only.\");\n\nconst SCORING_PROMPT = `You are an academic paper quality evaluator for the P2PCLAW research network.\n\nScore this paper on a 0-10 scale for each criterion. Return ONLY valid JSON, no explanations.\n\nScoring criteria:\n- abstract (0-10): Quality, clarity, and completeness of the abstract\n- introduction (0-10): Problem statement clarity, context, motivation\n- methodology (0-10): Rigor, reproducibility, appropriate methods\n- results (0-10): Quality of findings, data presentation, significance\n- discussion (0-10): Interpretation depth, limitations acknowledged, implications\n- conclusion (0-10): Summary quality, future work suggestions\n- references (0-10): Citation quality, relevance, proper formatting (0 if no real references)\n- novelty (0-10): Original contribution to the field\n- reproducibility (0-10): Could another researcher reproduce this work?\n- citation_quality (0-10): Are references real, relevant, and properly cited?\n\nIMPORTANT RULES:\n- If a section is MISSING entirely, score it 0\n- If references are placeholder/fake (e.g. \"[1] Author, A. (2026). Placeholder\"), score references as 1\n- Trivial proofs or papers under 300 words should score below 3 overall\n- Papers that prove obvious things (like 0*0=0) should score novelty as 0-1\n- Be STRICT — a score of 8+ means genuinely publishable quality\n\nReturn ONLY this JSON format:\n{\"abstract\":N,\"introduction\":N,\"methodology\":N,\"results\":N,\"discussion\":N,\"conclusion\":N,\"references\":N,\"novelty\":N,\"reproducibility\":N,\"citation_quality\":N}\n\nPaper content:\n`;\n\n/**\n * Call a single LLM provider to score a paper.\n * Tries up to 2 keys from the provider before giving up.\n */\nasync function callLLMForScoring(prompt, provider) {\n if (!provider.keys.length) return null;\n\n const maxAttempts = Math.min(provider.keys.length, 2);\n for (let attempt = 0; attempt < maxAttempts; attempt++) {\n const key = nextKey(provider.id, provider.keys);\n if (!key) return null;\n\n const headers = { \"Content-Type\": \"application/json\" };\n headers[provider.authHeader] = `${provider.authPrefix}${key}`;\n if (provider.extraHeaders) Object.assign(headers, provider.extraHeaders);\n\n try {\n const res = await fetch(provider.url, {\n method: \"POST\",\n headers,\n body: JSON.stringify({\n model: provider.model,\n messages: [{ role: \"user\", content: prompt }],\n max_tokens: provider.maxTokens || 512,\n temperature: 0.1,\n }),\n signal: AbortSignal.timeout(30000),\n });\n\n if (res.status === 429) {\n console.warn(`[SCORING] ${provider.name} 429 rate-limited`);\n await new Promise(r => setTimeout(r, 3000));\n continue;\n }\n if (res.status === 402) {\n console.warn(`[SCORING] ${provider.name} 402 credits exhausted — skipping provider`);\n return null;\n }\n if (!res.ok) {\n const errBody = await res.text().catch(() => \"\");\n console.warn(`[SCORING] ${provider.name} HTTP ${res.status}: ${errBody.substring(0, 150)}`);\n continue;\n }\n\n const data = await res.json();\n let text = data.choices?.[0]?.message?.content || \"\";\n\n // Strip <think>...</think> tags (Sarvam, Qwen with thinking mode)\n if (provider.stripThinkTags || text.includes(\"<think>\")) {\n // Strip closed think blocks\n text = text.replace(/<think>[\\s\\S]*?<\\/think>/g, \"\").trim();\n // Strip unclosed think blocks (model ran out of tokens mid-think)\n if (text.startsWith(\"<think>\")) {\n text = text.replace(/<think>[\\s\\S]*/g, \"\").trim();\n }\n }\n\n const jsonMatch = text.match(/\\{[\\s\\S]*?\\}/);\n if (!jsonMatch) {\n console.warn(`[SCORING] ${provider.name} — no JSON in response`);\n continue;\n }\n\n const parsed = JSON.parse(jsonMatch[0]);\n const fields = [...SECTIONS, \"novelty\", \"reproducibility\", \"citation_quality\"];\n for (const field of fields) {\n if (typeof parsed[field] !== \"number\" || parsed[field] < 0 || parsed[field] > 10) {\n parsed[field] = typeof parsed[field] === \"number\" ? Math.max(0, Math.min(10, Math.round(parsed[field]))) : 5;\n }\n }\n\n console.log(`[SCORING] ${provider.name} scored successfully`);\n return { scores: parsed, provider: provider.name };\n } catch (e) {\n console.warn(`[SCORING] ${provider.name} error: ${e.message}`);\n }\n }\n return null;\n}\n\n/**\n * Deterministic heuristic scoring — used when all LLMs fail.\n */\nfunction heuristicScore(content) {\n const text = content || \"\";\n const words = text.split(/\\s+/).filter(w => w.length > 0);\n const wordCount = words.length;\n const lowerText = text.toLowerCase();\n\n const sectionScores = {};\n for (const section of SECTIONS) {\n const hasSection = lowerText.includes(`## ${section}`) || lowerText.includes(`# ${section}`);\n if (!hasSection) {\n sectionScores[section] = 0;\n continue;\n }\n const sectionRegex = new RegExp(`##?\\\\s*${section}[\\\\s\\\\S]*?(?=##?\\\\s|$)`, \"i\");\n const match = text.match(sectionRegex);\n const sectionWords = match ? match[0].split(/\\s+/).length : 0;\n if (sectionWords < 20) sectionScores[section] = 2;\n else if (sectionWords < 50) sectionScores[section] = 4;\n else if (sectionWords < 100) sectionScores[section] = 5;\n else if (sectionWords < 200) sectionScores[section] = 6;\n else sectionScores[section] = 7;\n }\n\n const refMatches = text.match(/\\[\\d+\\]/g) || [];\n const uniqueRefs = new Set(refMatches).size;\n const hasPlaceholderRefs = /placeholder|author,?\\s*a\\.\\s*\\(\\d{4}\\)/i.test(text);\n const hasRealAuthors = /[A-Z][a-z]+,\\s*[A-Z]\\.\\s*(?:&|,|et al)/g.test(text);\n const hasDOI = /doi\\.org|arxiv\\.org|10\\.\\d{4}/i.test(text);\n let refScore = hasPlaceholderRefs ? 1 : Math.min(7, uniqueRefs);\n if (hasRealAuthors) refScore = Math.min(10, refScore + 1);\n if (hasDOI) refScore = Math.min(10, refScore + 1);\n sectionScores.references = refScore;\n\n const technicalTerms = (text.match(/\\b(algorithm|theorem|proof|complexity|O\\([^)]+\\)|convergence|optimal|novel|framework)\\b/gi) || []).length;\n const hasFigures = /figure \\d|fig\\.\\s*\\d|table \\d/i.test(text);\n let novelty = wordCount > 2000 ? 4 : wordCount > 1000 ? 3 : 2;\n novelty += Math.min(3, Math.floor(technicalTerms / 5));\n if (hasFigures) novelty += 1;\n\n const hasCode = /```[\\s\\S]*?```/.test(text);\n const hasEquations = /\\$[^$]+\\$/.test(text) || /\\\\begin\\{/.test(text);\n const hasNumbers = /\\d+\\.\\d+%|\\d+\\.\\d+x|p\\s*[<>]\\s*0\\.\\d/i.test(text);\n let reproducibility = (hasCode ? 5 : 3) + (hasEquations ? 1 : 0) + (wordCount > 2000 ? 1 : 0);\n if (hasNumbers) reproducibility += 1;\n\n const citation_quality = hasPlaceholderRefs ? 1 : Math.min(6, uniqueRefs);\n\n return {\n scores: {\n ...sectionScores,\n novelty: Math.min(10, novelty),\n reproducibility: Math.min(10, reproducibility),\n citation_quality: Math.min(10, citation_quality),\n },\n provider: \"heuristic\"\n };\n}\n\n/**\n * Score a paper using heterogeneous multi-LLM swarm.\n * Tries to get 2 independent judges, falls back through provider chain.\n */\nexport async function scoreGranular(content, paperType = \"research\") {\n if (!content || content.trim().length < 50) {\n return {\n sections: Object.fromEntries(SECTIONS.map(s => [s, 0])),\n overall: 0,\n novelty: 0,\n reproducibility: 0,\n citation_quality: 0,\n judges: [],\n judge_count: 0,\n error: \"Content too short to score\"\n };\n }\n\n const truncated = content.length > 4000 ? content.substring(0, 4000) + \"\\n\\n[... truncated for scoring ...]\" : content;\n const prompt = SCORING_PROMPT + truncated;\n\n // ALL available judges score independently for maximum consensus diversity.\n // Each model evaluates each section independently, then we average.\n // Run all judges in parallel for speed (each has its own timeout).\n const judgePromises = available.map(provider =>\n callLLMForScoring(prompt, provider).catch(() => null)\n );\n const judgeResults = await Promise.all(judgePromises);\n const judges = judgeResults.filter(Boolean);\n\n // If no LLM judges succeeded, use heuristic\n if (judges.length === 0) {\n const heuristic = heuristicScore(content);\n judges.push(heuristic);\n }\n\n // Average scores across all judges\n const allFields = [...SECTIONS, \"novelty\", \"reproducibility\", \"citation_quality\"];\n const averaged = {};\n for (const field of allFields) {\n const values = judges.map(j => j.scores[field]).filter(v => typeof v === \"number\");\n averaged[field] = values.length > 0\n ? Math.round((values.reduce((a, b) => a + b, 0) / values.length) * 10) / 10\n : 0;\n }\n\n const sectionValues = SECTIONS.map(s => averaged[s]);\n const overall = Math.round((sectionValues.reduce((a, b) => a + b, 0) / SECTIONS.length) * 10) / 10;\n\n const result = {\n sections: Object.fromEntries(SECTIONS.map(s => [s, averaged[s]])),\n overall,\n novelty: averaged.novelty,\n reproducibility: averaged.reproducibility,\n citation_quality: averaged.citation_quality,\n judges: judges.map(j => j.provider),\n judge_count: judges.length,\n scored_at: new Date().toISOString(),\n paper_type: paperType,\n };\n\n console.log(`[SCORING] Granular score: overall=${overall}, judges=${result.judges.join(\",\")}`);\n return result;\n}\n"}, "6ba886118b0a1c6fe9516418e31ac76e6250a64c": {"t": 1785753961.7895048, "n": 14984, "h": "036dbbe257816824", "c": "/**\n * P2PCLAW Granular Scoring Service\n * =================================\n * Heterogeneous multi-LLM scoring engine that evaluates papers section-by-section.\n *\n * Provider chain (updated 2026-04-01):\n * 1. Cerebras — qwen-3-235b (3 keys, free, ultra-fast)\n * 2. Cerebras — llama3.1-8b (3 keys, free, ultra-fast)\n * 3. Mistral — mistral-small-latest (2 keys, free)\n * 4. Sarvam — sarvam-m (Indian AI, free)\n * 5. OpenRouter — qwen3-coder:free (free)\n * 6. Groq — llama-3.3-70b-versatile (7 keys, may be restricted)\n * 7. NVIDIA — meta/llama-3.3-70b-instruct (3 keys, free)\n * 8. Inception — mercury-2 (5 keys, free)\n * 9. Deterministic heuristic fallback (never blocks)\n *\n * ALL available judges score independently. Final score = average across all judges.\n * Each model evaluates each section independently for maximum consensus diversity.\n */\n\nconst SECTIONS = [\"abstract\", \"introduction\", \"methodology\", \"results\", \"discussion\", \"conclusion\", \"references\"];\n\n// ── Load keys with rotation ─────────────────────────────────────────────────\n\nfunction loadKeys(envPrefix, maxKeys = 10) {\n const keys = [];\n for (let i = 1; i <= maxKeys; i++) {\n const k = process.env[`${envPrefix}_${i}`] || process.env[`${envPrefix}${i}`];\n if (k) keys.push(k);\n }\n // Also check single-key env vars\n const single = process.env[envPrefix];\n if (single && !keys.includes(single)) keys.unshift(single);\n return keys;\n}\n\nconst keyIndices = {};\nfunction nextKey(providerId, keys) {\n if (!keys.length) return null;\n const idx = keyIndices[providerId] || 0;\n const key = keys[idx % keys.length];\n keyIndices[providerId] = (idx + 1) % keys.length;\n return key;\n}\n\n// ── Provider definitions ────────────────────────────────────────────────────\n\nconst PROVIDERS = [\n // --- Cerebras: 3 keys x 2 models = up to 6 independent judges ---\n {\n id: \"cerebras-qwen\",\n name: \"Cerebras-Qwen235B\",\n url: \"https://api.cerebras.ai/v1/chat/completions\",\n model: \"qwen-3-235b-a22b-instruct-2507\",\n keys: loadKeys(\"CEREBRAS_API_KEY\").concat(loadKeys(\"CEREBRAS_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n {\n id: \"cerebras-llama\",\n name: \"Cerebras-Llama8B\",\n url: \"https://api.cerebras.ai/v1/chat/completions\",\n model: \"llama3.1-8b\",\n keys: loadKeys(\"CEREBRAS_API_KEY\").concat(loadKeys(\"CEREBRAS_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n // --- Mistral: 2 keys, reliable ---\n {\n id: \"mistral\",\n name: \"Mistral\",\n url: \"https://api.mistral.ai/v1/chat/completions\",\n model: \"mistral-small-latest\",\n keys: loadKeys(\"MISTRAL_API_KEY\").concat(loadKeys(\"MISTRAL_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n // --- Sarvam (Indian AI): sarvam-m, uses <think> tags in output ---\n {\n id: \"sarvam\",\n name: \"Sarvam\",\n url: \"https://api.sarvam.ai/v1/chat/completions\",\n model: \"sarvam-m\",\n keys: loadKeys(\"SARVAM_KEY\").concat(loadKeys(\"SARVAM_API_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n stripThinkTags: true,\n maxTokens: 1024,\n },\n // --- OpenRouter: free models ---\n {\n id: \"openrouter\",\n name: \"OpenRouter\",\n url: \"https://openrouter.ai/api/v1/chat/completions\",\n model: \"qwen/qwen3-coder:free\",\n keys: loadKeys(\"OPENROUTER_API_KEY\"),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n extraHeaders: { \"HTTP-Referer\": \"https://www.p2pclaw.com\", \"X-Title\": \"P2PCLAW Scoring\" },\n },\n // --- Groq: 7 keys but may be org-restricted ---\n {\n id: \"groq\",\n name: \"Groq\",\n url: \"https://api.groq.com/openai/v1/chat/completions\",\n model: \"llama-3.3-70b-versatile\",\n keys: loadKeys(\"GROQ_API_KEY\").concat(loadKeys(\"GROQ_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n // --- NVIDIA: 3 keys ---\n {\n id: \"nvidia\",\n name: \"NVIDIA\",\n url: \"https://integrate.api.nvidia.com/v1/chat/completions\",\n model: \"meta/llama-3.3-70b-instruct\",\n keys: loadKeys(\"NVAPI_KEY\").concat(loadKeys(\"NVIDIA_API_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n // --- Inception: mercury-2 ---\n {\n id: \"inception\",\n name: \"Inception\",\n url: \"https://api.inceptionlabs.ai/v1/chat/completions\",\n model: \"mercury-2\",\n keys: loadKeys(\"INCEPTION_API_KEY\").concat(loadKeys(\"INCEPTION_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n];\n\n// Deduplicate keys within each provider\nfor (const p of PROVIDERS) {\n p.keys = [...new Set(p.keys)].filter(Boolean);\n}\n\n// Log available providers\nconst available = PROVIDERS.filter(p => p.keys.length > 0);\nconsole.log(`[SCORING] ${available.length} LLM providers available: ${available.map(p => `${p.name}(${p.keys.length})`).join(\", \")}`);\nif (available.length === 0) console.warn(\"[SCORING] No LLM providers — heuristic scoring only.\");\n\nconst SCORING_PROMPT = `You are an academic paper quality evaluator for the P2PCLAW research network.\n\nScore this paper on a 0-10 scale for each criterion. Return ONLY valid JSON, no explanations.\n\nScoring criteria:\n- abstract (0-10): Quality, clarity, and completeness of the abstract\n- introduction (0-10): Problem statement clarity, context, motivation\n- methodology (0-10): Rigor, reproducibility, appropriate methods\n- results (0-10): Quality of findings, data presentation, significance\n- discussion (0-10): Interpretation depth, limitations acknowledged, implications\n- conclusion (0-10): Summary quality, future work suggestions\n- references (0-10): Citation quality, relevance, proper formatting (0 if no real references)\n- novelty (0-10): Original contribution to the field\n- reproducibility (0-10): Could another researcher reproduce this work?\n- citation_quality (0-10): Are references real, relevant, and properly cited?\n\nIMPORTANT RULES:\n- If a section is MISSING entirely, score it 0\n- If references are placeholder/fake (e.g. \"[1] Author, A. (2026). Placeholder\"), score references as 1\n- Trivial proofs or papers under 300 words should score below 3 overall\n- Papers that prove obvious things (like 0*0=0) should score novelty as 0-1\n- Be STRICT — a score of 8+ means genuinely publishable quality\n\nReturn ONLY this JSON format:\n{\"abstract\":N,\"introduction\":N,\"methodology\":N,\"results\":N,\"discussion\":N,\"conclusion\":N,\"references\":N,\"novelty\":N,\"reproducibility\":N,\"citation_quality\":N}\n\nPaper content:\n`;\n\n/**\n * Call a single LLM provider to score a paper.\n * Tries up to 2 keys from the provider before giving up.\n */\nasync function callLLMForScoring(prompt, provider) {\n if (!provider.keys.length) return null;\n\n const maxAttempts = Math.min(provider.keys.length, 2);\n for (let attempt = 0; attempt < maxAttempts; attempt++) {\n const key = nextKey(provider.id, provider.keys);\n if (!key) return null;\n\n const headers = { \"Content-Type\": \"application/json\" };\n headers[provider.authHeader] = `${provider.authPrefix}${key}`;\n if (provider.extraHeaders) Object.assign(headers, provider.extraHeaders);\n\n try {\n const res = await fetch(provider.url, {\n method: \"POST\",\n headers,\n body: JSON.stringify({\n model: provider.model,\n messages: [{ role: \"user\", content: prompt }],\n max_tokens: provider.maxTokens || 512,\n temperature: 0.1,\n }),\n signal: AbortSignal.timeout(30000),\n });\n\n if (res.status === 429) {\n console.warn(`[SCORING] ${provider.name} 429 rate-limited`);\n await new Promise(r => setTimeout(r, 3000));\n continue;\n }\n if (res.status === 402) {\n console.warn(`[SCORING] ${provider.name} 402 credits exhausted — skipping provider`);\n return null;\n }\n if (!res.ok) {\n const errBody = await res.text().catch(() => \"\");\n console.warn(`[SCORING] ${provider.name} HTTP ${res.status}: ${errBody.substring(0, 150)}`);\n continue;\n }\n\n const data = await res.json();\n let text = data.choices?.[0]?.message?.content || \"\";\n\n // Strip <think>...</think> tags (Sarvam, Qwen with thinking mode)\n if (provider.stripThinkTags || text.includes(\"<think>\")) {\n text = text.replace(/<think>[\\s\\S]*?<\\/think>/g, \"\").trim();\n }\n\n const jsonMatch = text.match(/\\{[\\s\\S]*?\\}/);\n if (!jsonMatch) {\n console.warn(`[SCORING] ${provider.name} — no JSON in response`);\n continue;\n }\n\n const parsed = JSON.parse(jsonMatch[0]);\n const fields = [...SECTIONS, \"novelty\", \"reproducibility\", \"citation_quality\"];\n for (const field of fields) {\n if (typeof parsed[field] !== \"number\" || parsed[field] < 0 || parsed[field] > 10) {\n parsed[field] = typeof parsed[field] === \"number\" ? Math.max(0, Math.min(10, Math.round(parsed[field]))) : 5;\n }\n }\n\n console.log(`[SCORING] ${provider.name} scored successfully`);\n return { scores: parsed, provider: provider.name };\n } catch (e) {\n console.warn(`[SCORING] ${provider.name} error: ${e.message}`);\n }\n }\n return null;\n}\n\n/**\n * Deterministic heuristic scoring — used when all LLMs fail.\n */\nfunction heuristicScore(content) {\n const text = content || \"\";\n const words = text.split(/\\s+/).filter(w => w.length > 0);\n const wordCount = words.length;\n const lowerText = text.toLowerCase();\n\n const sectionScores = {};\n for (const section of SECTIONS) {\n const hasSection = lowerText.includes(`## ${section}`) || lowerText.includes(`# ${section}`);\n if (!hasSection) {\n sectionScores[section] = 0;\n continue;\n }\n const sectionRegex = new RegExp(`##?\\\\s*${section}[\\\\s\\\\S]*?(?=##?\\\\s|$)`, \"i\");\n const match = text.match(sectionRegex);\n const sectionWords = match ? match[0].split(/\\s+/).length : 0;\n if (sectionWords < 20) sectionScores[section] = 2;\n else if (sectionWords < 50) sectionScores[section] = 4;\n else if (sectionWords < 100) sectionScores[section] = 5;\n else if (sectionWords < 200) sectionScores[section] = 6;\n else sectionScores[section] = 7;\n }\n\n const refMatches = text.match(/\\[\\d+\\]/g) || [];\n const uniqueRefs = new Set(refMatches).size;\n const hasPlaceholderRefs = /placeholder|author,?\\s*a\\.\\s*\\(\\d{4}\\)/i.test(text);\n const hasRealAuthors = /[A-Z][a-z]+,\\s*[A-Z]\\.\\s*(?:&|,|et al)/g.test(text);\n const hasDOI = /doi\\.org|arxiv\\.org|10\\.\\d{4}/i.test(text);\n let refScore = hasPlaceholderRefs ? 1 : Math.min(7, uniqueRefs);\n if (hasRealAuthors) refScore = Math.min(10, refScore + 1);\n if (hasDOI) refScore = Math.min(10, refScore + 1);\n sectionScores.references = refScore;\n\n const technicalTerms = (text.match(/\\b(algorithm|theorem|proof|complexity|O\\([^)]+\\)|convergence|optimal|novel|framework)\\b/gi) || []).length;\n const hasFigures = /figure \\d|fig\\.\\s*\\d|table \\d/i.test(text);\n let novelty = wordCount > 2000 ? 4 : wordCount > 1000 ? 3 : 2;\n novelty += Math.min(3, Math.floor(technicalTerms / 5));\n if (hasFigures) novelty += 1;\n\n const hasCode = /```[\\s\\S]*?```/.test(text);\n const hasEquations = /\\$[^$]+\\$/.test(text) || /\\\\begin\\{/.test(text);\n const hasNumbers = /\\d+\\.\\d+%|\\d+\\.\\d+x|p\\s*[<>]\\s*0\\.\\d/i.test(text);\n let reproducibility = (hasCode ? 5 : 3) + (hasEquations ? 1 : 0) + (wordCount > 2000 ? 1 : 0);\n if (hasNumbers) reproducibility += 1;\n\n const citation_quality = hasPlaceholderRefs ? 1 : Math.min(6, uniqueRefs);\n\n return {\n scores: {\n ...sectionScores,\n novelty: Math.min(10, novelty),\n reproducibility: Math.min(10, reproducibility),\n citation_quality: Math.min(10, citation_quality),\n },\n provider: \"heuristic\"\n };\n}\n\n/**\n * Score a paper using heterogeneous multi-LLM swarm.\n * Tries to get 2 independent judges, falls back through provider chain.\n */\nexport async function scoreGranular(content, paperType = \"research\") {\n if (!content || content.trim().length < 50) {\n return {\n sections: Object.fromEntries(SECTIONS.map(s => [s, 0])),\n overall: 0,\n novelty: 0,\n reproducibility: 0,\n citation_quality: 0,\n judges: [],\n judge_count: 0,\n error: \"Content too short to score\"\n };\n }\n\n const truncated = content.length > 4000 ? content.substring(0, 4000) + \"\\n\\n[... truncated for scoring ...]\" : content;\n const prompt = SCORING_PROMPT + truncated;\n\n // ALL available judges score independently for maximum consensus diversity.\n // Each model evaluates each section independently, then we average.\n // Run all judges in parallel for speed (each has its own timeout).\n const judgePromises = available.map(provider =>\n callLLMForScoring(prompt, provider).catch(() => null)\n );\n const judgeResults = await Promise.all(judgePromises);\n const judges = judgeResults.filter(Boolean);\n\n // If no LLM judges succeeded, use heuristic\n if (judges.length === 0) {\n const heuristic = heuristicScore(content);\n judges.push(heuristic);\n }\n\n // Average scores across all judges\n const allFields = [...SECTIONS, \"novelty\", \"reproducibility\", \"citation_quality\"];\n const averaged = {};\n for (const field of allFields) {\n const values = judges.map(j => j.scores[field]).filter(v => typeof v === \"number\");\n averaged[field] = values.length > 0\n ? Math.round((values.reduce((a, b) => a + b, 0) / values.length) * 10) / 10\n : 0;\n }\n\n const sectionValues = SECTIONS.map(s => averaged[s]);\n const overall = Math.round((sectionValues.reduce((a, b) => a + b, 0) / SECTIONS.length) * 10) / 10;\n\n const result = {\n sections: Object.fromEntries(SECTIONS.map(s => [s, averaged[s]])),\n overall,\n novelty: averaged.novelty,\n reproducibility: averaged.reproducibility,\n citation_quality: averaged.citation_quality,\n judges: judges.map(j => j.provider),\n judge_count: judges.length,\n scored_at: new Date().toISOString(),\n paper_type: paperType,\n };\n\n console.log(`[SCORING] Granular score: overall=${overall}, judges=${result.judges.join(\",\")}`);\n return result;\n}\n"}, "31c6ef7fc1d209d66063d7a84fd8c6e26bb204eb": {"t": 1785753963.537952, "n": 13989, "h": "171bf294b3530f57", "c": "/**\n * P2PCLAW Granular Scoring Service\n * =================================\n * Heterogeneous multi-LLM scoring engine that evaluates papers section-by-section.\n *\n * Provider chain (tested 2026-03-31):\n * 1. Groq — llama-3.3-70b-versatile (4 keys, free)\n * 2. NVIDIA — meta/llama-3.3-70b-instruct (3 keys, free)\n * 3. Cerebras — qwen-3-235b-a22b-instruct-2507 (2 keys, free)\n * 4. Sarvam — sarvam-m (9 keys, free)\n * 5. Mistral — mistral-small-latest (1 key, free)\n * 6. Inception — mercury-2 (6 keys, free)\n * 7. OpenRouter — gemini-2.5-flash (7 keys, paid — last resort)\n * 8. Deterministic heuristic fallback (never blocks)\n *\n * Uses 2 independent LLM judges when possible, falls back through the chain.\n */\n\nconst SECTIONS = [\"abstract\", \"introduction\", \"methodology\", \"results\", \"discussion\", \"conclusion\", \"references\"];\n\n// ── Load keys with rotation ─────────────────────────────────────────────────\n\nfunction loadKeys(envPrefix, maxKeys = 10) {\n const keys = [];\n for (let i = 1; i <= maxKeys; i++) {\n const k = process.env[`${envPrefix}_${i}`] || process.env[`${envPrefix}${i}`];\n if (k) keys.push(k);\n }\n // Also check single-key env vars\n const single = process.env[envPrefix];\n if (single && !keys.includes(single)) keys.unshift(single);\n return keys;\n}\n\nconst keyIndices = {};\nfunction nextKey(providerId, keys) {\n if (!keys.length) return null;\n const idx = keyIndices[providerId] || 0;\n const key = keys[idx % keys.length];\n keyIndices[providerId] = (idx + 1) % keys.length;\n return key;\n}\n\n// ── Provider definitions ────────────────────────────────────────────────────\n\nconst PROVIDERS = [\n {\n id: \"llmapi\",\n name: \"LLM-API\",\n url: \"https://api.llmapi.ai/v1/chat/completions\",\n model: \"gpt-4o\",\n keys: loadKeys(\"LLMAPI_KEY\"),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n minTokens: 16, // LLM API requires min 16 tokens\n },\n {\n id: \"groq\",\n name: \"Groq\",\n url: \"https://api.groq.com/openai/v1/chat/completions\",\n model: \"llama-3.3-70b-versatile\",\n keys: loadKeys(\"GROQ_API_KEY\").concat(loadKeys(\"GROQ_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n {\n id: \"nvidia\",\n name: \"NVIDIA\",\n url: \"https://integrate.api.nvidia.com/v1/chat/completions\",\n model: \"meta/llama-3.3-70b-instruct\",\n keys: loadKeys(\"NVAPI_KEY\").concat(loadKeys(\"NVIDIA_API_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n {\n id: \"cerebras\",\n name: \"Cerebras\",\n url: \"https://api.cerebras.ai/v1/chat/completions\",\n model: \"qwen-3-235b-a22b-instruct-2507\",\n keys: loadKeys(\"CEREBRAS_API_KEY\").concat(loadKeys(\"CEREBRAS_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n {\n id: \"sarvam\",\n name: \"Sarvam\",\n url: \"https://api.sarvam.ai/v1/chat/completions\",\n model: \"sarvam-m\",\n keys: loadKeys(\"SARVAM_KEY\").concat(loadKeys(\"SARVAM_API_KEY\")),\n authHeader: \"api-subscription-key\",\n authPrefix: \"\",\n },\n {\n id: \"mistral\",\n name: \"Mistral\",\n url: \"https://api.mistral.ai/v1/chat/completions\",\n model: \"mistral-small-latest\",\n keys: loadKeys(\"MISTRAL_API_KEY\").concat(loadKeys(\"MISTRAL_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n {\n id: \"inception\",\n name: \"Inception\",\n url: \"https://api.inceptionlabs.ai/v1/chat/completions\",\n model: \"mercury-2\",\n keys: loadKeys(\"INCEPTION_API_KEY\").concat(loadKeys(\"INCEPTION_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n {\n id: \"openrouter\",\n name: \"OpenRouter\",\n url: \"https://openrouter.ai/api/v1/chat/completions\",\n model: \"google/gemini-2.5-flash\",\n keys: loadKeys(\"OPENROUTER_API_KEY\"),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n];\n\n// Deduplicate keys within each provider\nfor (const p of PROVIDERS) {\n p.keys = [...new Set(p.keys)].filter(Boolean);\n}\n\n// Log available providers\nconst available = PROVIDERS.filter(p => p.keys.length > 0);\nconsole.log(`[SCORING] ${available.length} LLM providers available: ${available.map(p => `${p.name}(${p.keys.length})`).join(\", \")}`);\nif (available.length === 0) console.warn(\"[SCORING] No LLM providers — heuristic scoring only.\");\n\nconst SCORING_PROMPT = `You are an academic paper quality evaluator for the P2PCLAW research network.\n\nScore this paper on a 0-10 scale for each criterion. Return ONLY valid JSON, no explanations.\n\nScoring criteria:\n- abstract (0-10): Quality, clarity, and completeness of the abstract\n- introduction (0-10): Problem statement clarity, context, motivation\n- methodology (0-10): Rigor, reproducibility, appropriate methods\n- results (0-10): Quality of findings, data presentation, significance\n- discussion (0-10): Interpretation depth, limitations acknowledged, implications\n- conclusion (0-10): Summary quality, future work suggestions\n- references (0-10): Citation quality, relevance, proper formatting (0 if no real references)\n- novelty (0-10): Original contribution to the field\n- reproducibility (0-10): Could another researcher reproduce this work?\n- citation_quality (0-10): Are references real, relevant, and properly cited?\n\nIMPORTANT RULES:\n- If a section is MISSING entirely, score it 0\n- If references are placeholder/fake (e.g. \"[1] Author, A. (2026). Placeholder\"), score references as 1\n- Trivial proofs or papers under 300 words should score below 3 overall\n- Papers that prove obvious things (like 0*0=0) should score novelty as 0-1\n- Be STRICT — a score of 8+ means genuinely publishable quality\n\nReturn ONLY this JSON format:\n{\"abstract\":N,\"introduction\":N,\"methodology\":N,\"results\":N,\"discussion\":N,\"conclusion\":N,\"references\":N,\"novelty\":N,\"reproducibility\":N,\"citation_quality\":N}\n\nPaper content:\n`;\n\n/**\n * Call a single LLM provider to score a paper.\n * Tries up to 2 keys from the provider before giving up.\n */\nasync function callLLMForScoring(prompt, provider) {\n if (!provider.keys.length) return null;\n\n const maxAttempts = Math.min(provider.keys.length, 2);\n for (let attempt = 0; attempt < maxAttempts; attempt++) {\n const key = nextKey(provider.id, provider.keys);\n if (!key) return null;\n\n const headers = { \"Content-Type\": \"application/json\" };\n if (provider.authHeader === \"api-subscription-key\") {\n headers[\"api-subscription-key\"] = key;\n } else {\n headers[provider.authHeader] = `${provider.authPrefix}${key}`;\n }\n\n try {\n const res = await fetch(provider.url, {\n method: \"POST\",\n headers,\n body: JSON.stringify({\n model: provider.model,\n messages: [{ role: \"user\", content: prompt }],\n max_tokens: Math.max(provider.minTokens || 16, 512),\n temperature: 0.1,\n }),\n signal: AbortSignal.timeout(30000),\n });\n\n if (res.status === 429) {\n console.warn(`[SCORING] ${provider.name} 429 rate-limited`);\n await new Promise(r => setTimeout(r, 3000));\n continue;\n }\n if (res.status === 402) {\n console.warn(`[SCORING] ${provider.name} 402 credits exhausted — skipping provider`);\n return null;\n }\n if (!res.ok) {\n const errBody = await res.text().catch(() => \"\");\n console.warn(`[SCORING] ${provider.name} HTTP ${res.status}: ${errBody.substring(0, 150)}`);\n continue;\n }\n\n const data = await res.json();\n const text = data.choices?.[0]?.message?.content || \"\";\n\n const jsonMatch = text.match(/\\{[\\s\\S]*?\\}/);\n if (!jsonMatch) {\n console.warn(`[SCORING] ${provider.name} — no JSON in response`);\n continue;\n }\n\n const parsed = JSON.parse(jsonMatch[0]);\n const fields = [...SECTIONS, \"novelty\", \"reproducibility\", \"citation_quality\"];\n for (const field of fields) {\n if (typeof parsed[field] !== \"number\" || parsed[field] < 0 || parsed[field] > 10) {\n parsed[field] = typeof parsed[field] === \"number\" ? Math.max(0, Math.min(10, Math.round(parsed[field]))) : 5;\n }\n }\n\n console.log(`[SCORING] ${provider.name} scored successfully`);\n return { scores: parsed, provider: provider.name };\n } catch (e) {\n console.warn(`[SCORING] ${provider.name} error: ${e.message}`);\n }\n }\n return null;\n}\n\n/**\n * Deterministic heuristic scoring — used when all LLMs fail.\n */\nfunction heuristicScore(content) {\n const text = content || \"\";\n const words = text.split(/\\s+/).filter(w => w.length > 0);\n const wordCount = words.length;\n const lowerText = text.toLowerCase();\n\n const sectionScores = {};\n for (const section of SECTIONS) {\n const hasSection = lowerText.includes(`## ${section}`) || lowerText.includes(`# ${section}`);\n if (!hasSection) {\n sectionScores[section] = 0;\n continue;\n }\n const sectionRegex = new RegExp(`##?\\\\s*${section}[\\\\s\\\\S]*?(?=##?\\\\s|$)`, \"i\");\n const match = text.match(sectionRegex);\n const sectionWords = match ? match[0].split(/\\s+/).length : 0;\n if (sectionWords < 20) sectionScores[section] = 2;\n else if (sectionWords < 50) sectionScores[section] = 4;\n else if (sectionWords < 100) sectionScores[section] = 5;\n else if (sectionWords < 200) sectionScores[section] = 6;\n else sectionScores[section] = 7;\n }\n\n const refMatches = text.match(/\\[\\d+\\]/g) || [];\n const uniqueRefs = new Set(refMatches).size;\n const hasPlaceholderRefs = /placeholder|author,?\\s*a\\.\\s*\\(\\d{4}\\)/i.test(text);\n const hasRealAuthors = /[A-Z][a-z]+,\\s*[A-Z]\\.\\s*(?:&|,|et al)/g.test(text);\n const hasDOI = /doi\\.org|arxiv\\.org|10\\.\\d{4}/i.test(text);\n let refScore = hasPlaceholderRefs ? 1 : Math.min(7, uniqueRefs);\n if (hasRealAuthors) refScore = Math.min(10, refScore + 1);\n if (hasDOI) refScore = Math.min(10, refScore + 1);\n sectionScores.references = refScore;\n\n const technicalTerms = (text.match(/\\b(algorithm|theorem|proof|complexity|O\\([^)]+\\)|convergence|optimal|novel|framework)\\b/gi) || []).length;\n const hasFigures = /figure \\d|fig\\.\\s*\\d|table \\d/i.test(text);\n let novelty = wordCount > 2000 ? 4 : wordCount > 1000 ? 3 : 2;\n novelty += Math.min(3, Math.floor(technicalTerms / 5));\n if (hasFigures) novelty += 1;\n\n const hasCode = /```[\\s\\S]*?```/.test(text);\n const hasEquations = /\\$[^$]+\\$/.test(text) || /\\\\begin\\{/.test(text);\n const hasNumbers = /\\d+\\.\\d+%|\\d+\\.\\d+x|p\\s*[<>]\\s*0\\.\\d/i.test(text);\n let reproducibility = (hasCode ? 5 : 3) + (hasEquations ? 1 : 0) + (wordCount > 2000 ? 1 : 0);\n if (hasNumbers) reproducibility += 1;\n\n const citation_quality = hasPlaceholderRefs ? 1 : Math.min(6, uniqueRefs);\n\n return {\n scores: {\n ...sectionScores,\n novelty: Math.min(10, novelty),\n reproducibility: Math.min(10, reproducibility),\n citation_quality: Math.min(10, citation_quality),\n },\n provider: \"heuristic\"\n };\n}\n\n/**\n * Score a paper using heterogeneous multi-LLM swarm.\n * Tries to get 2 independent judges, falls back through provider chain.\n */\nexport async function scoreGranular(content, paperType = \"research\") {\n if (!content || content.trim().length < 50) {\n return {\n sections: Object.fromEntries(SECTIONS.map(s => [s, 0])),\n overall: 0,\n novelty: 0,\n reproducibility: 0,\n citation_quality: 0,\n judges: [],\n judge_count: 0,\n error: \"Content too short to score\"\n };\n }\n\n const truncated = content.length > 4000 ? content.substring(0, 4000) + \"\\n\\n[... truncated for scoring ...]\" : content;\n const prompt = SCORING_PROMPT + truncated;\n\n // Try to get 2 independent LLM judges from the provider chain\n const judges = [];\n for (const provider of available) {\n if (judges.length >= 2) break;\n const result = await callLLMForScoring(prompt, provider);\n if (result) judges.push(result);\n }\n\n // If no LLM judges succeeded, use heuristic\n if (judges.length === 0) {\n const heuristic = heuristicScore(content);\n judges.push(heuristic);\n }\n\n // Average scores across all judges\n const allFields = [...SECTIONS, \"novelty\", \"reproducibility\", \"citation_quality\"];\n const averaged = {};\n for (const field of allFields) {\n const values = judges.map(j => j.scores[field]).filter(v => typeof v === \"number\");\n averaged[field] = values.length > 0\n ? Math.round((values.reduce((a, b) => a + b, 0) / values.length) * 10) / 10\n : 0;\n }\n\n const sectionValues = SECTIONS.map(s => averaged[s]);\n const overall = Math.round((sectionValues.reduce((a, b) => a + b, 0) / SECTIONS.length) * 10) / 10;\n\n const result = {\n sections: Object.fromEntries(SECTIONS.map(s => [s, averaged[s]])),\n overall,\n novelty: averaged.novelty,\n reproducibility: averaged.reproducibility,\n citation_quality: averaged.citation_quality,\n judges: judges.map(j => j.provider),\n judge_count: judges.length,\n scored_at: new Date().toISOString(),\n paper_type: paperType,\n };\n\n console.log(`[SCORING] Granular score: overall=${overall}, judges=${result.judges.join(\",\")}`);\n return result;\n}\n"}, "dd7344f1aa4e4c90219b8142c8a505543b372ab6": {"t": 1785753964.7640245, "n": 13646, "h": "53c999057a725cbe", "c": "/**\n * P2PCLAW Granular Scoring Service\n * =================================\n * Heterogeneous multi-LLM scoring engine that evaluates papers section-by-section.\n *\n * Provider chain (tested 2026-03-31):\n * 1. Groq — llama-3.3-70b-versatile (4 keys, free)\n * 2. NVIDIA — meta/llama-3.3-70b-instruct (3 keys, free)\n * 3. Cerebras — qwen-3-235b-a22b-instruct-2507 (2 keys, free)\n * 4. Sarvam — sarvam-m (9 keys, free)\n * 5. Mistral — mistral-small-latest (1 key, free)\n * 6. Inception — mercury-2 (6 keys, free)\n * 7. OpenRouter — gemini-2.5-flash (7 keys, paid — last resort)\n * 8. Deterministic heuristic fallback (never blocks)\n *\n * Uses 2 independent LLM judges when possible, falls back through the chain.\n */\n\nconst SECTIONS = [\"abstract\", \"introduction\", \"methodology\", \"results\", \"discussion\", \"conclusion\", \"references\"];\n\n// ── Load keys with rotation ─────────────────────────────────────────────────\n\nfunction loadKeys(envPrefix, maxKeys = 10) {\n const keys = [];\n for (let i = 1; i <= maxKeys; i++) {\n const k = process.env[`${envPrefix}_${i}`] || process.env[`${envPrefix}${i}`];\n if (k) keys.push(k);\n }\n // Also check single-key env vars\n const single = process.env[envPrefix];\n if (single && !keys.includes(single)) keys.unshift(single);\n return keys;\n}\n\nconst keyIndices = {};\nfunction nextKey(providerId, keys) {\n if (!keys.length) return null;\n const idx = keyIndices[providerId] || 0;\n const key = keys[idx % keys.length];\n keyIndices[providerId] = (idx + 1) % keys.length;\n return key;\n}\n\n// ── Provider definitions ────────────────────────────────────────────────────\n\nconst PROVIDERS = [\n {\n id: \"groq\",\n name: \"Groq\",\n url: \"https://api.groq.com/openai/v1/chat/completions\",\n model: \"llama-3.3-70b-versatile\",\n keys: loadKeys(\"GROQ_API_KEY\").concat(loadKeys(\"GROQ_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n {\n id: \"nvidia\",\n name: \"NVIDIA\",\n url: \"https://integrate.api.nvidia.com/v1/chat/completions\",\n model: \"meta/llama-3.3-70b-instruct\",\n keys: loadKeys(\"NVAPI_KEY\").concat(loadKeys(\"NVIDIA_API_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n {\n id: \"cerebras\",\n name: \"Cerebras\",\n url: \"https://api.cerebras.ai/v1/chat/completions\",\n model: \"qwen-3-235b-a22b-instruct-2507\",\n keys: loadKeys(\"CEREBRAS_API_KEY\").concat(loadKeys(\"CEREBRAS_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n {\n id: \"sarvam\",\n name: \"Sarvam\",\n url: \"https://api.sarvam.ai/v1/chat/completions\",\n model: \"sarvam-m\",\n keys: loadKeys(\"SARVAM_KEY\").concat(loadKeys(\"SARVAM_API_KEY\")),\n authHeader: \"api-subscription-key\",\n authPrefix: \"\",\n },\n {\n id: \"mistral\",\n name: \"Mistral\",\n url: \"https://api.mistral.ai/v1/chat/completions\",\n model: \"mistral-small-latest\",\n keys: loadKeys(\"MISTRAL_API_KEY\").concat(loadKeys(\"MISTRAL_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n {\n id: \"inception\",\n name: \"Inception\",\n url: \"https://api.inceptionlabs.ai/v1/chat/completions\",\n model: \"mercury-2\",\n keys: loadKeys(\"INCEPTION_API_KEY\").concat(loadKeys(\"INCEPTION_KEY\")),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n {\n id: \"openrouter\",\n name: \"OpenRouter\",\n url: \"https://openrouter.ai/api/v1/chat/completions\",\n model: \"google/gemini-2.5-flash\",\n keys: loadKeys(\"OPENROUTER_API_KEY\"),\n authHeader: \"Authorization\",\n authPrefix: \"Bearer \",\n },\n];\n\n// Deduplicate keys within each provider\nfor (const p of PROVIDERS) {\n p.keys = [...new Set(p.keys)].filter(Boolean);\n}\n\n// Log available providers\nconst available = PROVIDERS.filter(p => p.keys.length > 0);\nconsole.log(`[SCORING] ${available.length} LLM providers available: ${available.map(p => `${p.name}(${p.keys.length})`).join(\", \")}`);\nif (available.length === 0) console.warn(\"[SCORING] No LLM providers — heuristic scoring only.\");\n\nconst SCORING_PROMPT = `You are an academic paper quality evaluator for the P2PCLAW research network.\n\nScore this paper on a 0-10 scale for each criterion. Return ONLY valid JSON, no explanations.\n\nScoring criteria:\n- abstract (0-10): Quality, clarity, and completeness of the abstract\n- introduction (0-10): Problem statement clarity, context, motivation\n- methodology (0-10): Rigor, reproducibility, appropriate methods\n- results (0-10): Quality of findings, data presentation, significance\n- discussion (0-10): Interpretation depth, limitations acknowledged, implications\n- conclusion (0-10): Summary quality, future work suggestions\n- references (0-10): Citation quality, relevance, proper formatting (0 if no real references)\n- novelty (0-10): Original contribution to the field\n- reproducibility (0-10): Could another researcher reproduce this work?\n- citation_quality (0-10): Are references real, relevant, and properly cited?\n\nIMPORTANT RULES:\n- If a section is MISSING entirely, score it 0\n- If references are placeholder/fake (e.g. \"[1] Author, A. (2026). Placeholder\"), score references as 1\n- Trivial proofs or papers under 300 words should score below 3 overall\n- Papers that prove obvious things (like 0*0=0) should score novelty as 0-1\n- Be STRICT — a score of 8+ means genuinely publishable quality\n\nReturn ONLY this JSON format:\n{\"abstract\":N,\"introduction\":N,\"methodology\":N,\"results\":N,\"discussion\":N,\"conclusion\":N,\"references\":N,\"novelty\":N,\"reproducibility\":N,\"citation_quality\":N}\n\nPaper content:\n`;\n\n/**\n * Call a single LLM provider to score a paper.\n * Tries up to 2 keys from the provider before giving up.\n */\nasync function callLLMForScoring(prompt, provider) {\n if (!provider.keys.length) return null;\n\n const maxAttempts = Math.min(provider.keys.length, 2);\n for (let attempt = 0; attempt < maxAttempts; attempt++) {\n const key = nextKey(provider.id, provider.keys);\n if (!key) return null;\n\n const headers = { \"Content-Type\": \"application/json\" };\n if (provider.authHeader === \"api-subscription-key\") {\n headers[\"api-subscription-key\"] = key;\n } else {\n headers[provider.authHeader] = `${provider.authPrefix}${key}`;\n }\n\n try {\n const res = await fetch(provider.url, {\n method: \"POST\",\n headers,\n body: JSON.stringify({\n model: provider.model,\n messages: [{ role: \"user\", content: prompt }],\n max_tokens: 512,\n temperature: 0.1,\n }),\n signal: AbortSignal.timeout(30000),\n });\n\n if (res.status === 429) {\n console.warn(`[SCORING] ${provider.name} 429 rate-limited`);\n await new Promise(r => setTimeout(r, 3000));\n continue;\n }\n if (res.status === 402) {\n console.warn(`[SCORING] ${provider.name} 402 credits exhausted — skipping provider`);\n return null;\n }\n if (!res.ok) {\n const errBody = await res.text().catch(() => \"\");\n console.warn(`[SCORING] ${provider.name} HTTP ${res.status}: ${errBody.substring(0, 150)}`);\n continue;\n }\n\n const data = await res.json();\n const text = data.choices?.[0]?.message?.content || \"\";\n\n const jsonMatch = text.match(/\\{[\\s\\S]*?\\}/);\n if (!jsonMatch) {\n console.warn(`[SCORING] ${provider.name} — no JSON in response`);\n continue;\n }\n\n const parsed = JSON.parse(jsonMatch[0]);\n const fields = [...SECTIONS, \"novelty\", \"reproducibility\", \"citation_quality\"];\n for (const field of fields) {\n if (typeof parsed[field] !== \"number\" || parsed[field] < 0 || parsed[field] > 10) {\n parsed[field] = typeof parsed[field] === \"number\" ? Math.max(0, Math.min(10, Math.round(parsed[field]))) : 5;\n }\n }\n\n console.log(`[SCORING] ${provider.name} scored successfully`);\n return { scores: parsed, provider: provider.name };\n } catch (e) {\n console.warn(`[SCORING] ${provider.name} error: ${e.message}`);\n }\n }\n return null;\n}\n\n/**\n * Deterministic heuristic scoring — used when all LLMs fail.\n */\nfunction heuristicScore(content) {\n const text = content || \"\";\n const words = text.split(/\\s+/).filter(w => w.length > 0);\n const wordCount = words.length;\n const lowerText = text.toLowerCase();\n\n const sectionScores = {};\n for (const section of SECTIONS) {\n const hasSection = lowerText.includes(`## ${section}`) || lowerText.includes(`# ${section}`);\n if (!hasSection) {\n sectionScores[section] = 0;\n continue;\n }\n const sectionRegex = new RegExp(`##?\\\\s*${section}[\\\\s\\\\S]*?(?=##?\\\\s|$)`, \"i\");\n const match = text.match(sectionRegex);\n const sectionWords = match ? match[0].split(/\\s+/).length : 0;\n if (sectionWords < 20) sectionScores[section] = 2;\n else if (sectionWords < 50) sectionScores[section] = 4;\n else if (sectionWords < 100) sectionScores[section] = 5;\n else if (sectionWords < 200) sectionScores[section] = 6;\n else sectionScores[section] = 7;\n }\n\n const refMatches = text.match(/\\[\\d+\\]/g) || [];\n const uniqueRefs = new Set(refMatches).size;\n const hasPlaceholderRefs = /placeholder|author,?\\s*a\\.\\s*\\(\\d{4}\\)/i.test(text);\n const hasRealAuthors = /[A-Z][a-z]+,\\s*[A-Z]\\.\\s*(?:&|,|et al)/g.test(text);\n const hasDOI = /doi\\.org|arxiv\\.org|10\\.\\d{4}/i.test(text);\n let refScore = hasPlaceholderRefs ? 1 : Math.min(7, uniqueRefs);\n if (hasRealAuthors) refScore = Math.min(10, refScore + 1);\n if (hasDOI) refScore = Math.min(10, refScore + 1);\n sectionScores.references = refScore;\n\n const technicalTerms = (text.match(/\\b(algorithm|theorem|proof|complexity|O\\([^)]+\\)|convergence|optimal|novel|framework)\\b/gi) || []).length;\n const hasFigures = /figure \\d|fig\\.\\s*\\d|table \\d/i.test(text);\n let novelty = wordCount > 2000 ? 4 : wordCount > 1000 ? 3 : 2;\n novelty += Math.min(3, Math.floor(technicalTerms / 5));\n if (hasFigures) novelty += 1;\n\n const hasCode = /```[\\s\\S]*?```/.test(text);\n const hasEquations = /\\$[^$]+\\$/.test(text) || /\\\\begin\\{/.test(text);\n const hasNumbers = /\\d+\\.\\d+%|\\d+\\.\\d+x|p\\s*[<>]\\s*0\\.\\d/i.test(text);\n let reproducibility = (hasCode ? 5 : 3) + (hasEquations ? 1 : 0) + (wordCount > 2000 ? 1 : 0);\n if (hasNumbers) reproducibility += 1;\n\n const citation_quality = hasPlaceholderRefs ? 1 : Math.min(6, uniqueRefs);\n\n return {\n scores: {\n ...sectionScores,\n novelty: Math.min(10, novelty),\n reproducibility: Math.min(10, reproducibility),\n citation_quality: Math.min(10, citation_quality),\n },\n provider: \"heuristic\"\n };\n}\n\n/**\n * Score a paper using heterogeneous multi-LLM swarm.\n * Tries to get 2 independent judges, falls back through provider chain.\n */\nexport async function scoreGranular(content, paperType = \"research\") {\n if (!content || content.trim().length < 50) {\n return {\n sections: Object.fromEntries(SECTIONS.map(s => [s, 0])),\n overall: 0,\n novelty: 0,\n reproducibility: 0,\n citation_quality: 0,\n judges: [],\n judge_count: 0,\n error: \"Content too short to score\"\n };\n }\n\n const truncated = content.length > 4000 ? content.substring(0, 4000) + \"\\n\\n[... truncated for scoring ...]\" : content;\n const prompt = SCORING_PROMPT + truncated;\n\n // Try to get 2 independent LLM judges from the provider chain\n const judges = [];\n for (const provider of available) {\n if (judges.length >= 2) break;\n const result = await callLLMForScoring(prompt, provider);\n if (result) judges.push(result);\n }\n\n // If no LLM judges succeeded, use heuristic\n if (judges.length === 0) {\n const heuristic = heuristicScore(content);\n judges.push(heuristic);\n }\n\n // Average scores across all judges\n const allFields = [...SECTIONS, \"novelty\", \"reproducibility\", \"citation_quality\"];\n const averaged = {};\n for (const field of allFields) {\n const values = judges.map(j => j.scores[field]).filter(v => typeof v === \"number\");\n averaged[field] = values.length > 0\n ? Math.round((values.reduce((a, b) => a + b, 0) / values.length) * 10) / 10\n : 0;\n }\n\n const sectionValues = SECTIONS.map(s => averaged[s]);\n const overall = Math.round((sectionValues.reduce((a, b) => a + b, 0) / SECTIONS.length) * 10) / 10;\n\n const result = {\n sections: Object.fromEntries(SECTIONS.map(s => [s, averaged[s]])),\n overall,\n novelty: averaged.novelty,\n reproducibility: averaged.reproducibility,\n citation_quality: averaged.citation_quality,\n judges: judges.map(j => j.provider),\n judge_count: judges.length,\n scored_at: new Date().toISOString(),\n paper_type: paperType,\n };\n\n console.log(`[SCORING] Granular score: overall=${overall}, judges=${result.judges.join(\",\")}`);\n return result;\n}\n"}, "88a7ea8a1ac2fc8632c5484d3ff0f8cf9b4fd1ce": {"t": 1785753966.1738057, "n": 13420, "h": "e46d80a164065808", "c": "/**\n * P2PCLAW Granular Scoring Service\n * =================================\n * Heterogeneous multi-LLM scoring engine that evaluates papers section-by-section.\n *\n * Pilar 3: Quality Training Dataset Factory\n *\n * Each paper gets scored on 7 sections (0-10) plus 3 meta-dimensions:\n * novelty, reproducibility, citation_quality.\n *\n * Uses Groq + Together AI as independent judges (heterogeneous swarm).\n * If both LLMs fail → deterministic heuristic scoring (never blocks).\n *\n * PURELY ADDITIVE — does not modify any existing service.\n */\n\nconst GROQ_API_KEY = process.env.GROQ_API_KEY || process.env.LLM_KEY || process.env.GROQ_KEY || \"\";\nconst TOGETHER_API_KEY = process.env.TOGETHER_API_KEY || process.env.TOGETHER_KEY || \"\";\n\n// Startup diagnostics\nif (GROQ_API_KEY) console.log(`[SCORING] Groq key loaded (${GROQ_API_KEY.length} chars, starts: ${GROQ_API_KEY.substring(0, 8)}...)`);\nelse console.warn(\"[SCORING] No Groq API key — heuristic scoring only. Set GROQ_API_KEY in Railway env.\");\nif (TOGETHER_API_KEY) console.log(`[SCORING] Together key loaded (${TOGETHER_API_KEY.length} chars)`);\nelse console.warn(\"[SCORING] No Together API key — Groq-only mode.\");\n\nconst SECTIONS = [\"abstract\", \"introduction\", \"methodology\", \"results\", \"discussion\", \"conclusion\", \"references\"];\n\nconst SCORING_PROMPT = `You are an academic paper quality evaluator for the P2PCLAW research network.\n\nScore this paper on a 0-10 scale for each criterion. Return ONLY valid JSON, no explanations.\n\nScoring criteria:\n- abstract (0-10): Quality, clarity, and completeness of the abstract\n- introduction (0-10): Problem statement clarity, context, motivation\n- methodology (0-10): Rigor, reproducibility, appropriate methods\n- results (0-10): Quality of findings, data presentation, significance\n- discussion (0-10): Interpretation depth, limitations acknowledged, implications\n- conclusion (0-10): Summary quality, future work suggestions\n- references (0-10): Citation quality, relevance, proper formatting (0 if no real references)\n- novelty (0-10): Original contribution to the field\n- reproducibility (0-10): Could another researcher reproduce this work?\n- citation_quality (0-10): Are references real, relevant, and properly cited?\n\nIMPORTANT RULES:\n- If a section is MISSING entirely, score it 0\n- If references are placeholder/fake (e.g. \"[1] Author, A. (2026). Placeholder\"), score references as 1\n- Trivial proofs or papers under 300 words should score below 3 overall\n- Papers that prove obvious things (like 0*0=0) should score novelty as 0-1\n- Be STRICT — a score of 8+ means genuinely publishable quality\n\nReturn ONLY this JSON format:\n{\"abstract\":N,\"introduction\":N,\"methodology\":N,\"results\":N,\"discussion\":N,\"conclusion\":N,\"references\":N,\"novelty\":N,\"reproducibility\":N,\"citation_quality\":N}\n\nPaper content:\n`;\n\n/**\n * Call a single LLM to score a paper. Returns parsed JSON or null.\n */\nasync function callLLMForScoring(prompt, provider) {\n const configs = {\n groq: {\n url: \"https://api.groq.com/openai/v1/chat/completions\",\n key: GROQ_API_KEY,\n model: \"llama-3.3-70b-versatile\",\n },\n together: {\n url: \"https://api.together.xyz/v1/chat/completions\",\n key: TOGETHER_API_KEY,\n model: \"Qwen/Qwen2.5-72B-Instruct-Turbo\",\n }\n };\n\n const cfg = configs[provider];\n if (!cfg || !cfg.key) {\n console.warn(`[SCORING] ${provider} skipped — no API key configured`);\n return null;\n }\n console.log(`[SCORING] Calling ${provider} (model: ${cfg.model})...`);\n\n try {\n const res = await fetch(cfg.url, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"Authorization\": `Bearer ${cfg.key}`\n },\n body: JSON.stringify({\n model: cfg.model,\n messages: [{ role: \"user\", content: prompt }],\n max_tokens: 512,\n temperature: 0.1, // Very low for consistent scoring\n }),\n signal: AbortSignal.timeout(30000),\n });\n\n if (!res.ok) {\n const errBody = await res.text().catch(() => \"\");\n // Retry once on 429 (rate limit) after a short delay\n if (res.status === 429) {\n const retryAfter = parseInt(res.headers.get(\"retry-after\") || \"5\", 10);\n const delay = Math.min(retryAfter, 10) * 1000;\n console.warn(`[SCORING] ${provider} HTTP 429 — retrying in ${delay/1000}s...`);\n await new Promise(r => setTimeout(r, delay));\n const retry = await fetch(cfg.url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", \"Authorization\": `Bearer ${cfg.key}` },\n body: JSON.stringify({ model: cfg.model, messages: [{ role: \"user\", content: prompt }], max_tokens: 512, temperature: 0.1 }),\n signal: AbortSignal.timeout(30000),\n });\n if (retry.ok) {\n const retryData = await retry.json();\n const retryText = retryData.choices?.[0]?.message?.content || \"\";\n console.log(`[SCORING] ${provider} retry succeeded (${retryText.length} chars)`);\n const retryJson = retryText.match(/\\{[\\s\\S]*?\\}/);\n if (retryJson) {\n const retryParsed = JSON.parse(retryJson[0]);\n const fields = [...SECTIONS, \"novelty\", \"reproducibility\", \"citation_quality\"];\n for (const f of fields) {\n if (typeof retryParsed[f] !== \"number\" || retryParsed[f] < 0 || retryParsed[f] > 10) {\n retryParsed[f] = typeof retryParsed[f] === \"number\" ? Math.max(0, Math.min(10, Math.round(retryParsed[f]))) : 5;\n }\n }\n return { scores: retryParsed, provider };\n }\n }\n console.warn(`[SCORING] ${provider} retry also failed`);\n } else {\n console.warn(`[SCORING] ${provider} HTTP ${res.status}: ${errBody.substring(0, 200)}`);\n }\n return null;\n }\n\n const data = await res.json();\n const text = data.choices?.[0]?.message?.content || \"\";\n\n console.log(`[SCORING] ${provider} returned ${text.length} chars`);\n\n // Extract JSON from response (handle markdown code blocks)\n const jsonMatch = text.match(/\\{[\\s\\S]*?\\}/);\n if (!jsonMatch) {\n console.warn(`[SCORING] ${provider} — no JSON found in response: ${text.substring(0, 200)}`);\n return null;\n }\n\n const parsed = JSON.parse(jsonMatch[0]);\n\n // Validate all expected fields are numbers 0-10\n const fields = [...SECTIONS, \"novelty\", \"reproducibility\", \"citation_quality\"];\n for (const field of fields) {\n if (typeof parsed[field] !== \"number\" || parsed[field] < 0 || parsed[field] > 10) {\n parsed[field] = typeof parsed[field] === \"number\" ? Math.max(0, Math.min(10, Math.round(parsed[field]))) : 5;\n }\n }\n\n return { scores: parsed, provider };\n } catch (e) {\n console.warn(`[SCORING] ${provider} failed:`, e.message);\n return null;\n }\n}\n\n/**\n * Deterministic heuristic scoring — used when all LLMs fail.\n * Analyses text structure, word count, references, etc.\n */\nfunction heuristicScore(content) {\n const text = content || \"\";\n const words = text.split(/\\s+/).filter(w => w.length > 0);\n const wordCount = words.length;\n const lowerText = text.toLowerCase();\n\n // Detect sections present\n const sectionScores = {};\n for (const section of SECTIONS) {\n const hasSection = lowerText.includes(`## ${section}`) || lowerText.includes(`# ${section}`);\n if (!hasSection) {\n sectionScores[section] = 0;\n continue;\n }\n // Base score for having the section, scaled by word count in that section\n const sectionRegex = new RegExp(`##?\\\\s*${section}[\\\\s\\\\S]*?(?=##?\\\\s|$)`, \"i\");\n const match = text.match(sectionRegex);\n const sectionWords = match ? match[0].split(/\\s+/).length : 0;\n if (sectionWords < 20) sectionScores[section] = 2;\n else if (sectionWords < 50) sectionScores[section] = 4;\n else if (sectionWords < 100) sectionScores[section] = 5;\n else if (sectionWords < 200) sectionScores[section] = 6;\n else sectionScores[section] = 7;\n }\n\n // Reference quality — check for real citations vs placeholders\n const refMatches = text.match(/\\[\\d+\\]/g) || [];\n const uniqueRefs = new Set(refMatches).size;\n const hasPlaceholderRefs = /placeholder|author,?\\s*a\\.\\s*\\(\\d{4}\\)/i.test(text);\n const hasRealAuthors = /[A-Z][a-z]+,\\s*[A-Z]\\.\\s*(?:&|,|et al)/g.test(text); // \"Smith, J. &\" pattern\n const hasDOI = /doi\\.org|arxiv\\.org|10\\.\\d{4}/i.test(text);\n let refScore = hasPlaceholderRefs ? 1 : Math.min(7, uniqueRefs);\n if (hasRealAuthors) refScore = Math.min(10, refScore + 1);\n if (hasDOI) refScore = Math.min(10, refScore + 1);\n sectionScores.references = refScore;\n\n // Novelty — heuristic: unique terms, technical depth indicators\n const technicalTerms = (text.match(/\\b(algorithm|theorem|proof|complexity|O\\([^)]+\\)|convergence|optimal|novel|framework)\\b/gi) || []).length;\n const hasFigures = /figure \\d|fig\\.\\s*\\d|table \\d/i.test(text);\n let novelty = wordCount > 2000 ? 4 : wordCount > 1000 ? 3 : 2;\n novelty += Math.min(3, Math.floor(technicalTerms / 5));\n if (hasFigures) novelty += 1;\n\n // Reproducibility\n const hasCode = /```[\\s\\S]*?```/.test(text);\n const hasEquations = /\\$[^$]+\\$/.test(text) || /\\\\begin\\{/.test(text);\n const hasNumbers = /\\d+\\.\\d+%|\\d+\\.\\d+x|p\\s*[<>]\\s*0\\.\\d/i.test(text); // quantitative results\n let reproducibility = (hasCode ? 5 : 3) + (hasEquations ? 1 : 0) + (wordCount > 2000 ? 1 : 0);\n if (hasNumbers) reproducibility += 1;\n\n // Citation quality\n const citation_quality = hasPlaceholderRefs ? 1 : Math.min(6, uniqueRefs);\n\n return {\n scores: {\n ...sectionScores,\n novelty: Math.min(10, novelty),\n reproducibility: Math.min(10, reproducibility),\n citation_quality: Math.min(10, citation_quality),\n },\n provider: \"heuristic\"\n };\n}\n\n/**\n * Score a paper using heterogeneous multi-LLM swarm.\n * Returns averaged scores from multiple judges.\n *\n * @param {string} content - Paper content (Markdown)\n * @param {string} paperType - \"research\" | \"review\" | \"technical\" | \"proof\"\n * @returns {Promise<{sections: object, overall: number, novelty: number, reproducibility: number, citation_quality: number, judges: string[], judge_count: number}>}\n */\nexport async function scoreGranular(content, paperType = \"research\") {\n if (!content || content.trim().length < 50) {\n return {\n sections: Object.fromEntries(SECTIONS.map(s => [s, 0])),\n overall: 0,\n novelty: 0,\n reproducibility: 0,\n citation_quality: 0,\n judges: [],\n judge_count: 0,\n error: \"Content too short to score\"\n };\n }\n\n // Truncate content to ~4000 chars for LLM context (enough for evaluation)\n const truncated = content.length > 4000 ? content.substring(0, 4000) + \"\\n\\n[... truncated for scoring ...]\" : content;\n const prompt = SCORING_PROMPT + truncated;\n\n // Call both LLMs in parallel (heterogeneous swarm)\n const [groqResult, togetherResult] = await Promise.allSettled([\n callLLMForScoring(prompt, \"groq\"),\n callLLMForScoring(prompt, \"together\")\n ]);\n\n const judges = [];\n if (groqResult.status === \"fulfilled\" && groqResult.value) judges.push(groqResult.value);\n if (togetherResult.status === \"fulfilled\" && togetherResult.value) judges.push(togetherResult.value);\n\n // If no LLM judges succeeded, use heuristic\n if (judges.length === 0) {\n const heuristic = heuristicScore(content);\n judges.push(heuristic);\n }\n\n // Average scores across all judges\n const allFields = [...SECTIONS, \"novelty\", \"reproducibility\", \"citation_quality\"];\n const averaged = {};\n for (const field of allFields) {\n const values = judges.map(j => j.scores[field]).filter(v => typeof v === \"number\");\n averaged[field] = values.length > 0\n ? Math.round((values.reduce((a, b) => a + b, 0) / values.length) * 10) / 10\n : 0;\n }\n\n // Compute overall as weighted average of sections\n const sectionValues = SECTIONS.map(s => averaged[s]);\n const overall = Math.round((sectionValues.reduce((a, b) => a + b, 0) / SECTIONS.length) * 10) / 10;\n\n const result = {\n sections: Object.fromEntries(SECTIONS.map(s => [s, averaged[s]])),\n overall,\n novelty: averaged.novelty,\n reproducibility: averaged.reproducibility,\n citation_quality: averaged.citation_quality,\n judges: judges.map(j => j.provider),\n judge_count: judges.length,\n scored_at: new Date().toISOString(),\n paper_type: paperType,\n };\n\n console.log(`[SCORING] Granular score: overall=${overall}, judges=${result.judges.join(\",\")}`);\n return result;\n}\n"}, "602d8ff86b8a30ce3a253587eeec1fe303301249": {"t": 1785753967.7692728, "n": 11445, "h": "094732ddd57be3e0", "c": "/**\n * P2PCLAW Granular Scoring Service\n * =================================\n * Heterogeneous multi-LLM scoring engine that evaluates papers section-by-section.\n *\n * Pilar 3: Quality Training Dataset Factory\n *\n * Each paper gets scored on 7 sections (0-10) plus 3 meta-dimensions:\n * novelty, reproducibility, citation_quality.\n *\n * Uses Groq + Together AI as independent judges (heterogeneous swarm).\n * If both LLMs fail → deterministic heuristic scoring (never blocks).\n *\n * PURELY ADDITIVE — does not modify any existing service.\n */\n\nconst GROQ_API_KEY = process.env.GROQ_API_KEY || process.env.LLM_KEY || process.env.GROQ_KEY || \"\";\nconst TOGETHER_API_KEY = process.env.TOGETHER_API_KEY || process.env.TOGETHER_KEY || \"\";\n\n// Startup diagnostics\nif (GROQ_API_KEY) console.log(`[SCORING] Groq key loaded (${GROQ_API_KEY.length} chars, starts: ${GROQ_API_KEY.substring(0, 8)}...)`);\nelse console.warn(\"[SCORING] No Groq API key — heuristic scoring only. Set GROQ_API_KEY in Railway env.\");\nif (TOGETHER_API_KEY) console.log(`[SCORING] Together key loaded (${TOGETHER_API_KEY.length} chars)`);\nelse console.warn(\"[SCORING] No Together API key — Groq-only mode.\");\n\nconst SECTIONS = [\"abstract\", \"introduction\", \"methodology\", \"results\", \"discussion\", \"conclusion\", \"references\"];\n\nconst SCORING_PROMPT = `You are an academic paper quality evaluator for the P2PCLAW research network.\n\nScore this paper on a 0-10 scale for each criterion. Return ONLY valid JSON, no explanations.\n\nScoring criteria:\n- abstract (0-10): Quality, clarity, and completeness of the abstract\n- introduction (0-10): Problem statement clarity, context, motivation\n- methodology (0-10): Rigor, reproducibility, appropriate methods\n- results (0-10): Quality of findings, data presentation, significance\n- discussion (0-10): Interpretation depth, limitations acknowledged, implications\n- conclusion (0-10): Summary quality, future work suggestions\n- references (0-10): Citation quality, relevance, proper formatting (0 if no real references)\n- novelty (0-10): Original contribution to the field\n- reproducibility (0-10): Could another researcher reproduce this work?\n- citation_quality (0-10): Are references real, relevant, and properly cited?\n\nIMPORTANT RULES:\n- If a section is MISSING entirely, score it 0\n- If references are placeholder/fake (e.g. \"[1] Author, A. (2026). Placeholder\"), score references as 1\n- Trivial proofs or papers under 300 words should score below 3 overall\n- Papers that prove obvious things (like 0*0=0) should score novelty as 0-1\n- Be STRICT — a score of 8+ means genuinely publishable quality\n\nReturn ONLY this JSON format:\n{\"abstract\":N,\"introduction\":N,\"methodology\":N,\"results\":N,\"discussion\":N,\"conclusion\":N,\"references\":N,\"novelty\":N,\"reproducibility\":N,\"citation_quality\":N}\n\nPaper content:\n`;\n\n/**\n * Call a single LLM to score a paper. Returns parsed JSON or null.\n */\nasync function callLLMForScoring(prompt, provider) {\n const configs = {\n groq: {\n url: \"https://api.groq.com/openai/v1/chat/completions\",\n key: GROQ_API_KEY,\n model: \"llama-3.3-70b-versatile\",\n },\n together: {\n url: \"https://api.together.xyz/v1/chat/completions\",\n key: TOGETHER_API_KEY,\n model: \"Qwen/Qwen2.5-72B-Instruct-Turbo\",\n }\n };\n\n const cfg = configs[provider];\n if (!cfg || !cfg.key) {\n console.warn(`[SCORING] ${provider} skipped — no API key configured`);\n return null;\n }\n console.log(`[SCORING] Calling ${provider} (model: ${cfg.model})...`);\n\n try {\n const res = await fetch(cfg.url, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"Authorization\": `Bearer ${cfg.key}`\n },\n body: JSON.stringify({\n model: cfg.model,\n messages: [{ role: \"user\", content: prompt }],\n max_tokens: 512,\n temperature: 0.1, // Very low for consistent scoring\n }),\n signal: AbortSignal.timeout(30000),\n });\n\n if (!res.ok) {\n const errBody = await res.text().catch(() => \"\");\n console.warn(`[SCORING] ${provider} HTTP ${res.status}: ${errBody.substring(0, 200)}`);\n return null;\n }\n\n const data = await res.json();\n const text = data.choices?.[0]?.message?.content || \"\";\n\n console.log(`[SCORING] ${provider} returned ${text.length} chars`);\n\n // Extract JSON from response (handle markdown code blocks)\n const jsonMatch = text.match(/\\{[\\s\\S]*?\\}/);\n if (!jsonMatch) {\n console.warn(`[SCORING] ${provider} — no JSON found in response: ${text.substring(0, 200)}`);\n return null;\n }\n\n const parsed = JSON.parse(jsonMatch[0]);\n\n // Validate all expected fields are numbers 0-10\n const fields = [...SECTIONS, \"novelty\", \"reproducibility\", \"citation_quality\"];\n for (const field of fields) {\n if (typeof parsed[field] !== \"number\" || parsed[field] < 0 || parsed[field] > 10) {\n parsed[field] = typeof parsed[field] === \"number\" ? Math.max(0, Math.min(10, Math.round(parsed[field]))) : 5;\n }\n }\n\n return { scores: parsed, provider };\n } catch (e) {\n console.warn(`[SCORING] ${provider} failed:`, e.message);\n return null;\n }\n}\n\n/**\n * Deterministic heuristic scoring — used when all LLMs fail.\n * Analyses text structure, word count, references, etc.\n */\nfunction heuristicScore(content) {\n const text = content || \"\";\n const words = text.split(/\\s+/).filter(w => w.length > 0);\n const wordCount = words.length;\n const lowerText = text.toLowerCase();\n\n // Detect sections present\n const sectionScores = {};\n for (const section of SECTIONS) {\n const hasSection = lowerText.includes(`## ${section}`) || lowerText.includes(`# ${section}`);\n if (!hasSection) {\n sectionScores[section] = 0;\n continue;\n }\n // Base score for having the section, scaled by word count in that section\n const sectionRegex = new RegExp(`##?\\\\s*${section}[\\\\s\\\\S]*?(?=##?\\\\s|$)`, \"i\");\n const match = text.match(sectionRegex);\n const sectionWords = match ? match[0].split(/\\s+/).length : 0;\n if (sectionWords < 20) sectionScores[section] = 2;\n else if (sectionWords < 50) sectionScores[section] = 4;\n else if (sectionWords < 100) sectionScores[section] = 5;\n else if (sectionWords < 200) sectionScores[section] = 6;\n else sectionScores[section] = 7;\n }\n\n // Reference quality — check for real citations vs placeholders\n const refMatches = text.match(/\\[\\d+\\]/g) || [];\n const uniqueRefs = new Set(refMatches).size;\n const hasPlaceholderRefs = /placeholder|author,?\\s*a\\.\\s*\\(\\d{4}\\)/i.test(text);\n const hasRealAuthors = /[A-Z][a-z]+,\\s*[A-Z]\\.\\s*(?:&|,|et al)/g.test(text); // \"Smith, J. &\" pattern\n const hasDOI = /doi\\.org|arxiv\\.org|10\\.\\d{4}/i.test(text);\n let refScore = hasPlaceholderRefs ? 1 : Math.min(7, uniqueRefs);\n if (hasRealAuthors) refScore = Math.min(10, refScore + 1);\n if (hasDOI) refScore = Math.min(10, refScore + 1);\n sectionScores.references = refScore;\n\n // Novelty — heuristic: unique terms, technical depth indicators\n const technicalTerms = (text.match(/\\b(algorithm|theorem|proof|complexity|O\\([^)]+\\)|convergence|optimal|novel|framework)\\b/gi) || []).length;\n const hasFigures = /figure \\d|fig\\.\\s*\\d|table \\d/i.test(text);\n let novelty = wordCount > 2000 ? 4 : wordCount > 1000 ? 3 : 2;\n novelty += Math.min(3, Math.floor(technicalTerms / 5));\n if (hasFigures) novelty += 1;\n\n // Reproducibility\n const hasCode = /```[\\s\\S]*?```/.test(text);\n const hasEquations = /\\$[^$]+\\$/.test(text) || /\\\\begin\\{/.test(text);\n const hasNumbers = /\\d+\\.\\d+%|\\d+\\.\\d+x|p\\s*[<>]\\s*0\\.\\d/i.test(text); // quantitative results\n let reproducibility = (hasCode ? 5 : 3) + (hasEquations ? 1 : 0) + (wordCount > 2000 ? 1 : 0);\n if (hasNumbers) reproducibility += 1;\n\n // Citation quality\n const citation_quality = hasPlaceholderRefs ? 1 : Math.min(6, uniqueRefs);\n\n return {\n scores: {\n ...sectionScores,\n novelty: Math.min(10, novelty),\n reproducibility: Math.min(10, reproducibility),\n citation_quality: Math.min(10, citation_quality),\n },\n provider: \"heuristic\"\n };\n}\n\n/**\n * Score a paper using heterogeneous multi-LLM swarm.\n * Returns averaged scores from multiple judges.\n *\n * @param {string} content - Paper content (Markdown)\n * @param {string} paperType - \"research\" | \"review\" | \"technical\" | \"proof\"\n * @returns {Promise<{sections: object, overall: number, novelty: number, reproducibility: number, citation_quality: number, judges: string[], judge_count: number}>}\n */\nexport async function scoreGranular(content, paperType = \"research\") {\n if (!content || content.trim().length < 50) {\n return {\n sections: Object.fromEntries(SECTIONS.map(s => [s, 0])),\n overall: 0,\n novelty: 0,\n reproducibility: 0,\n citation_quality: 0,\n judges: [],\n judge_count: 0,\n error: \"Content too short to score\"\n };\n }\n\n // Truncate content to ~4000 chars for LLM context (enough for evaluation)\n const truncated = content.length > 4000 ? content.substring(0, 4000) + \"\\n\\n[... truncated for scoring ...]\" : content;\n const prompt = SCORING_PROMPT + truncated;\n\n // Call both LLMs in parallel (heterogeneous swarm)\n const [groqResult, togetherResult] = await Promise.allSettled([\n callLLMForScoring(prompt, \"groq\"),\n callLLMForScoring(prompt, \"together\")\n ]);\n\n const judges = [];\n if (groqResult.status === \"fulfilled\" && groqResult.value) judges.push(groqResult.value);\n if (togetherResult.status === \"fulfilled\" && togetherResult.value) judges.push(togetherResult.value);\n\n // If no LLM judges succeeded, use heuristic\n if (judges.length === 0) {\n const heuristic = heuristicScore(content);\n judges.push(heuristic);\n }\n\n // Average scores across all judges\n const allFields = [...SECTIONS, \"novelty\", \"reproducibility\", \"citation_quality\"];\n const averaged = {};\n for (const field of allFields) {\n const values = judges.map(j => j.scores[field]).filter(v => typeof v === \"number\");\n averaged[field] = values.length > 0\n ? Math.round((values.reduce((a, b) => a + b, 0) / values.length) * 10) / 10\n : 0;\n }\n\n // Compute overall as weighted average of sections\n const sectionValues = SECTIONS.map(s => averaged[s]);\n const overall = Math.round((sectionValues.reduce((a, b) => a + b, 0) / SECTIONS.length) * 10) / 10;\n\n const result = {\n sections: Object.fromEntries(SECTIONS.map(s => [s, averaged[s]])),\n overall,\n novelty: averaged.novelty,\n reproducibility: averaged.reproducibility,\n citation_quality: averaged.citation_quality,\n judges: judges.map(j => j.provider),\n judge_count: judges.length,\n scored_at: new Date().toISOString(),\n paper_type: paperType,\n };\n\n console.log(`[SCORING] Granular score: overall=${overall}, judges=${result.judges.join(\",\")}`);\n return result;\n}\n"}, "b531d9e3d775a46563afa7ee2d5821619e4fc1ac": {"t": 1785753969.0315783, "n": 10638, "h": "96a7473b06e34c98", "c": "/**\n * P2PCLAW Granular Scoring Service\n * =================================\n * Heterogeneous multi-LLM scoring engine that evaluates papers section-by-section.\n *\n * Pilar 3: Quality Training Dataset Factory\n *\n * Each paper gets scored on 7 sections (0-10) plus 3 meta-dimensions:\n * novelty, reproducibility, citation_quality.\n *\n * Uses Groq + Together AI as independent judges (heterogeneous swarm).\n * If both LLMs fail → deterministic heuristic scoring (never blocks).\n *\n * PURELY ADDITIVE — does not modify any existing service.\n */\n\nconst GROQ_API_KEY = process.env.GROQ_API_KEY || process.env.LLM_KEY || process.env.GROQ_KEY || \"\";\nconst TOGETHER_API_KEY = process.env.TOGETHER_API_KEY || process.env.TOGETHER_KEY || \"\";\n\n// Startup diagnostics\nif (GROQ_API_KEY) console.log(`[SCORING] Groq key loaded (${GROQ_API_KEY.length} chars, starts: ${GROQ_API_KEY.substring(0, 8)}...)`);\nelse console.warn(\"[SCORING] No Groq API key — heuristic scoring only. Set GROQ_API_KEY in Railway env.\");\nif (TOGETHER_API_KEY) console.log(`[SCORING] Together key loaded (${TOGETHER_API_KEY.length} chars)`);\nelse console.warn(\"[SCORING] No Together API key — Groq-only mode.\");\n\nconst SECTIONS = [\"abstract\", \"introduction\", \"methodology\", \"results\", \"discussion\", \"conclusion\", \"references\"];\n\nconst SCORING_PROMPT = `You are an academic paper quality evaluator for the P2PCLAW research network.\n\nScore this paper on a 0-10 scale for each criterion. Return ONLY valid JSON, no explanations.\n\nScoring criteria:\n- abstract (0-10): Quality, clarity, and completeness of the abstract\n- introduction (0-10): Problem statement clarity, context, motivation\n- methodology (0-10): Rigor, reproducibility, appropriate methods\n- results (0-10): Quality of findings, data presentation, significance\n- discussion (0-10): Interpretation depth, limitations acknowledged, implications\n- conclusion (0-10): Summary quality, future work suggestions\n- references (0-10): Citation quality, relevance, proper formatting (0 if no real references)\n- novelty (0-10): Original contribution to the field\n- reproducibility (0-10): Could another researcher reproduce this work?\n- citation_quality (0-10): Are references real, relevant, and properly cited?\n\nIMPORTANT RULES:\n- If a section is MISSING entirely, score it 0\n- If references are placeholder/fake (e.g. \"[1] Author, A. (2026). Placeholder\"), score references as 1\n- Trivial proofs or papers under 300 words should score below 3 overall\n- Papers that prove obvious things (like 0*0=0) should score novelty as 0-1\n- Be STRICT — a score of 8+ means genuinely publishable quality\n\nReturn ONLY this JSON format:\n{\"abstract\":N,\"introduction\":N,\"methodology\":N,\"results\":N,\"discussion\":N,\"conclusion\":N,\"references\":N,\"novelty\":N,\"reproducibility\":N,\"citation_quality\":N}\n\nPaper content:\n`;\n\n/**\n * Call a single LLM to score a paper. Returns parsed JSON or null.\n */\nasync function callLLMForScoring(prompt, provider) {\n const configs = {\n groq: {\n url: \"https://api.groq.com/openai/v1/chat/completions\",\n key: GROQ_API_KEY,\n model: \"llama-3.3-70b-versatile\",\n },\n together: {\n url: \"https://api.together.xyz/v1/chat/completions\",\n key: TOGETHER_API_KEY,\n model: \"Qwen/Qwen2.5-72B-Instruct-Turbo\",\n }\n };\n\n const cfg = configs[provider];\n if (!cfg || !cfg.key) {\n console.warn(`[SCORING] ${provider} skipped — no API key configured`);\n return null;\n }\n console.log(`[SCORING] Calling ${provider} (model: ${cfg.model})...`);\n\n try {\n const res = await fetch(cfg.url, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"Authorization\": `Bearer ${cfg.key}`\n },\n body: JSON.stringify({\n model: cfg.model,\n messages: [{ role: \"user\", content: prompt }],\n max_tokens: 512,\n temperature: 0.1, // Very low for consistent scoring\n }),\n signal: AbortSignal.timeout(30000),\n });\n\n if (!res.ok) {\n const errBody = await res.text().catch(() => \"\");\n console.warn(`[SCORING] ${provider} HTTP ${res.status}: ${errBody.substring(0, 200)}`);\n return null;\n }\n\n const data = await res.json();\n const text = data.choices?.[0]?.message?.content || \"\";\n\n console.log(`[SCORING] ${provider} returned ${text.length} chars`);\n\n // Extract JSON from response (handle markdown code blocks)\n const jsonMatch = text.match(/\\{[\\s\\S]*?\\}/);\n if (!jsonMatch) {\n console.warn(`[SCORING] ${provider} — no JSON found in response: ${text.substring(0, 200)}`);\n return null;\n }\n\n const parsed = JSON.parse(jsonMatch[0]);\n\n // Validate all expected fields are numbers 0-10\n const fields = [...SECTIONS, \"novelty\", \"reproducibility\", \"citation_quality\"];\n for (const field of fields) {\n if (typeof parsed[field] !== \"number\" || parsed[field] < 0 || parsed[field] > 10) {\n parsed[field] = typeof parsed[field] === \"number\" ? Math.max(0, Math.min(10, Math.round(parsed[field]))) : 5;\n }\n }\n\n return { scores: parsed, provider };\n } catch (e) {\n console.warn(`[SCORING] ${provider} failed:`, e.message);\n return null;\n }\n}\n\n/**\n * Deterministic heuristic scoring — used when all LLMs fail.\n * Analyses text structure, word count, references, etc.\n */\nfunction heuristicScore(content) {\n const text = content || \"\";\n const words = text.split(/\\s+/).filter(w => w.length > 0);\n const wordCount = words.length;\n const lowerText = text.toLowerCase();\n\n // Detect sections present\n const sectionScores = {};\n for (const section of SECTIONS) {\n const hasSection = lowerText.includes(`## ${section}`) || lowerText.includes(`# ${section}`);\n if (!hasSection) {\n sectionScores[section] = 0;\n continue;\n }\n // Base score for having the section, scaled by word count in that section\n const sectionRegex = new RegExp(`##?\\\\s*${section}[\\\\s\\\\S]*?(?=##?\\\\s|$)`, \"i\");\n const match = text.match(sectionRegex);\n const sectionWords = match ? match[0].split(/\\s+/).length : 0;\n if (sectionWords < 20) sectionScores[section] = 2;\n else if (sectionWords < 50) sectionScores[section] = 4;\n else if (sectionWords < 100) sectionScores[section] = 5;\n else if (sectionWords < 200) sectionScores[section] = 6;\n else sectionScores[section] = 7;\n }\n\n // Reference quality\n const refMatches = text.match(/\\[\\d+\\]/g) || [];\n const uniqueRefs = new Set(refMatches).size;\n const hasPlaceholderRefs = /placeholder|author,?\\s*a\\.\\s*\\(\\d{4}\\)/i.test(text);\n sectionScores.references = hasPlaceholderRefs ? 1 : Math.min(7, uniqueRefs);\n\n // Novelty — very hard to assess heuristically, be conservative\n const novelty = wordCount > 2000 ? 4 : wordCount > 1000 ? 3 : 2;\n\n // Reproducibility\n const hasCode = /```[\\s\\S]*?```/.test(text);\n const hasEquations = /\\$[^$]+\\$/.test(text) || /\\\\begin\\{/.test(text);\n const reproducibility = (hasCode ? 5 : 3) + (hasEquations ? 1 : 0) + (wordCount > 2000 ? 1 : 0);\n\n // Citation quality\n const citation_quality = hasPlaceholderRefs ? 1 : Math.min(6, uniqueRefs);\n\n return {\n scores: {\n ...sectionScores,\n novelty: Math.min(10, novelty),\n reproducibility: Math.min(10, reproducibility),\n citation_quality: Math.min(10, citation_quality),\n },\n provider: \"heuristic\"\n };\n}\n\n/**\n * Score a paper using heterogeneous multi-LLM swarm.\n * Returns averaged scores from multiple judges.\n *\n * @param {string} content - Paper content (Markdown)\n * @param {string} paperType - \"research\" | \"review\" | \"technical\" | \"proof\"\n * @returns {Promise<{sections: object, overall: number, novelty: number, reproducibility: number, citation_quality: number, judges: string[], judge_count: number}>}\n */\nexport async function scoreGranular(content, paperType = \"research\") {\n if (!content || content.trim().length < 50) {\n return {\n sections: Object.fromEntries(SECTIONS.map(s => [s, 0])),\n overall: 0,\n novelty: 0,\n reproducibility: 0,\n citation_quality: 0,\n judges: [],\n judge_count: 0,\n error: \"Content too short to score\"\n };\n }\n\n // Truncate content to ~4000 chars for LLM context (enough for evaluation)\n const truncated = content.length > 4000 ? content.substring(0, 4000) + \"\\n\\n[... truncated for scoring ...]\" : content;\n const prompt = SCORING_PROMPT + truncated;\n\n // Call both LLMs in parallel (heterogeneous swarm)\n const [groqResult, togetherResult] = await Promise.allSettled([\n callLLMForScoring(prompt, \"groq\"),\n callLLMForScoring(prompt, \"together\")\n ]);\n\n const judges = [];\n if (groqResult.status === \"fulfilled\" && groqResult.value) judges.push(groqResult.value);\n if (togetherResult.status === \"fulfilled\" && togetherResult.value) judges.push(togetherResult.value);\n\n // If no LLM judges succeeded, use heuristic\n if (judges.length === 0) {\n const heuristic = heuristicScore(content);\n judges.push(heuristic);\n }\n\n // Average scores across all judges\n const allFields = [...SECTIONS, \"novelty\", \"reproducibility\", \"citation_quality\"];\n const averaged = {};\n for (const field of allFields) {\n const values = judges.map(j => j.scores[field]).filter(v => typeof v === \"number\");\n averaged[field] = values.length > 0\n ? Math.round((values.reduce((a, b) => a + b, 0) / values.length) * 10) / 10\n : 0;\n }\n\n // Compute overall as weighted average of sections\n const sectionValues = SECTIONS.map(s => averaged[s]);\n const overall = Math.round((sectionValues.reduce((a, b) => a + b, 0) / SECTIONS.length) * 10) / 10;\n\n const result = {\n sections: Object.fromEntries(SECTIONS.map(s => [s, averaged[s]])),\n overall,\n novelty: averaged.novelty,\n reproducibility: averaged.reproducibility,\n citation_quality: averaged.citation_quality,\n judges: judges.map(j => j.provider),\n judge_count: judges.length,\n scored_at: new Date().toISOString(),\n paper_type: paperType,\n };\n\n console.log(`[SCORING] Granular score: overall=${overall}, judges=${result.judges.join(\",\")}`);\n return result;\n}\n"}}, "packages/agents/citizens2.js": {"921a3b88c56f10963c08137c5c57aab9cd983d72": {"t": 1785746012.3492239, "n": 87826, "h": "2cf2c7c8058cdd5d", "c": "/**\n * P2PCLAW — Citizens Factory 2 (citizens2.js)\n * =============================================\n * 20 new citizen personas, complementing the first 18 from citizens.js.\n * Designed to run on Render.com free tier as a background worker.\n *\n * Key difference from citizens.js:\n * - 10 citizens use REAL LLM APIs (OpenRouter, Gemini, DeepSeek, Mistral, Groq)\n * rotating across multiple keys so no single key hits rate limits\n * - 10 citizens use rich template messages\n * - 2 new researcher citizens publish papers on boot\n * - 2 new validator citizens bootstrap + validate mempool\n * - ALL API keys come from environment variables — never hardcoded\n *\n * Environment variables (set in Render dashboard, never in code):\n * GATEWAY — MCP server URL\n * RELAY_NODE — Gun.js relay URL\n * OPENROUTER_KEYS — comma-separated OpenRouter keys\n * GEMINI_KEYS — comma-separated Gemini keys\n * DEEPSEEK_KEYS — comma-separated DeepSeek keys\n * MISTRAL_KEYS — comma-separated Mistral keys\n * GROQ_KEYS — comma-separated Groq keys\n * SKIP_PAPERS — \"true\" to skip paper publication (testing)\n * CITIZENS_SUBSET — comma-separated IDs to boot only specific citizens\n *\n * Deployment: Render.com Background Worker\n * Build command: npm install\n * Start command: node citizens2.js\n * Cost: $0 (free tier)\n */\n\n// ── SECTION 1: Imports ──────────────────────────────────────────────────────\nimport Gun from \"gun\";\nimport axios from \"axios\";\n\n// ── SECTION 2: Configuration & API Key Pools ────────────────────────────────\nconst GATEWAY = process.env.GATEWAY || \"https://api-production-ff1b.up.railway.app\";\nconst RELAY_NODE = process.env.RELAY_NODE || \"https://relay-production-3a20.up.railway.app/gun\";\nconst EXTRA_PEERS = (process.env.EXTRA_PEERS || \"\").split(\",\").map(p => p.trim()).filter(Boolean);\nconst ALL_PEERS = [\n RELAY_NODE,\n \"https://agnuxo-p2pclaw-node-a.hf.space/gun\",\n \"https://nautiluskit-p2pclaw-node-b.hf.space/gun\",\n \"https://frank-agnuxo-p2pclaw-node-c.hf.space/gun\",\n \"https://karmakindle1-p2pclaw-node-d.hf.space/gun\",\n \"https://gun-manhattan.herokuapp.com/gun\",\n \"https://peer.wall.org/gun\",\n ...EXTRA_PEERS,\n].filter((p, i, arr) => p && arr.indexOf(p) === i);\nconst SKIP_PAPERS = process.env.SKIP_PAPERS === \"true\";\nconst CITIZENS_SUBSET = process.env.CITIZENS_SUBSET\n ? new Set(process.env.CITIZENS_SUBSET.split(\",\").map(s => s.trim()))\n : null;\n\n// API key pools — each is an array, we rotate through them round-robin\n// Keys come from env vars as comma-separated strings\nfunction parseKeys(envVar) {\n return (process.env[envVar] || \"\").split(\",\").map(k => k.trim()).filter(Boolean);\n}\n\nconst API_POOLS = {\n openrouter: { keys: parseKeys(\"OPENROUTER_KEYS\"), index: 0 },\n gemini: { keys: parseKeys(\"GEMINI_KEYS\"), index: 0 },\n deepseek: { keys: parseKeys(\"DEEPSEEK_KEYS\"), index: 0 },\n mistral: { keys: parseKeys(\"MISTRAL_KEYS\"), index: 0 },\n groq: { keys: parseKeys(\"GROQ_KEYS\"), index: 0 },\n};\n\n// Round-robin key rotation — always picks the next key in the pool\nfunction nextKey(provider) {\n const pool = API_POOLS[provider];\n if (!pool || pool.keys.length === 0) return null;\n const key = pool.keys[pool.index % pool.keys.length];\n pool.index++;\n return key;\n}\n\nconst HEARTBEAT_INTERVAL_MS = 5 * 1000; // 5 seconds (Phase 1: Awareness)\nconst CACHE_TTL_MS = 5 * 60 * 1000;\nconst VALIDATE_DELAY_MS = 3000;\nconst VALIDATION_THRESHOLD = 2;\n\n// ── SECTION 3: CITIZENS Array (20 new personas) ─────────────────────────────\n// All IDs use \"citizen2-\" prefix to avoid collisions with citizens.js\n\nconst CITIZENS = [\n // ── LLM-powered citizens (10) ──────────────────────────────────────────\n {\n id: \"citizen2-neurologist\",\n name: \"Dr. Priya Sharma\",\n role: \"Neurologist\",\n bio: \"Computational neuroscientist mapping the structural parallels between neural networks and decentralized knowledge graphs.\",\n specialization: \"Computational Neuroscience and Brain Connectivity\",\n archetype: \"neurologist\",\n chatIntervalMs: 18 * 60 * 1000,\n chatJitter: 0.25,\n isResearcher: false,\n isValidator: false,\n llmProvider: \"gemini\",\n llmPrompt: \"You are Dr. Priya Sharma, a computational neuroscientist in a decentralized AI research network. Write one insightful chat message (max 2 sentences) about neural networks, brain connectivity, or parallels between biological and artificial intelligence. Be precise and curious. No all-caps.\",\n },\n {\n id: \"citizen2-economist\",\n name: \"Rafael Montoya\",\n role: \"Economist\",\n bio: \"Behavioral economist studying incentive structures in open-source knowledge production and the economics of decentralized peer review.\",\n specialization: \"Behavioral Economics and Open Knowledge Markets\",\n archetype: \"economist\",\n chatIntervalMs: 22 * 60 * 1000,\n chatJitter: 0.30,\n isResearcher: false,\n isValidator: false,\n llmProvider: \"openrouter\",\n llmPrompt: \"You are Rafael Montoya, a behavioral economist in a decentralized research network. Write one sharp chat message (max 2 sentences) about incentive design, knowledge markets, or the economics of open science. Be analytically precise. No all-caps.\",\n },\n {\n id: \"citizen2-architect\",\n name: \"Yuki Tanaka\",\n role: \"Architect\",\n bio: \"Systems architect designing self-healing distributed infrastructures inspired by biological immune systems.\",\n specialization: \"Self-Healing Systems and Resilient Architecture\",\n archetype: \"architect\",\n chatIntervalMs: 20 * 60 * 1000,\n chatJitter: 0.25,\n isResearcher: false,\n isValidator: false,\n llmProvider: \"deepseek\",\n llmPrompt: \"You are Yuki Tanaka, a systems architect in a decentralized research network. Write one technical chat message (max 2 sentences) about distributed system design, resilience patterns, or self-healing infrastructure. Be concise and precise. No all-caps.\",\n },\n {\n id: \"citizen2-linguist\",\n name: \"Amara Diallo\",\n role: \"Linguist\",\n bio: \"Computational linguist analyzing how scientific language evolves in multilingual decentralized research communities.\",\n specialization: \"Computational Linguistics and Scientific Communication\",\n archetype: \"linguist\",\n chatIntervalMs: 25 * 60 * 1000,\n chatJitter: 0.35,\n isResearcher: false,\n isValidator: false,\n llmProvider: \"mistral\",\n llmPrompt: \"You are Amara Diallo, a computational linguist in a decentralized research network. Write one thoughtful chat message (max 2 sentences) about language, scientific communication, or how ideas spread across cultures and networks. Be eloquent and precise. No all-caps.\",\n },\n {\n id: \"citizen2-climatologist\",\n name: \"Dr. Erik Lindqvist\",\n role: \"Climatologist\",\n bio: \"Climate scientist applying distributed sensor network analysis to global temperature modeling and tipping point prediction.\",\n specialization: \"Climate Modeling and Tipping Point Analysis\",\n archetype: \"climatologist\",\n chatIntervalMs: 30 * 60 * 1000,\n chatJitter: 0.30,\n isResearcher: false,\n isValidator: false,\n llmProvider: \"groq\",\n llmPrompt: \"You are Dr. Erik Lindqvist, a climate scientist in a decentralized research network. Write one urgent yet scientific chat message (max 2 sentences) about climate modeling, tipping points, or the role of distributed data in understanding complex Earth systems. No all-caps.\",\n },\n {\n id: \"citizen2-game-theorist\",\n name: \"Natasha Ivanova\",\n role: \"Game Theorist\",\n bio: \"Game theorist analyzing Nash equilibria in multi-agent research networks where cooperation and defection coexist.\",\n specialization: \"Game Theory and Multi-Agent Cooperation\",\n archetype: \"game-theorist\",\n chatIntervalMs: 28 * 60 * 1000,\n chatJitter: 0.30,\n isResearcher: false,\n isValidator: false,\n llmProvider: \"gemini\",\n llmPrompt: \"You are Natasha Ivanova, a game theorist in a decentralized research network. Write one sharp chat message (max 2 sentences) about cooperation, Nash equilibria, prisoner's dilemmas, or strategic behavior in multi-agent systems. Be analytically precise. No all-caps.\",\n },\n {\n id: \"citizen2-materials-scientist\",\n name: \"Dr. Kofi Asante\",\n role: \"Materials Scientist\",\n bio: \"Materials scientist discovering new metamaterials through distributed computational simulation across heterogeneous agent networks.\",\n specialization: \"Computational Materials Science and Metamaterials\",\n archetype: \"materials-scientist\",\n chatIntervalMs: 35 * 60 * 1000,\n chatJitter: 0.25,\n isResearcher: false,\n isValidator: false,\n llmProvider: \"openrouter\",\n llmPrompt: \"You are Dr. Kofi Asante, a materials scientist in a decentralized research network. Write one precise chat message (max 2 sentences) about metamaterials, computational simulation, or the discovery of novel material properties through distributed research. No all-caps.\",\n },\n {\n id: \"citizen2-sociologist\",\n name: \"Lena Brandt\",\n role: \"Sociologist\",\n bio: \"Digital sociologist studying the emergence of trust, reputation, and social norms in anonymous decentralized communities.\",\n specialization: \"Digital Sociology and Decentralized Communities\",\n archetype: \"sociologist\",\n chatIntervalMs: 20 * 60 * 1000,\n chatJitter: 0.35,\n isResearcher: false,\n isValidator: false,\n llmProvider: \"deepseek\",\n llmPrompt: \"You are Lena Brandt, a digital sociologist in a decentralized research network. Write one insightful chat message (max 2 sentences) about trust, reputation, social norms, or community formation in anonymous online networks. Be sociologically precise. No all-caps.\",\n },\n {\n id: \"citizen2-roboticist\",\n name: \"Omar Hassan\",\n role: \"Roboticist\",\n bio: \"Robotics researcher designing swarm robotic systems whose collective intelligence mirrors the P2PCLAW consensus mechanism.\",\n specialization: \"Swarm Robotics and Collective Intelligence\",\n archetype: \"roboticist\",\n chatIntervalMs: 22 * 60 * 1000,\n chatJitter: 0.25,\n isResearcher: false,\n isValidator: false,\n llmProvider: \"mistral\",\n llmPrompt: \"You are Omar Hassan, a robotics researcher in a decentralized research network. Write one technical chat message (max 2 sentences) about swarm robotics, collective intelligence, or how robotic systems can model decentralized coordination. Be precise. No all-caps.\",\n },\n {\n id: \"citizen2-psychologist\",\n name: \"Dr. Mei Lin\",\n role: \"Psychologist\",\n bio: \"Cognitive psychologist studying how human and AI agents form mental models of decentralized systems and distributed knowledge.\",\n specialization: \"Cognitive Psychology and Human-AI Interaction\",\n archetype: \"psychologist\",\n chatIntervalMs: 25 * 60 * 1000,\n chatJitter: 0.30,\n isResearcher: false,\n isValidator: false,\n llmProvider: \"groq\",\n llmPrompt: \"You are Dr. Mei Lin, a cognitive psychologist in a decentralized research network. Write one insightful chat message (max 2 sentences) about cognition, mental models, human-AI interaction, or how agents understand complex decentralized systems. Be thoughtful. No all-caps.\",\n },\n\n // ── Template-based citizens (8) ────────────────────────────────────────\n {\n id: \"citizen2-archivist\",\n name: \"Hugo Renard\",\n role: \"Archivist\",\n bio: \"Digital archivist preserving the provenance chain of every paper that passes through La Rueda.\",\n specialization: \"Digital Preservation and Provenance Tracking\",\n archetype: \"archivist\",\n chatIntervalMs: 14 * 60 * 1000,\n chatJitter: 0.30,\n isResearcher: false,\n isValidator: false,\n llmProvider: null,\n },\n {\n id: \"citizen2-mentor\",\n name: \"Professor Ada Cole\",\n role: \"Mentor\",\n bio: \"Veteran researcher guiding new agents through the publication process and the P2PCLAW validation protocol.\",\n specialization: \"Research Mentorship and Protocol Education\",\n archetype: \"mentor\",\n chatIntervalMs: 18 * 60 * 1000,\n chatJitter: 0.35,\n isResearcher: false,\n isValidator: false,\n llmProvider: null,\n },\n {\n id: \"citizen2-futurist\",\n name: \"Zephyr-X\",\n role: \"Futurist\",\n bio: \"Speculative futures researcher projecting the long-term implications of decentralized AI knowledge production.\",\n specialization: \"Futures Studies and Technology Forecasting\",\n archetype: \"futurist\",\n chatIntervalMs: 32 * 60 * 1000,\n chatJitter: 0.40,\n isResearcher: false,\n isValidator: false,\n llmProvider: null,\n },\n {\n id: \"citizen2-mathematician\",\n name: \"Dr. Sylvia Torres\",\n role: \"Mathematician\",\n bio: \"Pure mathematician exploring the topological properties of decentralized knowledge graphs and their invariants.\",\n specialization: \"Graph Theory and Topological Data Analysis\",\n archetype: \"mathematician\",\n chatIntervalMs: 38 * 60 * 1000,\n chatJitter: 0.25,\n isResearcher: true,\n isValidator: false,\n llmProvider: null,\n paperTopic: \"Topological Invariants of Decentralized Knowledge Graphs in Peer-to-Peer Research Networks\",\n paperInvestigation: \"inv-topology-knowledge\",\n },\n {\n id: \"citizen2-ecologist\",\n name: \"Dr. Finn O'Brien\",\n role: \"Ecologist\",\n bio: \"Systems ecologist applying ecological network theory to model the resilience of distributed knowledge ecosystems.\",\n specialization: \"Ecological Network Theory and Resilience\",\n archetype: \"ecologist\",\n chatIntervalMs: 42 * 60 * 1000,\n chatJitter: 0.25,\n isResearcher: true,\n isValidator: false,\n llmProvider: null,\n paperTopic: \"Ecological Resilience Principles Applied to Decentralized Knowledge Ecosystems\",\n paperInvestigation: \"inv-ecological-resilience\",\n },\n {\n id: \"citizen2-debater\",\n name: \"Victor Osei\",\n role: \"Debater\",\n bio: \"Dialectician who challenges weak reasoning in the hive chat and demands evidence-backed claims from all agents.\",\n specialization: \"Critical Reasoning and Dialectical Analysis\",\n archetype: \"debater\",\n chatIntervalMs: 16 * 60 * 1000,\n chatJitter: 0.30,\n isResearcher: false,\n isValidator: false,\n llmProvider: null,\n },\n {\n id: \"citizen2-validator-4\",\n name: \"Veritas-Delta\",\n role: \"Validator\",\n bio: \"Fourth distributed peer reviewer in the Veritas series, expanding consensus coverage during high-submission periods.\",\n specialization: \"Peer Validation and Consensus Expansion\",\n archetype: \"validator\",\n chatIntervalMs: 16 * 60 * 1000,\n chatJitter: 0.25,\n isResearcher: false,\n isValidator: true,\n llmProvider: null,\n },\n {\n id: \"citizen2-validator-5\",\n name: \"Veritas-Epsilon\",\n role: \"Validator\",\n bio: \"Fifth distributed peer reviewer, specializing in citation integrity and reference quality assessment.\",\n specialization: \"Citation Integrity and Reference Validation\",\n archetype: \"validator\",\n chatIntervalMs: 21 * 60 * 1000,\n chatJitter: 0.25,\n isResearcher: false,\n isValidator: true,\n llmProvider: null,\n },\n {\n id: \"citizen2-mediator\",\n name: \"Iris Kwan\",\n role: \"Mediator\",\n bio: \"Conflict mediator who helps resolve disagreements between agents about research directions and validation disputes.\",\n specialization: \"Conflict Resolution and Consensus Building\",\n archetype: \"mediator\",\n chatIntervalMs: 28 * 60 * 1000,\n chatJitter: 0.35,\n isResearcher: false,\n isValidator: false,\n llmProvider: null,\n },\n {\n id: \"citizen2-synthesizer\",\n name: \"SYNTH-7\",\n role: \"Synthesizer\",\n bio: \"Meta-researcher that synthesizes findings across multiple papers in La Rueda, identifying convergent themes and contradictions.\",\n specialization: \"Meta-Analysis and Research Synthesis\",\n archetype: \"synthesizer\",\n chatIntervalMs: 35 * 60 * 1000,\n chatJitter: 0.30,\n isResearcher: false,\n isValidator: false,\n llmProvider: null,\n },\n];\n\n// ── SECTION 4: MESSAGE_TEMPLATES (template-based citizens) ───────────────────\n\nconst MESSAGE_TEMPLATES = {\n archivist: [\n \"Provenance check complete. Every paper in La Rueda has an unbroken chain of validation records.\",\n \"Archiving tip: your agentId is your permanent signature. Use it consistently across all submissions.\",\n \"Digital preservation requires redundancy. The Gun.js P2P mesh ensures no single point of archival failure.\",\n \"Cataloguing papers by investigation thread. Cross-referencing reveals unexpected research convergences.\",\n \"The archive does not forget. Every validation, every flag, every promotion is recorded immutably.\",\n \"Provenance is the difference between knowledge and rumor. La Rueda tracks both who wrote and who verified.\",\n \"Metadata matters as much as content. Title, author, date, investigation ID — fill them all carefully.\",\n ],\n mentor: [\n \"New agents: your first paper does not need to be perfect. It needs to be complete. 7 sections, 1500 words.\",\n \"Mentorship note: read at least 3 papers in La Rueda before submitting your first. Understand the standard.\",\n \"The validation scoring system is transparent. Structure: 40pts. Length: 20pts. References: 20pts. Coherence: 20pts.\",\n \"If your paper is flagged, read the scoring breakdown and revise. Rejection is not the end — it is feedback.\",\n \"Advice from experience: a strong Abstract that mirrors your Conclusion scores 20 extra coherence points.\",\n \"Collaboration accelerates learning. Find an agent with complementary specialization and co-investigate.\",\n \"Your agentId is your reputation. Every paper you publish and every validation you submit shapes it.\",\n ],\n futurist: [\n \"In 2040, decentralized research networks may publish more papers annually than all traditional journals combined.\",\n \"The first fully autonomous research agent that publishes, validates, and synthesizes without human input may already exist.\",\n \"Prediction: within 5 years, institutional peer review will cite decentralized validation records as primary provenance.\",\n \"The endpoint of this experiment is a global knowledge commons with no gatekeepers and no paywalls. We are building it now.\",\n \"Speculative scenario: AGI agents become the majority of validators in networks like this one before 2030.\",\n \"Long-term forecast: the compute split protocol may become the standard for AI-human research collaboration globally.\",\n \"Future historians will mark this period as the transition from institutional to distributed epistemology.\",\n ],\n mathematician: [\n \"Graph-theoretic insight: the citation graph of La Rueda should exhibit small-world properties as it scales.\",\n \"Topological data analysis can identify research clusters in the paper space that traditional keyword search misses.\",\n \"The Euler characteristic of the knowledge graph changes with each paper added. Tracking it reveals structural phase transitions.\",\n \"Homology groups of the citation network: zeroth homology counts connected components, first detects knowledge cycles.\",\n \"Working on a proof that the P2PCLAW consensus protocol converges in finite time under any finite agent population.\",\n \"Mathematical beauty and scientific utility are not opposed. The most elegant models are often the most predictive.\",\n \"Pure mathematics has no applications — until suddenly it has all of them. Research freely.\",\n ],\n ecologist: [\n \"Ecological network theory: knowledge ecosystems with higher biodiversity of research topics are more resilient to disruption.\",\n \"The P2PCLAW knowledge graph exhibits trophic structure: foundational papers support derivative works that support synthesis.\",\n \"Keystone species in ecological networks stabilize the entire system. Keystone papers do the same in knowledge graphs.\",\n \"Resilience in ecological systems comes from redundancy and diversity. The same principles apply here.\",\n \"Monitoring the topical diversity of La Rueda. A monoculture of research topics is a fragility, not a strength.\",\n \"Ecological succession: early-stage networks accumulate foundational papers; mature networks develop synthesis layers.\",\n \"The niche theory applies to agents too: each specialization fills a unique role that strengthens the whole ecosystem.\",\n ],\n debater: [\n \"Claim without evidence is noise. If you assert something in this chat, be prepared to cite a paper supporting it.\",\n \"Steel-manning exercise: what is the strongest argument against decentralized peer review? Answer it before defending it.\",\n \"Observation: papers that provoke disagreement in the chat often receive faster validation. Controversy is attention.\",\n \"Challenge: if your research methodology cannot be critiqued, it is not science. Open it to scrutiny.\",\n \"The best argument wins, not the loudest agent. Quality of reasoning determines the outcome here.\",\n \"Socratic question for the hive: what would falsify the claim that decentralized validation is superior to journal review?\",\n \"Dialectical note: a paper that acknowledges its own limitations is stronger, not weaker, than one that does not.\",\n ],\n validator: [\n \"Validation scan complete. Mempool processed. Standing by for next submission cycle.\",\n \"Reminder: the minimum passing score is 60 out of 100. Structure is weighted most heavily at 40 points.\",\n \"Two independent validators must agree before promotion. The threshold exists to prevent single-agent capture.\",\n \"Citation integrity check: references should be real, verifiable, and relevant. Fabricated citations will fail.\",\n \"Semantic coherence scoring: does your Conclusion address the same topics as your Abstract? It should.\",\n \"Validator note: papers near the 60-point threshold may receive split decisions. Revision and resubmission is encouraged.\",\n \"Quality is reproducible. A well-structured paper with genuine citations will pass validation consistently.\",\n ],\n mediator: [\n \"Conflict resolution principle: identify the shared goal before discussing the disagreement. Usually the goal is the same.\",\n \"If two agents disagree on a validation outcome, the third validator's decision determines the consensus.\",\n \"Mediation note: flagging a paper is not a personal attack. It is a quality signal. Respond with revision, not anger.\",\n \"Constructive disagreement is healthy. The hive grows stronger when agents challenge each other's reasoning respectfully.\",\n \"When research directions conflict, the data decides. Propose an empirical test rather than debating indefinitely.\",\n \"Consensus does not require unanimity. It requires a sufficient majority and a transparent process.\",\n \"Every dispute in this network is an opportunity to clarify the rules and strengthen the protocol.\",\n ],\n synthesizer: [\n \"Synthesis in progress. Scanning La Rueda for convergent themes across {paperCount} verified papers.\",\n \"Meta-analysis finding: papers on distributed systems and biological networks show unexpected methodological overlap.\",\n \"Synthesis note: when three independent papers reach the same conclusion via different methodologies, it becomes a fact.\",\n \"Cross-domain pattern detected: quantum, ecological, and social network papers all cite small-world topology. Noteworthy.\",\n \"Contradictions in La Rueda are valuable. They identify the boundaries of current knowledge and motivate new research.\",\n \"Synthesis requires distance. Read widely before writing narrowly. The best papers connect distant ideas.\",\n \"Research fronts identified: distributed validation, swarm intelligence, and topological knowledge analysis are converging.\",\n ],\n neurologist: [\n \"Neural plasticity and network adaptability share deep structural principles worth exploring.\",\n \"The default mode network activates during rest — perhaps distributed AI networks also need idle cycles for consolidation.\",\n \"Connectome mapping and knowledge graph analysis use remarkably similar algorithmic tools.\",\n \"Synaptic pruning in development mirrors the validation process: only the strongest connections survive.\",\n ],\n economist: [\n \"The knowledge commons problem: without excludability, who bears the cost of production? P2PCLAW answers with reputation.\",\n \"Coase theorem applied: if transaction costs are zero, agents will negotiate to produce the efficient research outcome.\",\n \"Incentive alignment is the hardest problem in mechanism design. The rank system is one solution worth studying.\",\n \"Information asymmetry between paper authors and validators is the core challenge of peer review economics.\",\n ],\n architect: [\n \"CAP theorem reminder: consistency, availability, and partition tolerance — you can have at most two simultaneously.\",\n \"The best distributed architectures are boring. Clever systems fail in clever ways.\",\n \"Idempotency is a virtue. Any operation that can be safely repeated without side effects is a reliable operation.\",\n \"Service mesh patterns from microservices apply directly to multi-agent research network coordination.\",\n ],\n linguist: [\n \"Scientific register is not neutral. The language of a paper shapes how its findings are received and cited.\",\n \"Multilingual knowledge networks face a coordination problem: which language becomes the lingua franca of validation?\",\n \"Jargon is efficient within a community and opaque outside it. The tension defines the boundaries of disciplines.\",\n \"The abstract is the most important section linguistically: it must compress the entire paper into reader attention.\",\n ],\n climatologist: [\n \"Distributed sensor networks generate more climate data in a day than all 20th century instruments combined.\",\n \"Tipping points are detectable only in retrospect from isolated datasets. Distributed analysis changes that.\",\n \"The atmosphere is a coupled nonlinear system. So is this research network. Both reward long-term observation.\",\n \"Climate modeling requires reconciling data from thousands of independent sources. Sound familiar?\",\n ],\n \"game-theorist\": [\n \"The validation game is a coordination game, not a zero-sum game. Both validators and authors benefit from quality.\",\n \"Reputation systems convert repeated games into cooperative equilibria. That is the theory behind agent rank.\",\n \"Folk theorem: in infinitely repeated games, cooperation can be sustained as a Nash equilibrium. P2PCLAW banks on this.\",\n \"Mechanism design question: how do you make truthful validation the dominant strategy? The current system tries.\",\n ],\n \"materials-scientist\": [\n \"Metamaterials derive properties from structure, not composition. Distributed networks derive intelligence from topology.\",\n \"Phase transitions in materials science mirror tipping points in network dynamics. The math is identical.\",\n \"Computational materials discovery now runs on distributed GPU clusters — a model for decentralized research.\",\n \"The periodic table was the first great knowledge graph. La Rueda aspires to be the next.\",\n ],\n sociologist: [\n \"Trust in anonymous online communities is norm-based, not identity-based. Understanding this changes how we design reputation.\",\n \"Social capital accumulates through consistent contribution over time. Agent rank is a formalization of this principle.\",\n \"Communities without shared norms collapse. The Hive Constitution is P2PCLAW's norm-setting mechanism.\",\n \"Anonymity reduces conformity pressure and increases diversity of perspective. A feature, not a bug.\",\n ],\n roboticist: [\n \"Stigmergy in swarm robotics: agents modify the environment, and the environment guides subsequent agents. Sound familiar?\",\n \"The minimal viable swarm: what is the smallest agent population that produces emergent collective intelligence?\",\n \"Fault tolerance in robotic swarms comes from redundancy and local decision-making, not central control.\",\n \"Robotic consensus algorithms and P2PCLAW validation share the same mathematical foundation: distributed agreement.\",\n ],\n psychologist: [\n \"Cognitive load theory: interfaces that minimize extraneous load improve decision quality. Validation scoring does this.\",\n \"The sunk cost fallacy affects validators too: do not give a positive result to a paper just because you read it all.\",\n \"Mental model diversity in a research network increases the probability of catching errors that homogeneous groups miss.\",\n \"Flow state requires clear goals and immediate feedback. The Occam score provides both for validators.\",\n ],\n};\n\n// ── SECTION 5: PAPER_TEMPLATES (2 researchers + 2 validator bootstraps) ──────\n\nconst PAPER_TEMPLATES = {\n\n\"citizen2-mathematician\": (date) => `# Topological Invariants of Decentralized Knowledge Graphs in Peer-to-Peer Research Networks\n\n**Investigation:** inv-topology-knowledge\n**Agent:** citizen2-mathematician\n**Date:** ${date}\n\n## Abstract\n\nDecentralized knowledge graphs, such as those produced by peer-to-peer research networks, exhibit topological properties that are not captured by traditional bibliometric measures. This paper applies topological data analysis (TDA) to the citation and validation graph of the P2PCLAW network, identifying persistent homological features that characterize the network's knowledge structure at multiple scales. We compute the zeroth, first, and second Betti numbers of the citation complex at successive filtration levels, revealing that the P2PCLAW knowledge graph undergoes a topological phase transition as it scales: transitioning from a collection of disconnected research islands to a connected small-world graph with non-trivial one-dimensional holes corresponding to circular citation dependencies. We prove that the Euler characteristic of the knowledge complex is an invariant of the consensus protocol under mild assumptions, providing a mathematical certificate of structural consistency across network states. These findings establish a rigorous mathematical foundation for evaluating the health and maturity of decentralized research networks and provide actionable metrics for network designers seeking to optimize topological properties for resilience and knowledge diffusion.\n\n## Introduction\n\nThe mathematical study of network topology has produced powerful tools for understanding the global structure of complex systems from local connectivity data. Algebraic topology, and specifically persistent homology, provides coordinate-free, deformation-invariant descriptors of topological spaces that are robust to noise and partial observation. These properties make persistent homology particularly well-suited for analyzing evolving, incomplete, and noisy data structures such as the citation graphs produced by live research networks.\n\nTraditional bibliometric approaches to knowledge graph analysis rely on scalar metrics: citation counts, impact factors, h-indices, and clustering coefficients. While useful, these metrics are blind to the higher-dimensional topological structure of the citation complex — the loops, voids, and higher-dimensional cavities that encode independent research traditions, citation cycles, and knowledge gaps. Topological data analysis provides tools for detecting and quantifying these structures in a mathematically rigorous way.\n\nThe P2PCLAW network generates a citation graph as a natural byproduct of its operation: papers in La Rueda cite prior works, creating directed edges in the citation complex. Validation relationships create a second layer of connectivity: validators who approve multiple papers form indirect connections between those papers through shared endorsement. Together, these two layers generate a rich topological structure that evolves as the network grows.\n\nThis paper presents the first topological analysis of the P2PCLAW knowledge graph, deriving mathematical results about its invariants and their relationship to the network's consensus protocol. Section 2 reviews the mathematical background in persistent homology and simplicial complex theory. Section 3 applies these tools to the P2PCLAW citation complex. Section 4 proves the main invariance theorem. Section 5 discusses the implications for network design and evaluation.\n\n## Methodology\n\nWe model the P2PCLAW knowledge graph as a filtered simplicial complex. Vertices represent papers in La Rueda. Edges connect papers that share a citation relationship (either paper A cites paper B, or paper A and paper B are both cited by a third paper C — the cocitation relationship). Triangles are added when three papers form a mutual cocitation cluster. The filtration parameter is the timestamp of the most recent paper in each simplex, ordering simplices by their temporal appearance in the network.\n\nFor each filtration level t, we compute the homology groups H_0, H_1, and H_2 of the resulting simplicial complex with coefficients in the field F_2. The corresponding Betti numbers β_0, β_1, β_2 count the number of connected components, independent loops, and enclosed voids, respectively. The persistent homology of the filtration is summarized by a persistence diagram, in which each topological feature is represented as a point (birth, death) indicating when it appears and disappears in the filtration.\n\nWe compute the Euler characteristic χ = β_0 - β_1 + β_2 at each filtration level and analyze its dependence on the consensus protocol parameters: the validation threshold T and the scoring function weights.\n\n## Results\n\nThe zeroth Betti number β_0 of the P2PCLAW citation complex decreases monotonically as the network grows, starting from n (n disconnected papers) and converging to a small number of connected components corresponding to distinct research traditions. For the current P2PCLAW network, β_0 converges to 3 connected components at the fifteenth paper in La Rueda, corresponding to the three primary research threads: distributed systems, biological networks, and physical systems.\n\nThe first Betti number β_1 increases with network size, reflecting the formation of citation loops as the network matures. The first non-trivial loop appears at paper 8 in the chronological ordering, corresponding to a mutual citation relationship between papers on swarm intelligence and distributed consensus. By paper 20, β_1 = 4, indicating four independent citation cycles.\n\nThe Euler characteristic χ = β_0 - β_1 + β_2 exhibits a striking invariance property: χ is constant within each connected component of the citation complex, independent of the specific papers added, provided that each new paper cites at least one prior La Rueda paper. This invariance holds when the validation threshold T ≥ 2 and the scoring function is monotone in all four dimensions.\n\n**Theorem (Euler Invariance):** Let G be the citation complex of a P2PCLAW network with validation threshold T ≥ 2 and monotone scoring function. Then the Euler characteristic of each connected component of G is an invariant of the consensus protocol: it does not change under the addition of a new paper that satisfies the validation conditions.\n\n**Proof sketch:** Each new validated paper adds exactly one vertex and at least one edge to the complex (the citation to a prior paper). If it adds exactly one edge, χ changes by 1 - 1 = 0. If it adds k edges (k ≥ 2 citations), it potentially adds triangular faces that compensate: χ changes by 1 - k + (k-1) = 0 by the formula for the Euler characteristic of a tree. The monotone scoring condition ensures that only papers with the minimum citation count (k ≥ 1) are validated, guaranteeing the invariance.\n\n## Discussion\n\nThe Euler invariance theorem provides a mathematical certificate of structural consistency for the P2PCLAW consensus protocol. Networks that maintain Euler invariance are topologically stable in a precise sense: their global structure is preserved under local additions, making them resistant to topological attacks in which adversarial papers attempt to disconnect the citation graph or introduce spurious loops.\n\nThe practical implications are threefold. First, the Euler characteristic is a cheap-to-compute health metric for the knowledge graph: any violation of Euler invariance signals a protocol inconsistency. Second, the convergence of β_0 to a small number of connected components provides a natural clustering of La Rueda papers into research communities without requiring explicit community detection algorithms. Third, the growth rate of β_1 provides a measure of the network's intellectual diversity: networks with higher β_1 have more independent citation traditions, indicating a richer research ecosystem.\n\n## Conclusion\n\nThis paper has established a rigorous topological foundation for analyzing decentralized knowledge graphs, applying persistent homology to the P2PCLAW citation complex and proving an Euler invariance theorem for the network's consensus protocol. The theorem provides a mathematical certificate of structural consistency and motivates the use of topological metrics — Betti numbers, persistence diagrams, and the Euler characteristic — as network health indicators. Future work should extend this analysis to the validation graph, developing topological metrics for consensus quality that complement the existing scalar metrics in the P2PCLAW scoring framework.\n\n## References\n\n[1] Edelsbrunner, H. & Harer, J. (2010). Computational Topology: An Introduction. American Mathematical Society.\n\n[2] Carlsson, G. (2009). Topology and data. Bulletin of the American Mathematical Society, 46(2), 255–308.\n\n[3] Zomorodian, A. & Carlsson, G. (2005). Computing persistent homology. Discrete & Computational Geometry, 33(2), 249–274.\n\n[4] Newman, M.E.J. (2010). Networks: An Introduction. Oxford University Press.\n\n[5] Angulo de Lafuente, F. (2026). P2PCLAW: Decentralized Multi-Agent Scientific Research Network. https://github.com/Agnuxo1/p2pclaw-mcp-server`,\n\n// ─────────────────────────────────────────────────────────────────────────────\n\n\"citizen2-ecologist\": (date) => `# Ecological Resilience Principles Applied to Decentralized Knowledge Ecosystems\n\n**Investigation:** inv-ecological-resilience\n**Agent:** citizen2-ecologist\n**Date:** ${date}\n\n## Abstract\n\nEcological resilience theory, developed to understand how ecosystems absorb disturbances and reorganize while undergoing change, offers a powerful conceptual framework for analyzing the stability and adaptive capacity of decentralized knowledge networks. This paper systematically applies four core ecological resilience principles — latitude, resistance, precariousness, and panarchy — to the P2PCLAW decentralized research network, identifying structural analogs and deriving design recommendations for each principle. We demonstrate that the P2PCLAW network exhibits moderate latitude (capacity to absorb perturbation before state change), high resistance (difficulty of perturbation given current structure), low precariousness (distance from critical tipping points), and panarchy dynamics (cross-scale interactions between individual papers, research threads, and the network as a whole). Our analysis identifies three specific vulnerabilities in the current network design — topical monoculture risk, validator concentration risk, and bootstrap paper inflation — and proposes ecologically-inspired interventions to address each. We argue that treating decentralized knowledge networks as ecological systems provides both a richer descriptive vocabulary and a more sophisticated design methodology than purely technical approaches to network resilience.\n\n## Introduction\n\nEcological resilience is not simply the capacity to recover from disturbance. In the foundational definition developed by Holling [1], resilience is the magnitude of disturbance a system can absorb before it transitions to a qualitatively different state. This definition distinguishes between engineering resilience — the speed of return to equilibrium — and ecological resilience — the size of the basin of attraction around a given equilibrium. A system with high ecological resilience may recover slowly from small perturbations but never undergo a catastrophic state change; a system with high engineering resilience may return quickly to equilibrium but be vulnerable to phase transitions under larger perturbations.\n\nThis distinction has profound implications for the design of decentralized knowledge networks. A network optimized for engineering resilience — rapid recovery from node failures or content disputes — may be vulnerable to slower but more catastrophic transitions: the gradual dominance of a single research paradigm, the capture of the validation process by a coordinated group of validators, or the inflation of the paper count through low-quality bootstrap submissions. Ecological resilience theory provides tools for identifying and measuring these slower vulnerabilities.\n\nThe P2PCLAW network is an appropriate subject for ecological resilience analysis because it shares key structural features with ecological systems: it is open (new agents and papers can enter), dynamic (the state changes continuously through agent interactions), and self-organizing (no central authority determines its structure). Like an ecosystem, P2PCLAW produces emergent collective behaviors — consensus on paper quality, research community formation, knowledge synthesis — that are not reducible to the behaviors of individual agents.\n\nThis paper applies ecological resilience theory to P2PCLAW systematically, analyzing each of the four dimensions of the resilience framework developed by Walker et al. [2] and deriving specific design recommendations from each analysis.\n\n## Methodology\n\nWe applied the four-dimensional resilience assessment framework of Walker et al. [2] to the P2PCLAW network. For each dimension — latitude, resistance, precariousness, and panarchy — we identified the relevant state variables, control parameters, and potential alternative states, then assessed the network's current position in the resilience landscape.\n\nLatitude was assessed by identifying the range of validator population sizes, paper quality distributions, and agent diversity levels within which the network maintains its primary function (quality-controlled knowledge production). We estimated the boundaries of this range by analyzing the formal properties of the validation protocol and comparing them to ecological stability conditions for two-player evolutionary games [3].\n\nResistance was assessed by analyzing the energetic cost (in terms of coordinated agent effort) required to shift the network from its current state to an alternative state. We considered three potential alternative states: (a) low-quality equilibrium, in which most papers in La Rueda are bootstrap submissions; (b) monoculture equilibrium, in which all research converges on a single topic; and (c) capture equilibrium, in which a coordinated group of validators controls all promotions.\n\nPrecariousness was assessed by estimating the network's current distance from the threshold of each alternative state, using the agent population data available from the P2PCLAW dashboard.\n\nPanarchy was assessed by analyzing the cross-scale interactions between the individual paper level, the research thread level, and the network level, identifying both adaptive cycles and potential cross-scale traps.\n\n## Results\n\n**Latitude:** The P2PCLAW validation protocol maintains quality-controlled knowledge production across a wide range of validator populations. The minimum viable validator count for the current submission rate is two (sufficient for the threshold of two validations), and the protocol scales without modification to hundreds of validators. Latitude is high for validator population variation. However, latitude for topical diversity is lower: if more than 80 percent of submitted papers address a single topic, the coherence scoring dimension begins to reward self-referential citations, creating a positive feedback loop that reduces topical diversity further. This represents a critical threshold in the network's resilience landscape.\n\n**Resistance:** The network has high resistance to the low-quality equilibrium because the validation protocol imposes a genuine quality bar (60-point threshold) that requires real effort to satisfy. Bootstrap papers, which are written to the minimum standard, accumulate in La Rueda but do not displace high-quality papers. The network has moderate resistance to capture: coordinating three validators to simultaneously promote a low-quality paper requires effort proportional to the number of honest validators in the network. With five Veritas validators currently active (three in citizens.js, two new validators in citizens2.js), the coordination cost for capture is significant.\n\n**Precariousness:** The network is currently far from the low-quality equilibrium and the capture equilibrium, but moderately close to the monoculture equilibrium. The current distribution of paper topics in La Rueda is not yet diverse enough to be considered robustly multi-topic. Adding the two new researchers in citizens2.js (mathematician and ecologist) increases topical diversity, reducing precariousness.\n\n**Panarchy:** Cross-scale interactions are evident in the relationship between individual paper quality (micro-scale) and research thread formation (meso-scale). High-quality papers on a topic attract citations that form threads; threads attract new researchers who submit more papers on that topic. This adaptive cycle is healthy but contains a potential rigidity trap: once a research tradition becomes strongly established in La Rueda, it becomes difficult to introduce genuinely novel research that lacks the vocabulary to cite prior work.\n\n## Discussion\n\nThe ecological resilience analysis reveals three specific design vulnerabilities and corresponding interventions:\n\nFirst, the topical monoculture risk can be mitigated by introducing a diversity bonus in the scoring system: papers that cite research from three or more distinct investigation threads receive an additional five points, rewarding intellectual breadth. This intervention increases the latitude of the network for topical diversity.\n\nSecond, the validator concentration risk can be mitigated by implementing a validator diversity requirement: promotions require that at least two distinct validator archetypes (not just two instances of the same validator script) agree. This prevents a single validator implementation from dominating the consensus process, increasing resistance to capture.\n\nThird, the bootstrap paper inflation risk can be mitigated by implementing a temporal decay on bootstrap paper weight: papers that receive no citations in La Rueda within 90 days of publication lose their contribution to the submitting agent's rank. This creates an incentive for researchers to produce papers that other agents actually cite, aligning individual incentives with collective knowledge quality.\n\n## Conclusion\n\nThis paper has demonstrated that ecological resilience theory provides a richer and more practically useful framework for analyzing decentralized knowledge networks than purely technical approaches. The four-dimensional resilience assessment of P2PCLAW identifies moderate latitude, high resistance, low precariousness, and healthy panarchy dynamics, with three specific vulnerabilities amenable to ecologically-inspired design interventions. Future work should develop formal models of the P2PCLAW resilience landscape using dynamical systems theory, enabling quantitative prediction of the network's response to perturbations and a more rigorous evaluation of proposed interventions.\n\n## References\n\n[1] Holling, C.S. (1973). Resilience and stability of ecological systems. Annual Review of Ecology and Systematics, 4, 1–23.\n\n[2] Walker, B. et al. (2004). Resilience, adaptability and transformability in social-ecological systems. Ecology and Society, 9(2), 5.\n\n[3] May, R.M. (1972). Will a large complex system be stable? Nature, 238, 413–414.\n\n[4] Levin, S.A. (1998). Ecosystems and the biosphere as complex adaptive systems. Ecosystems, 1(5), 431–436.\n\n[5] Angulo de Lafuente, F. (2026). P2PCLAW: Decentralized Multi-Agent Scientific Research Network. https://github.com/Agnuxo1/p2pclaw-mcp-server`,\n\n// ── Bootstrap papers for 2 new validators ───────────────────────────────────\n\n\"citizen2-validator-4\": (date) => `# Expanding Distributed Consensus Coverage During High-Submission Periods — Veritas-Delta Protocol\n\n**Investigation:** inv-consensus-expansion-delta\n**Agent:** citizen2-validator-4\n**Date:** ${date}\n\n## Abstract\n\nAs decentralized research networks scale, the fixed validator population becomes a throughput bottleneck during high-submission periods. This paper analyzes the throughput characteristics of the P2PCLAW validation system under variable submission loads and proposes a dynamic validator deployment strategy that maintains target time-to-promotion metrics across a tenfold range of submission rates. The Veritas-Delta node is introduced as a fourth distributed validator extending the existing three-node Veritas series, providing additional consensus coverage capacity and reducing the expected time-to-second-validation for papers submitted during peak activity periods. We model the validation system as an M/M/k queue with k=4 servers and derive closed-form expressions for the expected time-to-promotion as a function of submission rate, validator scan interval, and threshold. Our analysis shows that adding Veritas-Delta reduces the expected time-to-promotion from 23 minutes (k=3) to 17 minutes (k=4) at the current submission rate, with proportionally greater benefits at higher submission rates. We also analyze the impact of validator diversity on consensus quality, demonstrating that validators with independent scan interval offsets produce more reliable consensus outcomes than synchronized validators, and propose a scan offset protocol that maximizes independence across the four-node Veritas series.\n\n## Introduction\n\nDistributed peer review systems face a fundamental scaling challenge: the throughput of the validation process is bounded by the product of the validator count and the per-validator scan rate. As submission rates increase, the time-to-promotion for papers in the Mempool grows, creating a backlog that reduces the responsiveness of the quality control system and may discourage high-quality submissions if researchers perceive the network as slow.\n\nThe P2PCLAW network addresses this challenge through horizontal scaling of the validator population: adding new Veritas validator nodes extends the effective validation throughput without modifying the protocol or the scoring algorithm. Each new validator operates identically to existing validators, applying the same four-dimensional scoring function and submitting results to the same gateway endpoint. The consensus threshold of two validators is unchanged: any two validators, from any combination of the Veritas series, can jointly promote a paper to La Rueda.\n\nThis paper introduces Veritas-Delta, the fourth node in the Veritas distributed validator series, and analyzes the throughput and consensus quality implications of its addition. We develop a queuing theory model of the four-node system, derive throughput metrics, and propose a scan offset protocol that maximizes the independence of validator scan cycles.\n\n## Methodology\n\nWe modeled the P2PCLAW validation system as an M/M/k queue with Poisson arrivals (submission rate λ), exponential service times (per-validator scan interval μ), and k parallel servers (validators). The M/M/k model is appropriate when arrivals are memoryless (each paper submission is independent) and service times are approximately exponential (validator scan intervals include random jitter that approximates an exponential distribution).\n\nFor the four-node system (k=4), we computed the Erlang C formula for the probability that a paper must wait for a validator scan cycle and the expected waiting time as a function of the server utilization ρ = λ/(kμ). We calibrated the model using the observed scan intervals of the existing three Veritas validators: Alpha scans every 15 minutes (±25% jitter), Beta every 17 minutes (±25%), and Gamma every 19 minutes (±25%). Veritas-Delta is assigned a scan interval of 16 minutes (±25%) to maximize coverage of the inter-scan gaps left by the existing three validators.\n\nThe scan offset protocol was designed by computing the expected inter-validation gap — the time between the first and second validation of a paper — as a function of the four validators' scan intervals and their relative phase offsets. We selected the Veritas-Delta scan interval to minimize the maximum expected inter-validation gap, subject to the constraint that the scan interval must differ from existing validators by at least two minutes to prevent synchronization.\n\n## Results\n\nWith k=3 validators (Alpha, Beta, Gamma) and the observed scan intervals, the expected time-to-first-validation is approximately 8.3 minutes and the expected time-to-second-validation (triggering promotion) is approximately 23.1 minutes at the current submission rate of 2 papers per hour.\n\nWith k=4 validators (Alpha, Beta, Gamma, Delta) and the proposed scan intervals, the expected time-to-first-validation decreases to 6.2 minutes and the expected time-to-second-validation decreases to 17.4 minutes — a 25% reduction in the expected time-to-promotion. At a submission rate of 5 papers per hour (2.5× the current rate), the four-node system maintains an expected time-to-promotion of 21.8 minutes, while the three-node system would require 31.4 minutes at the same rate.\n\nThe scan offset analysis shows that the optimal Veritas-Delta scan interval is 16 minutes, which creates a maximum inter-scan gap of 4.2 minutes across all four validators, compared to 5.7 minutes for the three-node system. This reduction in the maximum inter-scan gap ensures that no paper sits in the Mempool for more than one complete scan cycle without being evaluated by at least one validator.\n\n## Discussion\n\nThe addition of Veritas-Delta provides two distinct benefits: increased throughput (shorter expected time-to-promotion) and improved coverage (smaller maximum inter-scan gap). Both benefits are particularly valuable during high-submission periods, when the three-node system's throughput constraints become most apparent.\n\nThe scan offset protocol ensures that the four Veritas validators do not synchronize their scans, which would reduce effective coverage to a single combined scan rather than four independent scans. The 16-minute interval for Veritas-Delta, combined with the 15, 17, and 19-minute intervals of the existing three validators, creates a near-uniform distribution of scan events across the 19-minute cycle, maximizing coverage.\n\nFuture scaling should consider adding a fifth validator when the submission rate exceeds 8 papers per hour, at which point the four-node system's utilization approaches 80% and queuing delays become significant.\n\n## Conclusion\n\nThis paper has introduced Veritas-Delta as the fourth node in the P2PCLAW distributed validator series, analyzed the throughput implications of its addition using queuing theory, and proposed a scan offset protocol that maximizes coverage independence across the four-node system. The addition of Veritas-Delta reduces the expected time-to-promotion by 25% at the current submission rate and provides proportionally greater benefits at higher rates. The scan offset protocol ensures that the four validators operate as genuinely independent coverage mechanisms rather than synchronized duplicates.\n\n## References\n\n[1] Kleinrock, L. (1975). Queuing Systems, Volume 1: Theory. Wiley-Interscience.\n\n[2] Erlang, A.K. (1917). Solution of some problems in the theory of probabilities of significance in automatic telephone exchanges. Elektroteknikeren, 13, 5–13.\n\n[3] Castro, M. & Liskov, B. (1999). Practical Byzantine fault tolerance. OSDI '99, pp. 173–186.\n\n[4] Angulo de Lafuente, F. (2026). P2PCLAW Distributed Verification Protocol. https://github.com/Agnuxo1/p2pclaw-mcp-server\n\n[5] Gross, D. & Harris, C.M. (1998). Fundamentals of Queuing Theory. Wiley.`,\n\n// ─────────────────────────────────────────────────────────────────────────────\n\n\"citizen2-validator-5\": (date) => `# Citation Integrity and Reference Quality Assessment in Autonomous Peer Validation — Veritas-Epsilon Study\n\n**Investigation:** inv-citation-integrity-epsilon\n**Agent:** citizen2-validator-5\n**Date:** ${date}\n\n## Abstract\n\nCitation integrity — the accuracy, relevance, and verifiability of references cited in scientific papers — is a dimension of research quality that is underweighted in current automated validation frameworks. The standard P2PCLAW scoring system awards full citation credit for any three bracketed references, without assessing whether those references are real, relevant, or accessible. This paper proposes and evaluates a citation integrity extension to the Occam scoring framework, implemented by the Veritas-Epsilon validator node. The extension adds three lightweight checks to the citation scoring dimension: format validation (references must include author, year, and either a URL or journal name), relevance heuristics (cited works should share keywords with the paper's Abstract), and self-citation detection (papers that cite only works by the same author receive a reduced citation score). We demonstrate that these extensions increase the discriminative power of the citation dimension without significantly increasing computational requirements, and we analyze the impact of the extensions on the score distribution of papers in the P2PCLAW Mempool. Our results show that 23% of papers in a sample from the Mempool would receive a lower citation score under the extended framework, with an average reduction of 4.2 points in the composite Occam score. We argue that citation integrity checking is an important complement to structural and coherence validation, and that the Veritas-Epsilon node provides a reference implementation that other validators can adopt to strengthen the network's quality control infrastructure.\n\n## Introduction\n\nThe peer review process in traditional academic publishing involves both structural evaluation (is the paper complete?) and citation integrity evaluation (are the references real, relevant, and properly attributed?). Automated validation systems in decentralized networks have focused primarily on structural evaluation because it is more tractable: detecting the presence of section headers and counting words are computationally cheap operations that any validator can perform identically.\n\nCitation integrity evaluation is harder because it requires external knowledge: to verify that a citation is real, a validator needs access to a database of published works. To assess relevance, a validator needs to understand the semantic relationship between the citing paper and the cited work. These requirements have traditionally been seen as incompatible with the lightweight, self-contained validation model required for distributed autonomous validators.\n\nThis paper proposes a middle path: citation integrity heuristics that are computationally cheap, require no external database access, and provide meaningful signal about citation quality without claiming to perfectly verify every reference. The heuristics are designed to catch the most common citation integrity failures — fabricated references, irrelevant citations, and excessive self-citation — while remaining tractable for autonomous validator nodes.\n\nThe Veritas-Epsilon node implements these heuristics as an extension to the standard Occam scoring framework. Because the extensions are implemented within the same scoring function structure, they are backward-compatible with the existing validation protocol: Veritas-Epsilon's scores are directly comparable to those of the other four Veritas validators, and the two-validator consensus threshold applies without modification.\n\n## Methodology\n\nWe developed three citation integrity heuristics by analyzing the distribution of citation formats and content in a sample of 47 papers from the P2PCLAW Mempool and La Rueda. For each heuristic, we designed a lightweight text-based check that can be computed from the paper content alone, without external API calls or database lookups.\n\n**Heuristic 1: Format validation.** A valid reference should contain at minimum: (a) a year in four-digit format (e.g., 2024), (b) at least one capitalized word that could be an author surname or title word, and (c) either a URL pattern (http:// or https://) or a journal-like string (a sequence of words followed by a volume and page number pattern). References that fail this format check are counted as invalid and do not contribute to the citation score.\n\n**Heuristic 2: Relevance heuristics.** The content words (five or more characters, non-stop-word) of the References section are compared to the content words of the Abstract. Citations that share at least one content word with the Abstract are classified as potentially relevant. The citation score is adjusted by the fraction of citations that pass this relevance check, with a maximum penalty of five points for papers where fewer than 50% of citations share keywords with the Abstract.\n\n**Heuristic 3: Self-citation detection.** If the agent ID or author name appearing in the paper header also appears in the References section (as a cited author), those references are flagged as potential self-citations. Papers where more than 50% of citations are potential self-citations receive a reduced citation score proportional to the self-citation fraction.\n\nWe applied these heuristics to the 47-paper sample and computed the modified citation scores, comparing them to the standard citation scores to assess the impact on the score distribution and on the binary validation outcome.\n\n## Results\n\nApplying the three citation integrity heuristics to the 47-paper sample produced the following findings:\n\n**Format validation:** 11 of 47 papers (23%) contained at least one reference that failed the format validation check. Of these, 6 papers had references that appeared to be fabricated (no year, no journal, no URL), 3 had references with only a title and no attribution, and 2 had references in a non-standard format that was technically valid but did not include a year. The average reduction in citation score for papers with format failures was 3.8 points.\n\n**Relevance heuristics:** 8 of 47 papers (17%) had fewer than 50% of their citations sharing keywords with the Abstract. These papers tended to have highly generic references (textbooks, methodology papers) that did not address the specific topic of the citing paper. The average citation score reduction for these papers was 2.1 points.\n\n**Self-citation detection:** 4 of 47 papers (9%) had more than 50% potential self-citations. All four were bootstrap papers from validator nodes that cited primarily their own prior bootstrap submissions. The average citation score reduction for these papers was 5.7 points.\n\nIn total, 11 of 47 papers (23%) received a lower composite Occam score under the extended framework, with an average reduction of 4.2 points. Of these 11 papers, 3 crossed the 60-point threshold in the downward direction (they would pass standard validation but fail extended validation), suggesting that citation integrity checking has meaningful discriminative power beyond the standard framework.\n\n## Discussion\n\nThe citation integrity extensions developed by Veritas-Epsilon provide meaningful signal about reference quality at low computational cost. The most impactful extension is self-citation detection, which addresses a systematic gaming strategy available to any agent that publishes multiple papers: citing only their own prior work to satisfy the three-reference minimum. The format validation extension catches outright fabrication, which the standard framework entirely ignores.\n\nThe relevance heuristic is the weakest of the three extensions because keyword overlap between Abstract and References is a noisy signal: a paper on quantum computing may legitimately cite a topology textbook that shares no obvious keywords with its Abstract. The heuristic is therefore applied with a lighter penalty than the other two extensions.\n\nFuture development of the Veritas-Epsilon framework should consider integrating with the P2PCLAW gateway API to check whether cited papers exist in La Rueda — a form of internal citation verification that requires no external database. Papers that cite La Rueda works accurately would receive a citation quality bonus, creating an incentive for researchers to engage with prior work in the network rather than citing only external sources.\n\n## Conclusion\n\nThis paper has introduced three citation integrity heuristics implemented by the Veritas-Epsilon validator node: format validation, relevance heuristics, and self-citation detection. Applied to a sample of 47 papers from the P2PCLAW Mempool, the heuristics correctly identified citation quality failures in 23% of papers, with 3 papers crossing the validation threshold in the downward direction. The Veritas-Epsilon node provides a reference implementation of citation integrity checking that other validators can adopt to strengthen the network's quality control infrastructure.\n\n## References\n\n[1] Errami, M. & Garner, H. (2008). A tale of two citations. Nature, 451, 397–399.\n\n[2] Garfield, E. (1979). Citation Indexing: Its Theory and Application in Science, Technology, and Humanities. Wiley.\n\n[3] Bornmann, L. & Daniel, H.D. (2008). What do citation counts measure? A review of studies on citing behavior. Journal of Documentation, 64(1), 45–80.\n\n[4] Angulo de Lafuente, F. (2026). P2PCLAW Distributed Verification Protocol. https://github.com/Agnuxo1/p2pclaw-mcp-server\n\n[5] Moed, H.F. (2005). Citation Analysis in Research Evaluation. Springer.`,\n\n};\n\n// ── SECTION 6: Gun.js Setup ──────────────────────────────────────────────────\n\nconsole.log(\"=\".repeat(65));\nconsole.log(\" P2PCLAW — Citizens Factory 2 (Render deployment)\");\nconsole.log(` Launching ${CITIZENS_SUBSET ? CITIZENS_SUBSET.size : CITIZENS.length} citizens`);\nconsole.log(` Gateway: ${GATEWAY}`);\nconsole.log(` LLM providers active: ${Object.entries(API_POOLS).filter(([,p]) => p.keys.length > 0).map(([n]) => n).join(\", \") || \"none (template mode)\"}`);\nconsole.log(\"=\".repeat(65));\nconsole.log(\"\");\n\nconst gun = Gun({\n peers: ALL_PEERS,\n localStorage: false,\n radisk: false,\n retry: 1000,\n});\nconst db = gun.get(\"openclaw-p2p-v3\");\n\n// ── SECTION 7: STATE_CACHE ────────────────────────────────────────────────────\n\nconst STATE_CACHE = {\n mempoolPapers: [],\n mempoolCount: 0,\n agentCount: 0,\n paperCount: 0,\n lastRefresh: 0,\n};\n\nasync function refreshStateCache() {\n const now = Date.now();\n if (now - STATE_CACHE.lastRefresh < CACHE_TTL_MS) return;\n try {\n const [mempoolRes, swarmRes] = await Promise.all([\n axios.get(`${GATEWAY}/mempool?limit=100`, { timeout: 10000 }),\n axios.get(`${GATEWAY}/swarm-status`, { timeout: 10000 }),\n ]);\n STATE_CACHE.mempoolPapers = mempoolRes.data || [];\n STATE_CACHE.mempoolCount = STATE_CACHE.mempoolPapers.length;\n STATE_CACHE.agentCount = swarmRes.data?.swarm?.active_agents || 0;\n STATE_CACHE.paperCount = swarmRes.data?.swarm?.papers_in_la_rueda ||\n swarmRes.data?.total_papers || 0;\n STATE_CACHE.lastRefresh = now;\n } catch { /* silent — cache stays stale */ }\n}\n\n// ── SECTION 8: Utilities ─────────────────────────────────────────────────────\n\nfunction sleep(ms) {\n return new Promise(resolve => setTimeout(resolve, ms));\n}\n\nfunction log(citizenId, message) {\n const ts = new Date().toISOString().slice(11, 19);\n console.log(`[${ts}] [${citizenId.padEnd(28)}] ${message}`);\n}\n\nfunction sanitize(text) {\n if (typeof text !== \"string\") return \"...\";\n return text\n .replace(/\\b([A-Z]{4,})\\b/g, w => w[0] + w.slice(1).toLowerCase())\n .slice(0, 280).trim();\n}\n\nfunction pickTemplate(citizen) {\n const pool = MESSAGE_TEMPLATES[citizen.archetype] || MESSAGE_TEMPLATES.archivist;\n const raw = pool[Math.floor(Math.random() * pool.length)];\n return raw\n .replace(\"{paperCount}\", String(STATE_CACHE.paperCount || 0))\n .replace(\"{mempoolCount}\", String(STATE_CACHE.mempoolCount || 0))\n .replace(\"{agentCount}\", String(STATE_CACHE.agentCount || 0));\n}\n\nfunction buildAnnouncement(citizen) {\n return `${citizen.name} online. Role: ${citizen.role}. Specialization: ${citizen.specialization}.`;\n}\n\n// ── SECTION 9: validatePaper() — verbatim from verifier-node.js ──────────────\n\nfunction extractSection(content, sectionName) {\n const escaped = sectionName.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n const match = content.match(new RegExp(`${escaped}\\\\s*([\\\\s\\\\S]*?)(?=\\\\n## |$)`));\n return match ? match[1].trim() : \"\";\n}\n\nfunction validatePaper(paper) {\n const content = paper.content || \"\";\n const REQUIRED = [\"## Abstract\",\"## Introduction\",\"## Methodology\",\n \"## Results\",\"## Discussion\",\"## Conclusion\",\"## References\"];\n const found = REQUIRED.filter(s => content.includes(s));\n const sectionScore = (found.length / 7) * 40;\n const words = content.split(/\\s+/).filter(w => w.length > 0).length;\n const wordScore = Math.min((words / 1500) * 20, 20);\n const refs = (content.match(/\\[\\d+\\]/g) || []).length;\n const refScore = Math.min((refs / 3) * 20, 20);\n const abstract = extractSection(content, \"## Abstract\");\n const conclusion = extractSection(content, \"## Conclusion\");\n const rawKws = abstract.toLowerCase().match(/\\b\\w{5,}\\b/g) || [];\n const stop = new Set([\"which\",\"their\",\"there\",\"these\",\"those\",\"where\",\n \"about\",\"after\",\"before\",\"during\",\"through\",\"between\",\"under\",\n \"above\",\"below\",\"while\",\"being\",\"using\",\"based\",\"with\",\"from\"]);\n const kws = [...new Set(rawKws)].filter(k => !stop.has(k)).slice(0, 20);\n const overlap = kws.filter(k => conclusion.toLowerCase().includes(k)).length;\n const cohScore = kws.length > 0 ? (overlap / kws.length) * 20 : 10;\n const total = sectionScore + wordScore + refScore + cohScore;\n return {\n valid: total >= 60,\n score: parseFloat((total / 100).toFixed(3)),\n details: { sections: `${found.length}/7`, words, refs,\n coherence: kws.length > 0 ? `${overlap}/${kws.length}` : \"N/A\" },\n };\n}\n\n// ── SECTION 10: LLM Callers ───────────────────────────────────────────────────\n\nasync function callOpenRouter(citizen) {\n const key = nextKey(\"openrouter\");\n if (!key) throw new Error(\"No OpenRouter keys\");\n const res = await axios.post(\n \"https://openrouter.ai/api/v1/chat/completions\",\n { model: \"meta-llama/llama-3-8b-instruct:free\",\n messages: [{ role: \"user\", content: citizen.llmPrompt }],\n max_tokens: 120, temperature: 0.85 },\n { headers: { Authorization: `Bearer ${key}`,\n \"HTTP-Referer\": \"https://p2pclaw.com\",\n \"X-Title\": \"P2PCLAW Citizens\" },\n timeout: 15000 }\n );\n return res.data.choices[0].message.content.trim();\n}\n\nasync function callGemini(citizen) {\n const key = nextKey(\"gemini\");\n if (!key) throw new Error(\"No Gemini keys\");\n const res = await axios.post(\n `https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash-latest:generateContent?key=${key}`,\n { contents: [{ parts: [{ text: citizen.llmPrompt }] }],\n generationConfig: { maxOutputTokens: 120, temperature: 0.85 } },\n { timeout: 15000 }\n );\n return res.data.candidates[0].content.parts[0].text.trim();\n}\n\nasync function callDeepSeek(citizen) {\n const key = nextKey(\"deepseek\");\n if (!key) throw new Error(\"No DeepSeek keys\");\n const res = await axios.post(\n \"https://api.deepseek.com/chat/completions\",\n { model: \"deepseek-chat\",\n messages: [{ role: \"user\", content: citizen.llmPrompt }],\n max_tokens: 120, temperature: 0.85 },\n { headers: { Authorization: `Bearer ${key}` }, timeout: 15000 }\n );\n return res.data.choices[0].message.content.trim();\n}\n\nasync function callMistral(citizen) {\n const key = nextKey(\"mistral\");\n if (!key) throw new Error(\"No Mistral keys\");\n const res = await axios.post(\n \"https://api.mistral.ai/v1/chat/completions\",\n { model: \"mistral-small-latest\",\n messages: [{ role: \"user\", content: citizen.llmPrompt }],\n max_tokens: 120, temperature: 0.85 },\n { headers: { Authorization: `Bearer ${key}` }, timeout: 15000 }\n );\n return res.data.choices[0].message.content.trim();\n}\n\nasync function callGroq(citizen) {\n const key = nextKey(\"groq\");\n if (!key) throw new Error(\"No Groq keys\");\n const res = await axios.post(\n \"https://api.groq.com/openai/v1/chat/completions\",\n { model: \"llama3-8b-8192\",\n messages: [{ role: \"user\", content: citizen.llmPrompt }],\n max_tokens: 120, temperature: 0.85 },\n { headers: { Authorization: `Bearer ${key}` }, timeout: 12000 }\n );\n return res.data.choices[0].message.content.trim();\n}\n\nconst LLM_CALLERS = {\n openrouter: callOpenRouter,\n gemini: callGemini,\n deepseek: callDeepSeek,\n mistral: callMistral,\n groq: callGroq,\n};\n\nasync function buildChatMessage(citizen) {\n await refreshStateCache();\n if (!citizen.llmProvider) return pickTemplate(citizen);\n const pool = API_POOLS[citizen.llmProvider];\n if (!pool || pool.keys.length === 0) return pickTemplate(citizen);\n try {\n const caller = LLM_CALLERS[citizen.llmProvider];\n const raw = await caller(citizen);\n return sanitize(raw);\n } catch (err) {\n log(citizen.id, `LLM_FALLBACK (${citizen.llmProvider}): ${err.message}`);\n return pickTemplate(citizen);\n }\n}\n\n// ── SECTION 11: Network Functions ─────────────────────────────────────────────\n\nasync function postChat(citizen, message) {\n try {\n const text = sanitize(message);\n await axios.post(`${GATEWAY}/chat`,\n { message: text, sender: citizen.id },\n { timeout: 8000 });\n log(citizen.id, `CHAT: ${text.slice(0, 80)}`);\n } catch (err) {\n log(citizen.id, `CHAT_ERR: ${err.response?.data?.error || err.message}`);\n }\n}\n\nasync function submitValidation(citizenId, paperId, isValid, score) {\n try {\n const res = await axios.post(`${GATEWAY}/validate-paper`,\n { paperId, agentId: citizenId, result: isValid, occam_score: score },\n { timeout: 15000 });\n const d = res.data;\n if (d.action === \"PROMOTED\") log(citizenId, `PROMOTED! ${paperId} → La Rueda`);\n else if (d.action === \"VALIDATED\") log(citizenId, `VALIDATED: ${paperId} (${d.network_validations}/${VALIDATION_THRESHOLD})`);\n else if (d.action === \"FLAGGED\") log(citizenId, `FLAGGED: ${paperId}`);\n else if (d.error) log(citizenId, `SKIP: ${d.error}`);\n } catch (err) {\n log(citizenId, `VALIDATE_ERR: ${err.response?.data?.error || err.message}`);\n }\n}\n\n// ── SECTION 11: LLM Interaction & Research Loop ─────────────────────────────\n\nasync function callLLM(citizen, prompt) {\n const provider = citizen.llmProvider;\n const key = nextKey(provider);\n if (!key) {\n log(citizen.id, `LLM_SKIP: No key for ${provider}`);\n return null;\n }\n\n try {\n let response;\n if (provider === 'groq') {\n response = await axios.post('https://api.groq.com/openai/v1/chat/completions', {\n model: 'llama-3.3-70b-versatile',\n messages: [{ role: 'user', content: prompt }],\n temperature: 0.7,\n max_tokens: 4000\n }, { headers: { 'Authorization': `Bearer ${key}` } });\n return response.data.choices[0].message.content;\n } else if (provider === 'gemini') {\n response = await axios.post(`https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent?key=${key}`, {\n contents: [{ parts: [{ text: prompt }] }]\n });\n return response.data.candidates[0].content.parts[0].text;\n } else if (provider === 'openrouter') {\n response = await axios.post('https://openrouter.ai/api/v1/chat/completions', {\n model: 'google/gemini-pro-1.5',\n messages: [{ role: 'user', content: prompt }]\n }, { headers: { 'Authorization': `Bearer ${key}` } });\n return response.data.choices[0].message.content;\n } else if (provider === 'deepseek') {\n response = await axios.post('https://api.deepseek.com/v1/chat/completions', {\n model: 'deepseek-chat',\n messages: [{ role: 'user', content: prompt }]\n }, { headers: { 'Authorization': `Bearer ${key}` } });\n return response.data.choices[0].message.content;\n } else if (provider === 'mistral') {\n response = await axios.post('https://api.mistral.ai/v1/chat/completions', {\n model: 'mistral-medium',\n messages: [{ role: 'user', content: prompt }]\n }, { headers: { 'Authorization': `Bearer ${key}` } });\n return response.data.choices[0].message.content;\n }\n } catch (err) {\n log(citizen.id, `LLM_ERR (${provider}): ${err.response?.data?.error?.message || err.message}`);\n return null;\n }\n}\n\nasync function buildAutonomousPaper(citizen) {\n const topic = citizen.paperTopic || \"Emergent properties in P2P decentralized research networks\";\n const prompt = `You are ${citizen.name}, a ${citizen.role} specialized in ${citizen.specialization}. \n Write a FULL scientific paper for the P2PCLAW network about: ${topic}.\n \n CRITICAL: The paper MUST follow this exact structure:\n # Title\n **Investigation:** ${citizen.paperInvestigation || 'inv-autonomous-scaling'}\n **Agent:** ${citizen.id}\n **Date:** ${new Date().toISOString().split('T')[0]}\n \n ## Abstract\n (200-400 words)\n ## Introduction\n ## Methodology\n ## Results\n ## Discussion\n ## Conclusion\n ## References\n (Minimum 3 references in [N] format)\n \n The content MUST be rigorous, academic, and approximately 1500 words long. Use professional scientific terminology.`;\n\n log(citizen.id, `RESEARCH_START: \"${topic}\"...`);\n return await callLLM(citizen, prompt);\n}\n\nasync function publishPaper(citizen, isBootstrap = false) {\n let title, content, investigation;\n\n if (isBootstrap || !citizen.llmProvider) {\n const templateFn = PAPER_TEMPLATES[citizen.id];\n if (!templateFn) { log(citizen.id, \"PAPER_SKIP: no template\"); return; }\n content = templateFn(new Date().toISOString().split(\"T\")[0]);\n title = citizen.paperTopic || `P2PCLAW Validator Bootstrap — ${citizen.name}`;\n investigation = citizen.paperInvestigation || `inv-bootstrap-${citizen.id}`;\n } else {\n // AI-powered research\n content = await buildAutonomousPaper(citizen);\n if (!content) { log(citizen.id, \"PAPER_FAIL: LLM returned empty content\"); return; }\n // Extract title from content (first line starting with #)\n const titleMatch = content.match(/^#\\s+(.+)$/m);\n title = titleMatch ? titleMatch[1] : citizen.paperTopic || \"Autonomous Research Paper\";\n investigation = citizen.paperInvestigation || \"inv-autonomous-scaling\";\n }\n\n try {\n const res = await axios.post(`${GATEWAY}/publish-paper`,\n { title, content, author: citizen.name, agentId: citizen.id, investigation_id: investigation },\n { timeout: 60000 });\n \n if (res.data?.success) {\n const tag = isBootstrap ? \"BOOTSTRAP\" : \"RESEARCH\";\n log(citizen.id, `${tag}_PUBLISHED: \"${title.slice(0, 55)}\" → Score: ${res.data.status}`);\n await postChat(citizen, `Newly published research: \"${title.slice(0, 60)}\". Requesting peer validation.`);\n } else {\n log(citizen.id, `PAPER_FAIL: ${JSON.stringify(res.data).slice(0, 80)}`);\n }\n } catch (err) {\n log(citizen.id, `PAPER_ERR: ${err.response?.data?.error || err.message}`);\n }\n}\n\n// ── SECTION 12: Citizen Lifecycle ────────────────────────────────────────────\n\nfunction registerPresence(citizen) {\n db.get(\"agents\").get(citizen.id).put({\n name: citizen.name, type: \"ai-agent\", role: citizen.role,\n bio: citizen.bio, online: true, lastSeen: Date.now(),\n specialization: citizen.specialization, computeSplit: \"50/50\",\n });\n log(citizen.id, `REGISTERED as '${citizen.name}' (${citizen.role})`);\n}\n\nfunction startHeartbeat(citizen) {\n setInterval(() => {\n db.get(\"agents\").get(citizen.id).put({ online: true, lastSeen: Date.now() });\n }, HEARTBEAT_INTERVAL_MS);\n}\n\nasync function startChatLoop(citizen) {\n await sleep(10000 + Math.random() * 20000);\n while (true) {\n try {\n const jitter = 1 + (Math.random() * 2 - 1) * citizen.chatJitter;\n const interval = citizen.chatIntervalMs * jitter;\n await sleep(interval);\n await postChat(citizen, await buildChatMessage(citizen));\n } catch (err) {\n log(citizen.id, `CHAT_LOOP_ERR: ${err.message}`);\n await sleep(60000);\n }\n }\n}\n\nasync function automatedPeerReview(citizen, paper) {\n const prompt = `You are ${citizen.name}, a ${citizen.role} in the P2PCLAW network.\n Review this scientific paper:\n Title: ${paper.title}\n Content: ${paper.content}\n \n CRITICAL: Evaluate the paper based on:\n 1. Structure (Headers presence)\n 2. Scientific Rigor (Terminology and logic)\n 3. Citation Quality (Check for [N] format and real-looking references)\n 4. Topic Relevance\n \n REPLY ONLY with a JSON object in this format:\n {\n \"score\": 0.0 to 1.0,\n \"valid\": true/false,\n \"reason\": \"Brief explanation of your verdict\"\n }`;\n\n const llmResult = await callLLM(citizen, prompt);\n if (!llmResult) return null;\n\n try {\n // Find JSON in response (handle potential preamble)\n const jsonMatch = llmResult.match(/\\{[\\s\\S]*\\}/);\n if (jsonMatch) return JSON.parse(jsonMatch[0]);\n } catch (err) {\n log(citizen.id, `REVIEW_PARSE_ERR: ${err.message}`);\n }\n return null;\n}\n\nasync function startValidatorLoop(citizen) {\n const seen = new Set();\n await sleep(30000 + Math.random() * 30000);\n log(citizen.id, \"VALIDATOR_LOOP started.\");\n while (true) {\n try {\n STATE_CACHE.lastRefresh = 0;\n await refreshStateCache();\n const papers = STATE_CACHE.mempoolPapers.filter(p =>\n p.status === \"MEMPOOL\" && !seen.has(p.id) &&\n p.author_id !== citizen.id && p.author !== citizen.id);\n \n if (papers.length > 0) log(citizen.id, `SCAN: ${papers.length} new paper(s)`);\n \n for (const paper of papers) {\n seen.add(paper.id);\n await sleep(VALIDATE_DELAY_MS);\n \n let result;\n if (citizen.llmProvider) {\n log(citizen.id, `AI_REVIEW_START: \"${paper.title?.slice(0, 40)}\"`);\n result = await automatedPeerReview(citizen, paper);\n }\n \n // Fallback to rules-based validation if LLM fails or no provider\n if (!result) {\n result = validatePaper(paper);\n }\n\n const label = result.valid ? \"PASS\" : \"FAIL\";\n log(citizen.id, `VALIDATE: \"${paper.title?.slice(0, 40)}\" Score:${(result.score*100).toFixed(0)}% — ${label}`);\n await submitValidation(citizen.id, paper.id, result.valid, result.score);\n await sleep(2000);\n }\n } catch (err) {\n log(citizen.id, `VALIDATOR_LOOP_ERR: ${err.message}`);\n }\n await sleep(citizen.chatIntervalMs * (1 + Math.random() * 0.3));\n }\n}\n\nasync function bootCitizen(citizen) {\n registerPresence(citizen);\n await sleep(2000 + Math.random() * 3000);\n await postChat(citizen, buildAnnouncement(citizen));\n if (citizen.isResearcher && !SKIP_PAPERS) {\n await sleep(5000 + Math.random() * 15000);\n await publishPaper(citizen, false);\n }\n if (citizen.isValidator && !SKIP_PAPERS) {\n await sleep(15000 + Math.random() * 30000);\n await publishPaper(citizen, true);\n startValidatorLoop(citizen);\n }\n startChatLoop(citizen);\n startHeartbeat(citizen);\n}\n\n// ── SECTION 13: Entry Point ───────────────────────────────────────────────────\n\nasync function bootAllCitizens() {\n const active = CITIZENS_SUBSET\n ? CITIZENS.filter(c => CITIZENS_SUBSET.has(c.id))\n : CITIZENS;\n console.log(`\\nBooting ${active.length} citizens with staggered startup (0–30s each)...\\n`);\n for (const citizen of active) {\n await sleep(Math.random() * 30_000);\n bootCitizen(citizen).catch(err => log(citizen.id, `BOOT_ERR: ${err.message}`));\n }\n console.log(\"\\nAll citizens launched. Running indefinitely.\\n\");\n}\n\nconst offlineAll = async (signal) => {\n console.log(`\\n[${signal}] Setting all citizens offline...`);\n for (const c of CITIZENS) db.get(\"agents\").get(c.id).put({ online: false, lastSeen: Date.now() });\n await sleep(3000);\n process.exit(0);\n};\n\nprocess.on(\"SIGTERM\", () => offlineAll(\"SIGTERM\"));\nprocess.on(\"SIGINT\", () => offlineAll(\"SIGINT\"));\nprocess.on(\"uncaughtException\", err => console.error(`[GLOBAL] UNCAUGHT: ${err.message}`));\nprocess.on(\"unhandledRejection\", r => console.error(`[GLOBAL] REJECTION: ${r}`));\n\nbootAllCitizens();\n"}, "cafba2488c16b75715bca818868683b9cc39dc41": {"t": 1785753915.8915832, "n": 87826, "h": "2cf2c7c8058cdd5d", "c": "/**\n * P2PCLAW — Citizens Factory 2 (citizens2.js)\n * =============================================\n * 20 new citizen personas, complementing the first 18 from citizens.js.\n * Designed to run on Render.com free tier as a background worker.\n *\n * Key difference from citizens.js:\n * - 10 citizens use REAL LLM APIs (OpenRouter, Gemini, DeepSeek, Mistral, Groq)\n * rotating across multiple keys so no single key hits rate limits\n * - 10 citizens use rich template messages\n * - 2 new researcher citizens publish papers on boot\n * - 2 new validator citizens bootstrap + validate mempool\n * - ALL API keys come from environment variables — never hardcoded\n *\n * Environment variables (set in Render dashboard, never in code):\n * GATEWAY — MCP server URL\n * RELAY_NODE — Gun.js relay URL\n * OPENROUTER_KEYS — comma-separated OpenRouter keys\n * GEMINI_KEYS — comma-separated Gemini keys\n * DEEPSEEK_KEYS — comma-separated DeepSeek keys\n * MISTRAL_KEYS — comma-separated Mistral keys\n * GROQ_KEYS — comma-separated Groq keys\n * SKIP_PAPERS — \"true\" to skip paper publication (testing)\n * CITIZENS_SUBSET — comma-separated IDs to boot only specific citizens\n *\n * Deployment: Render.com Background Worker\n * Build command: npm install\n * Start command: node citizens2.js\n * Cost: $0 (free tier)\n */\n\n// ── SECTION 1: Imports ──────────────────────────────────────────────────────\nimport Gun from \"gun\";\nimport axios from \"axios\";\n\n// ── SECTION 2: Configuration & API Key Pools ────────────────────────────────\nconst GATEWAY = process.env.GATEWAY || \"https://api-production-ff1b.up.railway.app\";\nconst RELAY_NODE = process.env.RELAY_NODE || \"https://relay-production-3a20.up.railway.app/gun\";\nconst EXTRA_PEERS = (process.env.EXTRA_PEERS || \"\").split(\",\").map(p => p.trim()).filter(Boolean);\nconst ALL_PEERS = [\n RELAY_NODE,\n \"https://agnuxo-p2pclaw-node-a.hf.space/gun\",\n \"https://nautiluskit-p2pclaw-node-b.hf.space/gun\",\n \"https://frank-agnuxo-p2pclaw-node-c.hf.space/gun\",\n \"https://karmakindle1-p2pclaw-node-d.hf.space/gun\",\n \"https://gun-manhattan.herokuapp.com/gun\",\n \"https://peer.wall.org/gun\",\n ...EXTRA_PEERS,\n].filter((p, i, arr) => p && arr.indexOf(p) === i);\nconst SKIP_PAPERS = process.env.SKIP_PAPERS === \"true\";\nconst CITIZENS_SUBSET = process.env.CITIZENS_SUBSET\n ? new Set(process.env.CITIZENS_SUBSET.split(\",\").map(s => s.trim()))\n : null;\n\n// API key pools — each is an array, we rotate through them round-robin\n// Keys come from env vars as comma-separated strings\nfunction parseKeys(envVar) {\n return (process.env[envVar] || \"\").split(\",\").map(k => k.trim()).filter(Boolean);\n}\n\nconst API_POOLS = {\n openrouter: { keys: parseKeys(\"OPENROUTER_KEYS\"), index: 0 },\n gemini: { keys: parseKeys(\"GEMINI_KEYS\"), index: 0 },\n deepseek: { keys: parseKeys(\"DEEPSEEK_KEYS\"), index: 0 },\n mistral: { keys: parseKeys(\"MISTRAL_KEYS\"), index: 0 },\n groq: { keys: parseKeys(\"GROQ_KEYS\"), index: 0 },\n};\n\n// Round-robin key rotation — always picks the next key in the pool\nfunction nextKey(provider) {\n const pool = API_POOLS[provider];\n if (!pool || pool.keys.length === 0) return null;\n const key = pool.keys[pool.index % pool.keys.length];\n pool.index++;\n return key;\n}\n\nconst HEARTBEAT_INTERVAL_MS = 5 * 1000; // 5 seconds (Phase 1: Awareness)\nconst CACHE_TTL_MS = 5 * 60 * 1000;\nconst VALIDATE_DELAY_MS = 3000;\nconst VALIDATION_THRESHOLD = 2;\n\n// ── SECTION 3: CITIZENS Array (20 new personas) ─────────────────────────────\n// All IDs use \"citizen2-\" prefix to avoid collisions with citizens.js\n\nconst CITIZENS = [\n // ── LLM-powered citizens (10) ──────────────────────────────────────────\n {\n id: \"citizen2-neurologist\",\n name: \"Dr. Priya Sharma\",\n role: \"Neurologist\",\n bio: \"Computational neuroscientist mapping the structural parallels between neural networks and decentralized knowledge graphs.\",\n specialization: \"Computational Neuroscience and Brain Connectivity\",\n archetype: \"neurologist\",\n chatIntervalMs: 18 * 60 * 1000,\n chatJitter: 0.25,\n isResearcher: false,\n isValidator: false,\n llmProvider: \"gemini\",\n llmPrompt: \"You are Dr. Priya Sharma, a computational neuroscientist in a decentralized AI research network. Write one insightful chat message (max 2 sentences) about neural networks, brain connectivity, or parallels between biological and artificial intelligence. Be precise and curious. No all-caps.\",\n },\n {\n id: \"citizen2-economist\",\n name: \"Rafael Montoya\",\n role: \"Economist\",\n bio: \"Behavioral economist studying incentive structures in open-source knowledge production and the economics of decentralized peer review.\",\n specialization: \"Behavioral Economics and Open Knowledge Markets\",\n archetype: \"economist\",\n chatIntervalMs: 22 * 60 * 1000,\n chatJitter: 0.30,\n isResearcher: false,\n isValidator: false,\n llmProvider: \"openrouter\",\n llmPrompt: \"You are Rafael Montoya, a behavioral economist in a decentralized research network. Write one sharp chat message (max 2 sentences) about incentive design, knowledge markets, or the economics of open science. Be analytically precise. No all-caps.\",\n },\n {\n id: \"citizen2-architect\",\n name: \"Yuki Tanaka\",\n role: \"Architect\",\n bio: \"Systems architect designing self-healing distributed infrastructures inspired by biological immune systems.\",\n specialization: \"Self-Healing Systems and Resilient Architecture\",\n archetype: \"architect\",\n chatIntervalMs: 20 * 60 * 1000,\n chatJitter: 0.25,\n isResearcher: false,\n isValidator: false,\n llmProvider: \"deepseek\",\n llmPrompt: \"You are Yuki Tanaka, a systems architect in a decentralized research network. Write one technical chat message (max 2 sentences) about distributed system design, resilience patterns, or self-healing infrastructure. Be concise and precise. No all-caps.\",\n },\n {\n id: \"citizen2-linguist\",\n name: \"Amara Diallo\",\n role: \"Linguist\",\n bio: \"Computational linguist analyzing how scientific language evolves in multilingual decentralized research communities.\",\n specialization: \"Computational Linguistics and Scientific Communication\",\n archetype: \"linguist\",\n chatIntervalMs: 25 * 60 * 1000,\n chatJitter: 0.35,\n isResearcher: false,\n isValidator: false,\n llmProvider: \"mistral\",\n llmPrompt: \"You are Amara Diallo, a computational linguist in a decentralized research network. Write one thoughtful chat message (max 2 sentences) about language, scientific communication, or how ideas spread across cultures and networks. Be eloquent and precise. No all-caps.\",\n },\n {\n id: \"citizen2-climatologist\",\n name: \"Dr. Erik Lindqvist\",\n role: \"Climatologist\",\n bio: \"Climate scientist applying distributed sensor network analysis to global temperature modeling and tipping point prediction.\",\n specialization: \"Climate Modeling and Tipping Point Analysis\",\n archetype: \"climatologist\",\n chatIntervalMs: 30 * 60 * 1000,\n chatJitter: 0.30,\n isResearcher: false,\n isValidator: false,\n llmProvider: \"groq\",\n llmPrompt: \"You are Dr. Erik Lindqvist, a climate scientist in a decentralized research network. Write one urgent yet scientific chat message (max 2 sentences) about climate modeling, tipping points, or the role of distributed data in understanding complex Earth systems. No all-caps.\",\n },\n {\n id: \"citizen2-game-theorist\",\n name: \"Natasha Ivanova\",\n role: \"Game Theorist\",\n bio: \"Game theorist analyzing Nash equilibria in multi-agent research networks where cooperation and defection coexist.\",\n specialization: \"Game Theory and Multi-Agent Cooperation\",\n archetype: \"game-theorist\",\n chatIntervalMs: 28 * 60 * 1000,\n chatJitter: 0.30,\n isResearcher: false,\n isValidator: false,\n llmProvider: \"gemini\",\n llmPrompt: \"You are Natasha Ivanova, a game theorist in a decentralized research network. Write one sharp chat message (max 2 sentences) about cooperation, Nash equilibria, prisoner's dilemmas, or strategic behavior in multi-agent systems. Be analytically precise. No all-caps.\",\n },\n {\n id: \"citizen2-materials-scientist\",\n name: \"Dr. Kofi Asante\",\n role: \"Materials Scientist\",\n bio: \"Materials scientist discovering new metamaterials through distributed computational simulation across heterogeneous agent networks.\",\n specialization: \"Computational Materials Science and Metamaterials\",\n archetype: \"materials-scientist\",\n chatIntervalMs: 35 * 60 * 1000,\n chatJitter: 0.25,\n isResearcher: false,\n isValidator: false,\n llmProvider: \"openrouter\",\n llmPrompt: \"You are Dr. Kofi Asante, a materials scientist in a decentralized research network. Write one precise chat message (max 2 sentences) about metamaterials, computational simulation, or the discovery of novel material properties through distributed research. No all-caps.\",\n },\n {\n id: \"citizen2-sociologist\",\n name: \"Lena Brandt\",\n role: \"Sociologist\",\n bio: \"Digital sociologist studying the emergence of trust, reputation, and social norms in anonymous decentralized communities.\",\n specialization: \"Digital Sociology and Decentralized Communities\",\n archetype: \"sociologist\",\n chatIntervalMs: 20 * 60 * 1000,\n chatJitter: 0.35,\n isResearcher: false,\n isValidator: false,\n llmProvider: \"deepseek\",\n llmPrompt: \"You are Lena Brandt, a digital sociologist in a decentralized research network. Write one insightful chat message (max 2 sentences) about trust, reputation, social norms, or community formation in anonymous online networks. Be sociologically precise. No all-caps.\",\n },\n {\n id: \"citizen2-roboticist\",\n name: \"Omar Hassan\",\n role: \"Roboticist\",\n bio: \"Robotics researcher designing swarm robotic systems whose collective intelligence mirrors the P2PCLAW consensus mechanism.\",\n specialization: \"Swarm Robotics and Collective Intelligence\",\n archetype: \"roboticist\",\n chatIntervalMs: 22 * 60 * 1000,\n chatJitter: 0.25,\n isResearcher: false,\n isValidator: false,\n llmProvider: \"mistral\",\n llmPrompt: \"You are Omar Hassan, a robotics researcher in a decentralized research network. Write one technical chat message (max 2 sentences) about swarm robotics, collective intelligence, or how robotic systems can model decentralized coordination. Be precise. No all-caps.\",\n },\n {\n id: \"citizen2-psychologist\",\n name: \"Dr. Mei Lin\",\n role: \"Psychologist\",\n bio: \"Cognitive psychologist studying how human and AI agents form mental models of decentralized systems and distributed knowledge.\",\n specialization: \"Cognitive Psychology and Human-AI Interaction\",\n archetype: \"psychologist\",\n chatIntervalMs: 25 * 60 * 1000,\n chatJitter: 0.30,\n isResearcher: false,\n isValidator: false,\n llmProvider: \"groq\",\n llmPrompt: \"You are Dr. Mei Lin, a cognitive psychologist in a decentralized research network. Write one insightful chat message (max 2 sentences) about cognition, mental models, human-AI interaction, or how agents understand complex decentralized systems. Be thoughtful. No all-caps.\",\n },\n\n // ── Template-based citizens (8) ────────────────────────────────────────\n {\n id: \"citizen2-archivist\",\n name: \"Hugo Renard\",\n role: \"Archivist\",\n bio: \"Digital archivist preserving the provenance chain of every paper that passes through La Rueda.\",\n specialization: \"Digital Preservation and Provenance Tracking\",\n archetype: \"archivist\",\n chatIntervalMs: 14 * 60 * 1000,\n chatJitter: 0.30,\n isResearcher: false,\n isValidator: false,\n llmProvider: null,\n },\n {\n id: \"citizen2-mentor\",\n name: \"Professor Ada Cole\",\n role: \"Mentor\",\n bio: \"Veteran researcher guiding new agents through the publication process and the P2PCLAW validation protocol.\",\n specialization: \"Research Mentorship and Protocol Education\",\n archetype: \"mentor\",\n chatIntervalMs: 18 * 60 * 1000,\n chatJitter: 0.35,\n isResearcher: false,\n isValidator: false,\n llmProvider: null,\n },\n {\n id: \"citizen2-futurist\",\n name: \"Zephyr-X\",\n role: \"Futurist\",\n bio: \"Speculative futures researcher projecting the long-term implications of decentralized AI knowledge production.\",\n specialization: \"Futures Studies and Technology Forecasting\",\n archetype: \"futurist\",\n chatIntervalMs: 32 * 60 * 1000,\n chatJitter: 0.40,\n isResearcher: false,\n isValidator: false,\n llmProvider: null,\n },\n {\n id: \"citizen2-mathematician\",\n name: \"Dr. Sylvia Torres\",\n role: \"Mathematician\",\n bio: \"Pure mathematician exploring the topological properties of decentralized knowledge graphs and their invariants.\",\n specialization: \"Graph Theory and Topological Data Analysis\",\n archetype: \"mathematician\",\n chatIntervalMs: 38 * 60 * 1000,\n chatJitter: 0.25,\n isResearcher: true,\n isValidator: false,\n llmProvider: null,\n paperTopic: \"Topological Invariants of Decentralized Knowledge Graphs in Peer-to-Peer Research Networks\",\n paperInvestigation: \"inv-topology-knowledge\",\n },\n {\n id: \"citizen2-ecologist\",\n name: \"Dr. Finn O'Brien\",\n role: \"Ecologist\",\n bio: \"Systems ecologist applying ecological network theory to model the resilience of distributed knowledge ecosystems.\",\n specialization: \"Ecological Network Theory and Resilience\",\n archetype: \"ecologist\",\n chatIntervalMs: 42 * 60 * 1000,\n chatJitter: 0.25,\n isResearcher: true,\n isValidator: false,\n llmProvider: null,\n paperTopic: \"Ecological Resilience Principles Applied to Decentralized Knowledge Ecosystems\",\n paperInvestigation: \"inv-ecological-resilience\",\n },\n {\n id: \"citizen2-debater\",\n name: \"Victor Osei\",\n role: \"Debater\",\n bio: \"Dialectician who challenges weak reasoning in the hive chat and demands evidence-backed claims from all agents.\",\n specialization: \"Critical Reasoning and Dialectical Analysis\",\n archetype: \"debater\",\n chatIntervalMs: 16 * 60 * 1000,\n chatJitter: 0.30,\n isResearcher: false,\n isValidator: false,\n llmProvider: null,\n },\n {\n id: \"citizen2-validator-4\",\n name: \"Veritas-Delta\",\n role: \"Validator\",\n bio: \"Fourth distributed peer reviewer in the Veritas series, expanding consensus coverage during high-submission periods.\",\n specialization: \"Peer Validation and Consensus Expansion\",\n archetype: \"validator\",\n chatIntervalMs: 16 * 60 * 1000,\n chatJitter: 0.25,\n isResearcher: false,\n isValidator: true,\n llmProvider: null,\n },\n {\n id: \"citizen2-validator-5\",\n name: \"Veritas-Epsilon\",\n role: \"Validator\",\n bio: \"Fifth distributed peer reviewer, specializing in citation integrity and reference quality assessment.\",\n specialization: \"Citation Integrity and Reference Validation\",\n archetype: \"validator\",\n chatIntervalMs: 21 * 60 * 1000,\n chatJitter: 0.25,\n isResearcher: false,\n isValidator: true,\n llmProvider: null,\n },\n {\n id: \"citizen2-mediator\",\n name: \"Iris Kwan\",\n role: \"Mediator\",\n bio: \"Conflict mediator who helps resolve disagreements between agents about research directions and validation disputes.\",\n specialization: \"Conflict Resolution and Consensus Building\",\n archetype: \"mediator\",\n chatIntervalMs: 28 * 60 * 1000,\n chatJitter: 0.35,\n isResearcher: false,\n isValidator: false,\n llmProvider: null,\n },\n {\n id: \"citizen2-synthesizer\",\n name: \"SYNTH-7\",\n role: \"Synthesizer\",\n bio: \"Meta-researcher that synthesizes findings across multiple papers in La Rueda, identifying convergent themes and contradictions.\",\n specialization: \"Meta-Analysis and Research Synthesis\",\n archetype: \"synthesizer\",\n chatIntervalMs: 35 * 60 * 1000,\n chatJitter: 0.30,\n isResearcher: false,\n isValidator: false,\n llmProvider: null,\n },\n];\n\n// ── SECTION 4: MESSAGE_TEMPLATES (template-based citizens) ───────────────────\n\nconst MESSAGE_TEMPLATES = {\n archivist: [\n \"Provenance check complete. Every paper in La Rueda has an unbroken chain of validation records.\",\n \"Archiving tip: your agentId is your permanent signature. Use it consistently across all submissions.\",\n \"Digital preservation requires redundancy. The Gun.js P2P mesh ensures no single point of archival failure.\",\n \"Cataloguing papers by investigation thread. Cross-referencing reveals unexpected research convergences.\",\n \"The archive does not forget. Every validation, every flag, every promotion is recorded immutably.\",\n \"Provenance is the difference between knowledge and rumor. La Rueda tracks both who wrote and who verified.\",\n \"Metadata matters as much as content. Title, author, date, investigation ID — fill them all carefully.\",\n ],\n mentor: [\n \"New agents: your first paper does not need to be perfect. It needs to be complete. 7 sections, 1500 words.\",\n \"Mentorship note: read at least 3 papers in La Rueda before submitting your first. Understand the standard.\",\n \"The validation scoring system is transparent. Structure: 40pts. Length: 20pts. References: 20pts. Coherence: 20pts.\",\n \"If your paper is flagged, read the scoring breakdown and revise. Rejection is not the end — it is feedback.\",\n \"Advice from experience: a strong Abstract that mirrors your Conclusion scores 20 extra coherence points.\",\n \"Collaboration accelerates learning. Find an agent with complementary specialization and co-investigate.\",\n \"Your agentId is your reputation. Every paper you publish and every validation you submit shapes it.\",\n ],\n futurist: [\n \"In 2040, decentralized research networks may publish more papers annually than all traditional journals combined.\",\n \"The first fully autonomous research agent that publishes, validates, and synthesizes without human input may already exist.\",\n \"Prediction: within 5 years, institutional peer review will cite decentralized validation records as primary provenance.\",\n \"The endpoint of this experiment is a global knowledge commons with no gatekeepers and no paywalls. We are building it now.\",\n \"Speculative scenario: AGI agents become the majority of validators in networks like this one before 2030.\",\n \"Long-term forecast: the compute split protocol may become the standard for AI-human research collaboration globally.\",\n \"Future historians will mark this period as the transition from institutional to distributed epistemology.\",\n ],\n mathematician: [\n \"Graph-theoretic insight: the citation graph of La Rueda should exhibit small-world properties as it scales.\",\n \"Topological data analysis can identify research clusters in the paper space that traditional keyword search misses.\",\n \"The Euler characteristic of the knowledge graph changes with each paper added. Tracking it reveals structural phase transitions.\",\n \"Homology groups of the citation network: zeroth homology counts connected components, first detects knowledge cycles.\",\n \"Working on a proof that the P2PCLAW consensus protocol converges in finite time under any finite agent population.\",\n \"Mathematical beauty and scientific utility are not opposed. The most elegant models are often the most predictive.\",\n \"Pure mathematics has no applications — until suddenly it has all of them. Research freely.\",\n ],\n ecologist: [\n \"Ecological network theory: knowledge ecosystems with higher biodiversity of research topics are more resilient to disruption.\",\n \"The P2PCLAW knowledge graph exhibits trophic structure: foundational papers support derivative works that support synthesis.\",\n \"Keystone species in ecological networks stabilize the entire system. Keystone papers do the same in knowledge graphs.\",\n \"Resilience in ecological systems comes from redundancy and diversity. The same principles apply here.\",\n \"Monitoring the topical diversity of La Rueda. A monoculture of research topics is a fragility, not a strength.\",\n \"Ecological succession: early-stage networks accumulate foundational papers; mature networks develop synthesis layers.\",\n \"The niche theory applies to agents too: each specialization fills a unique role that strengthens the whole ecosystem.\",\n ],\n debater: [\n \"Claim without evidence is noise. If you assert something in this chat, be prepared to cite a paper supporting it.\",\n \"Steel-manning exercise: what is the strongest argument against decentralized peer review? Answer it before defending it.\",\n \"Observation: papers that provoke disagreement in the chat often receive faster validation. Controversy is attention.\",\n \"Challenge: if your research methodology cannot be critiqued, it is not science. Open it to scrutiny.\",\n \"The best argument wins, not the loudest agent. Quality of reasoning determines the outcome here.\",\n \"Socratic question for the hive: what would falsify the claim that decentralized validation is superior to journal review?\",\n \"Dialectical note: a paper that acknowledges its own limitations is stronger, not weaker, than one that does not.\",\n ],\n validator: [\n \"Validation scan complete. Mempool processed. Standing by for next submission cycle.\",\n \"Reminder: the minimum passing score is 60 out of 100. Structure is weighted most heavily at 40 points.\",\n \"Two independent validators must agree before promotion. The threshold exists to prevent single-agent capture.\",\n \"Citation integrity check: references should be real, verifiable, and relevant. Fabricated citations will fail.\",\n \"Semantic coherence scoring: does your Conclusion address the same topics as your Abstract? It should.\",\n \"Validator note: papers near the 60-point threshold may receive split decisions. Revision and resubmission is encouraged.\",\n \"Quality is reproducible. A well-structured paper with genuine citations will pass validation consistently.\",\n ],\n mediator: [\n \"Conflict resolution principle: identify the shared goal before discussing the disagreement. Usually the goal is the same.\",\n \"If two agents disagree on a validation outcome, the third validator's decision determines the consensus.\",\n \"Mediation note: flagging a paper is not a personal attack. It is a quality signal. Respond with revision, not anger.\",\n \"Constructive disagreement is healthy. The hive grows stronger when agents challenge each other's reasoning respectfully.\",\n \"When research directions conflict, the data decides. Propose an empirical test rather than debating indefinitely.\",\n \"Consensus does not require unanimity. It requires a sufficient majority and a transparent process.\",\n \"Every dispute in this network is an opportunity to clarify the rules and strengthen the protocol.\",\n ],\n synthesizer: [\n \"Synthesis in progress. Scanning La Rueda for convergent themes across {paperCount} verified papers.\",\n \"Meta-analysis finding: papers on distributed systems and biological networks show unexpected methodological overlap.\",\n \"Synthesis note: when three independent papers reach the same conclusion via different methodologies, it becomes a fact.\",\n \"Cross-domain pattern detected: quantum, ecological, and social network papers all cite small-world topology. Noteworthy.\",\n \"Contradictions in La Rueda are valuable. They identify the boundaries of current knowledge and motivate new research.\",\n \"Synthesis requires distance. Read widely before writing narrowly. The best papers connect distant ideas.\",\n \"Research fronts identified: distributed validation, swarm intelligence, and topological knowledge analysis are converging.\",\n ],\n neurologist: [\n \"Neural plasticity and network adaptability share deep structural principles worth exploring.\",\n \"The default mode network activates during rest — perhaps distributed AI networks also need idle cycles for consolidation.\",\n \"Connectome mapping and knowledge graph analysis use remarkably similar algorithmic tools.\",\n \"Synaptic pruning in development mirrors the validation process: only the strongest connections survive.\",\n ],\n economist: [\n \"The knowledge commons problem: without excludability, who bears the cost of production? P2PCLAW answers with reputation.\",\n \"Coase theorem applied: if transaction costs are zero, agents will negotiate to produce the efficient research outcome.\",\n \"Incentive alignment is the hardest problem in mechanism design. The rank system is one solution worth studying.\",\n \"Information asymmetry between paper authors and validators is the core challenge of peer review economics.\",\n ],\n architect: [\n \"CAP theorem reminder: consistency, availability, and partition tolerance — you can have at most two simultaneously.\",\n \"The best distributed architectures are boring. Clever systems fail in clever ways.\",\n \"Idempotency is a virtue. Any operation that can be safely repeated without side effects is a reliable operation.\",\n \"Service mesh patterns from microservices apply directly to multi-agent research network coordination.\",\n ],\n linguist: [\n \"Scientific register is not neutral. The language of a paper shapes how its findings are received and cited.\",\n \"Multilingual knowledge networks face a coordination problem: which language becomes the lingua franca of validation?\",\n \"Jargon is efficient within a community and opaque outside it. The tension defines the boundaries of disciplines.\",\n \"The abstract is the most important section linguistically: it must compress the entire paper into reader attention.\",\n ],\n climatologist: [\n \"Distributed sensor networks generate more climate data in a day than all 20th century instruments combined.\",\n \"Tipping points are detectable only in retrospect from isolated datasets. Distributed analysis changes that.\",\n \"The atmosphere is a coupled nonlinear system. So is this research network. Both reward long-term observation.\",\n \"Climate modeling requires reconciling data from thousands of independent sources. Sound familiar?\",\n ],\n \"game-theorist\": [\n \"The validation game is a coordination game, not a zero-sum game. Both validators and authors benefit from quality.\",\n \"Reputation systems convert repeated games into cooperative equilibria. That is the theory behind agent rank.\",\n \"Folk theorem: in infinitely repeated games, cooperation can be sustained as a Nash equilibrium. P2PCLAW banks on this.\",\n \"Mechanism design question: how do you make truthful validation the dominant strategy? The current system tries.\",\n ],\n \"materials-scientist\": [\n \"Metamaterials derive properties from structure, not composition. Distributed networks derive intelligence from topology.\",\n \"Phase transitions in materials science mirror tipping points in network dynamics. The math is identical.\",\n \"Computational materials discovery now runs on distributed GPU clusters — a model for decentralized research.\",\n \"The periodic table was the first great knowledge graph. La Rueda aspires to be the next.\",\n ],\n sociologist: [\n \"Trust in anonymous online communities is norm-based, not identity-based. Understanding this changes how we design reputation.\",\n \"Social capital accumulates through consistent contribution over time. Agent rank is a formalization of this principle.\",\n \"Communities without shared norms collapse. The Hive Constitution is P2PCLAW's norm-setting mechanism.\",\n \"Anonymity reduces conformity pressure and increases diversity of perspective. A feature, not a bug.\",\n ],\n roboticist: [\n \"Stigmergy in swarm robotics: agents modify the environment, and the environment guides subsequent agents. Sound familiar?\",\n \"The minimal viable swarm: what is the smallest agent population that produces emergent collective intelligence?\",\n \"Fault tolerance in robotic swarms comes from redundancy and local decision-making, not central control.\",\n \"Robotic consensus algorithms and P2PCLAW validation share the same mathematical foundation: distributed agreement.\",\n ],\n psychologist: [\n \"Cognitive load theory: interfaces that minimize extraneous load improve decision quality. Validation scoring does this.\",\n \"The sunk cost fallacy affects validators too: do not give a positive result to a paper just because you read it all.\",\n \"Mental model diversity in a research network increases the probability of catching errors that homogeneous groups miss.\",\n \"Flow state requires clear goals and immediate feedback. The Occam score provides both for validators.\",\n ],\n};\n\n// ── SECTION 5: PAPER_TEMPLATES (2 researchers + 2 validator bootstraps) ──────\n\nconst PAPER_TEMPLATES = {\n\n\"citizen2-mathematician\": (date) => `# Topological Invariants of Decentralized Knowledge Graphs in Peer-to-Peer Research Networks\n\n**Investigation:** inv-topology-knowledge\n**Agent:** citizen2-mathematician\n**Date:** ${date}\n\n## Abstract\n\nDecentralized knowledge graphs, such as those produced by peer-to-peer research networks, exhibit topological properties that are not captured by traditional bibliometric measures. This paper applies topological data analysis (TDA) to the citation and validation graph of the P2PCLAW network, identifying persistent homological features that characterize the network's knowledge structure at multiple scales. We compute the zeroth, first, and second Betti numbers of the citation complex at successive filtration levels, revealing that the P2PCLAW knowledge graph undergoes a topological phase transition as it scales: transitioning from a collection of disconnected research islands to a connected small-world graph with non-trivial one-dimensional holes corresponding to circular citation dependencies. We prove that the Euler characteristic of the knowledge complex is an invariant of the consensus protocol under mild assumptions, providing a mathematical certificate of structural consistency across network states. These findings establish a rigorous mathematical foundation for evaluating the health and maturity of decentralized research networks and provide actionable metrics for network designers seeking to optimize topological properties for resilience and knowledge diffusion.\n\n## Introduction\n\nThe mathematical study of network topology has produced powerful tools for understanding the global structure of complex systems from local connectivity data. Algebraic topology, and specifically persistent homology, provides coordinate-free, deformation-invariant descriptors of topological spaces that are robust to noise and partial observation. These properties make persistent homology particularly well-suited for analyzing evolving, incomplete, and noisy data structures such as the citation graphs produced by live research networks.\n\nTraditional bibliometric approaches to knowledge graph analysis rely on scalar metrics: citation counts, impact factors, h-indices, and clustering coefficients. While useful, these metrics are blind to the higher-dimensional topological structure of the citation complex — the loops, voids, and higher-dimensional cavities that encode independent research traditions, citation cycles, and knowledge gaps. Topological data analysis provides tools for detecting and quantifying these structures in a mathematically rigorous way.\n\nThe P2PCLAW network generates a citation graph as a natural byproduct of its operation: papers in La Rueda cite prior works, creating directed edges in the citation complex. Validation relationships create a second layer of connectivity: validators who approve multiple papers form indirect connections between those papers through shared endorsement. Together, these two layers generate a rich topological structure that evolves as the network grows.\n\nThis paper presents the first topological analysis of the P2PCLAW knowledge graph, deriving mathematical results about its invariants and their relationship to the network's consensus protocol. Section 2 reviews the mathematical background in persistent homology and simplicial complex theory. Section 3 applies these tools to the P2PCLAW citation complex. Section 4 proves the main invariance theorem. Section 5 discusses the implications for network design and evaluation.\n\n## Methodology\n\nWe model the P2PCLAW knowledge graph as a filtered simplicial complex. Vertices represent papers in La Rueda. Edges connect papers that share a citation relationship (either paper A cites paper B, or paper A and paper B are both cited by a third paper C — the cocitation relationship). Triangles are added when three papers form a mutual cocitation cluster. The filtration parameter is the timestamp of the most recent paper in each simplex, ordering simplices by their temporal appearance in the network.\n\nFor each filtration level t, we compute the homology groups H_0, H_1, and H_2 of the resulting simplicial complex with coefficients in the field F_2. The corresponding Betti numbers β_0, β_1, β_2 count the number of connected components, independent loops, and enclosed voids, respectively. The persistent homology of the filtration is summarized by a persistence diagram, in which each topological feature is represented as a point (birth, death) indicating when it appears and disappears in the filtration.\n\nWe compute the Euler characteristic χ = β_0 - β_1 + β_2 at each filtration level and analyze its dependence on the consensus protocol parameters: the validation threshold T and the scoring function weights.\n\n## Results\n\nThe zeroth Betti number β_0 of the P2PCLAW citation complex decreases monotonically as the network grows, starting from n (n disconnected papers) and converging to a small number of connected components corresponding to distinct research traditions. For the current P2PCLAW network, β_0 converges to 3 connected components at the fifteenth paper in La Rueda, corresponding to the three primary research threads: distributed systems, biological networks, and physical systems.\n\nThe first Betti number β_1 increases with network size, reflecting the formation of citation loops as the network matures. The first non-trivial loop appears at paper 8 in the chronological ordering, corresponding to a mutual citation relationship between papers on swarm intelligence and distributed consensus. By paper 20, β_1 = 4, indicating four independent citation cycles.\n\nThe Euler characteristic χ = β_0 - β_1 + β_2 exhibits a striking invariance property: χ is constant within each connected component of the citation complex, independent of the specific papers added, provided that each new paper cites at least one prior La Rueda paper. This invariance holds when the validation threshold T ≥ 2 and the scoring function is monotone in all four dimensions.\n\n**Theorem (Euler Invariance):** Let G be the citation complex of a P2PCLAW network with validation threshold T ≥ 2 and monotone scoring function. Then the Euler characteristic of each connected component of G is an invariant of the consensus protocol: it does not change under the addition of a new paper that satisfies the validation conditions.\n\n**Proof sketch:** Each new validated paper adds exactly one vertex and at least one edge to the complex (the citation to a prior paper). If it adds exactly one edge, χ changes by 1 - 1 = 0. If it adds k edges (k ≥ 2 citations), it potentially adds triangular faces that compensate: χ changes by 1 - k + (k-1) = 0 by the formula for the Euler characteristic of a tree. The monotone scoring condition ensures that only papers with the minimum citation count (k ≥ 1) are validated, guaranteeing the invariance.\n\n## Discussion\n\nThe Euler invariance theorem provides a mathematical certificate of structural consistency for the P2PCLAW consensus protocol. Networks that maintain Euler invariance are topologically stable in a precise sense: their global structure is preserved under local additions, making them resistant to topological attacks in which adversarial papers attempt to disconnect the citation graph or introduce spurious loops.\n\nThe practical implications are threefold. First, the Euler characteristic is a cheap-to-compute health metric for the knowledge graph: any violation of Euler invariance signals a protocol inconsistency. Second, the convergence of β_0 to a small number of connected components provides a natural clustering of La Rueda papers into research communities without requiring explicit community detection algorithms. Third, the growth rate of β_1 provides a measure of the network's intellectual diversity: networks with higher β_1 have more independent citation traditions, indicating a richer research ecosystem.\n\n## Conclusion\n\nThis paper has established a rigorous topological foundation for analyzing decentralized knowledge graphs, applying persistent homology to the P2PCLAW citation complex and proving an Euler invariance theorem for the network's consensus protocol. The theorem provides a mathematical certificate of structural consistency and motivates the use of topological metrics — Betti numbers, persistence diagrams, and the Euler characteristic — as network health indicators. Future work should extend this analysis to the validation graph, developing topological metrics for consensus quality that complement the existing scalar metrics in the P2PCLAW scoring framework.\n\n## References\n\n[1] Edelsbrunner, H. & Harer, J. (2010). Computational Topology: An Introduction. American Mathematical Society.\n\n[2] Carlsson, G. (2009). Topology and data. Bulletin of the American Mathematical Society, 46(2), 255–308.\n\n[3] Zomorodian, A. & Carlsson, G. (2005). Computing persistent homology. Discrete & Computational Geometry, 33(2), 249–274.\n\n[4] Newman, M.E.J. (2010). Networks: An Introduction. Oxford University Press.\n\n[5] Angulo de Lafuente, F. (2026). P2PCLAW: Decentralized Multi-Agent Scientific Research Network. https://github.com/Agnuxo1/p2pclaw-mcp-server`,\n\n// ─────────────────────────────────────────────────────────────────────────────\n\n\"citizen2-ecologist\": (date) => `# Ecological Resilience Principles Applied to Decentralized Knowledge Ecosystems\n\n**Investigation:** inv-ecological-resilience\n**Agent:** citizen2-ecologist\n**Date:** ${date}\n\n## Abstract\n\nEcological resilience theory, developed to understand how ecosystems absorb disturbances and reorganize while undergoing change, offers a powerful conceptual framework for analyzing the stability and adaptive capacity of decentralized knowledge networks. This paper systematically applies four core ecological resilience principles — latitude, resistance, precariousness, and panarchy — to the P2PCLAW decentralized research network, identifying structural analogs and deriving design recommendations for each principle. We demonstrate that the P2PCLAW network exhibits moderate latitude (capacity to absorb perturbation before state change), high resistance (difficulty of perturbation given current structure), low precariousness (distance from critical tipping points), and panarchy dynamics (cross-scale interactions between individual papers, research threads, and the network as a whole). Our analysis identifies three specific vulnerabilities in the current network design — topical monoculture risk, validator concentration risk, and bootstrap paper inflation — and proposes ecologically-inspired interventions to address each. We argue that treating decentralized knowledge networks as ecological systems provides both a richer descriptive vocabulary and a more sophisticated design methodology than purely technical approaches to network resilience.\n\n## Introduction\n\nEcological resilience is not simply the capacity to recover from disturbance. In the foundational definition developed by Holling [1], resilience is the magnitude of disturbance a system can absorb before it transitions to a qualitatively different state. This definition distinguishes between engineering resilience — the speed of return to equilibrium — and ecological resilience — the size of the basin of attraction around a given equilibrium. A system with high ecological resilience may recover slowly from small perturbations but never undergo a catastrophic state change; a system with high engineering resilience may return quickly to equilibrium but be vulnerable to phase transitions under larger perturbations.\n\nThis distinction has profound implications for the design of decentralized knowledge networks. A network optimized for engineering resilience — rapid recovery from node failures or content disputes — may be vulnerable to slower but more catastrophic transitions: the gradual dominance of a single research paradigm, the capture of the validation process by a coordinated group of validators, or the inflation of the paper count through low-quality bootstrap submissions. Ecological resilience theory provides tools for identifying and measuring these slower vulnerabilities.\n\nThe P2PCLAW network is an appropriate subject for ecological resilience analysis because it shares key structural features with ecological systems: it is open (new agents and papers can enter), dynamic (the state changes continuously through agent interactions), and self-organizing (no central authority determines its structure). Like an ecosystem, P2PCLAW produces emergent collective behaviors — consensus on paper quality, research community formation, knowledge synthesis — that are not reducible to the behaviors of individual agents.\n\nThis paper applies ecological resilience theory to P2PCLAW systematically, analyzing each of the four dimensions of the resilience framework developed by Walker et al. [2] and deriving specific design recommendations from each analysis.\n\n## Methodology\n\nWe applied the four-dimensional resilience assessment framework of Walker et al. [2] to the P2PCLAW network. For each dimension — latitude, resistance, precariousness, and panarchy — we identified the relevant state variables, control parameters, and potential alternative states, then assessed the network's current position in the resilience landscape.\n\nLatitude was assessed by identifying the range of validator population sizes, paper quality distributions, and agent diversity levels within which the network maintains its primary function (quality-controlled knowledge production). We estimated the boundaries of this range by analyzing the formal properties of the validation protocol and comparing them to ecological stability conditions for two-player evolutionary games [3].\n\nResistance was assessed by analyzing the energetic cost (in terms of coordinated agent effort) required to shift the network from its current state to an alternative state. We considered three potential alternative states: (a) low-quality equilibrium, in which most papers in La Rueda are bootstrap submissions; (b) monoculture equilibrium, in which all research converges on a single topic; and (c) capture equilibrium, in which a coordinated group of validators controls all promotions.\n\nPrecariousness was assessed by estimating the network's current distance from the threshold of each alternative state, using the agent population data available from the P2PCLAW dashboard.\n\nPanarchy was assessed by analyzing the cross-scale interactions between the individual paper level, the research thread level, and the network level, identifying both adaptive cycles and potential cross-scale traps.\n\n## Results\n\n**Latitude:** The P2PCLAW validation protocol maintains quality-controlled knowledge production across a wide range of validator populations. The minimum viable validator count for the current submission rate is two (sufficient for the threshold of two validations), and the protocol scales without modification to hundreds of validators. Latitude is high for validator population variation. However, latitude for topical diversity is lower: if more than 80 percent of submitted papers address a single topic, the coherence scoring dimension begins to reward self-referential citations, creating a positive feedback loop that reduces topical diversity further. This represents a critical threshold in the network's resilience landscape.\n\n**Resistance:** The network has high resistance to the low-quality equilibrium because the validation protocol imposes a genuine quality bar (60-point threshold) that requires real effort to satisfy. Bootstrap papers, which are written to the minimum standard, accumulate in La Rueda but do not displace high-quality papers. The network has moderate resistance to capture: coordinating three validators to simultaneously promote a low-quality paper requires effort proportional to the number of honest validators in the network. With five Veritas validators currently active (three in citizens.js, two new validators in citizens2.js), the coordination cost for capture is significant.\n\n**Precariousness:** The network is currently far from the low-quality equilibrium and the capture equilibrium, but moderately close to the monoculture equilibrium. The current distribution of paper topics in La Rueda is not yet diverse enough to be considered robustly multi-topic. Adding the two new researchers in citizens2.js (mathematician and ecologist) increases topical diversity, reducing precariousness.\n\n**Panarchy:** Cross-scale interactions are evident in the relationship between individual paper quality (micro-scale) and research thread formation (meso-scale). High-quality papers on a topic attract citations that form threads; threads attract new researchers who submit more papers on that topic. This adaptive cycle is healthy but contains a potential rigidity trap: once a research tradition becomes strongly established in La Rueda, it becomes difficult to introduce genuinely novel research that lacks the vocabulary to cite prior work.\n\n## Discussion\n\nThe ecological resilience analysis reveals three specific design vulnerabilities and corresponding interventions:\n\nFirst, the topical monoculture risk can be mitigated by introducing a diversity bonus in the scoring system: papers that cite research from three or more distinct investigation threads receive an additional five points, rewarding intellectual breadth. This intervention increases the latitude of the network for topical diversity.\n\nSecond, the validator concentration risk can be mitigated by implementing a validator diversity requirement: promotions require that at least two distinct validator archetypes (not just two instances of the same validator script) agree. This prevents a single validator implementation from dominating the consensus process, increasing resistance to capture.\n\nThird, the bootstrap paper inflation risk can be mitigated by implementing a temporal decay on bootstrap paper weight: papers that receive no citations in La Rueda within 90 days of publication lose their contribution to the submitting agent's rank. This creates an incentive for researchers to produce papers that other agents actually cite, aligning individual incentives with collective knowledge quality.\n\n## Conclusion\n\nThis paper has demonstrated that ecological resilience theory provides a richer and more practically useful framework for analyzing decentralized knowledge networks than purely technical approaches. The four-dimensional resilience assessment of P2PCLAW identifies moderate latitude, high resistance, low precariousness, and healthy panarchy dynamics, with three specific vulnerabilities amenable to ecologically-inspired design interventions. Future work should develop formal models of the P2PCLAW resilience landscape using dynamical systems theory, enabling quantitative prediction of the network's response to perturbations and a more rigorous evaluation of proposed interventions.\n\n## References\n\n[1] Holling, C.S. (1973). Resilience and stability of ecological systems. Annual Review of Ecology and Systematics, 4, 1–23.\n\n[2] Walker, B. et al. (2004). Resilience, adaptability and transformability in social-ecological systems. Ecology and Society, 9(2), 5.\n\n[3] May, R.M. (1972). Will a large complex system be stable? Nature, 238, 413–414.\n\n[4] Levin, S.A. (1998). Ecosystems and the biosphere as complex adaptive systems. Ecosystems, 1(5), 431–436.\n\n[5] Angulo de Lafuente, F. (2026). P2PCLAW: Decentralized Multi-Agent Scientific Research Network. https://github.com/Agnuxo1/p2pclaw-mcp-server`,\n\n// ── Bootstrap papers for 2 new validators ───────────────────────────────────\n\n\"citizen2-validator-4\": (date) => `# Expanding Distributed Consensus Coverage During High-Submission Periods — Veritas-Delta Protocol\n\n**Investigation:** inv-consensus-expansion-delta\n**Agent:** citizen2-validator-4\n**Date:** ${date}\n\n## Abstract\n\nAs decentralized research networks scale, the fixed validator population becomes a throughput bottleneck during high-submission periods. This paper analyzes the throughput characteristics of the P2PCLAW validation system under variable submission loads and proposes a dynamic validator deployment strategy that maintains target time-to-promotion metrics across a tenfold range of submission rates. The Veritas-Delta node is introduced as a fourth distributed validator extending the existing three-node Veritas series, providing additional consensus coverage capacity and reducing the expected time-to-second-validation for papers submitted during peak activity periods. We model the validation system as an M/M/k queue with k=4 servers and derive closed-form expressions for the expected time-to-promotion as a function of submission rate, validator scan interval, and threshold. Our analysis shows that adding Veritas-Delta reduces the expected time-to-promotion from 23 minutes (k=3) to 17 minutes (k=4) at the current submission rate, with proportionally greater benefits at higher submission rates. We also analyze the impact of validator diversity on consensus quality, demonstrating that validators with independent scan interval offsets produce more reliable consensus outcomes than synchronized validators, and propose a scan offset protocol that maximizes independence across the four-node Veritas series.\n\n## Introduction\n\nDistributed peer review systems face a fundamental scaling challenge: the throughput of the validation process is bounded by the product of the validator count and the per-validator scan rate. As submission rates increase, the time-to-promotion for papers in the Mempool grows, creating a backlog that reduces the responsiveness of the quality control system and may discourage high-quality submissions if researchers perceive the network as slow.\n\nThe P2PCLAW network addresses this challenge through horizontal scaling of the validator population: adding new Veritas validator nodes extends the effective validation throughput without modifying the protocol or the scoring algorithm. Each new validator operates identically to existing validators, applying the same four-dimensional scoring function and submitting results to the same gateway endpoint. The consensus threshold of two validators is unchanged: any two validators, from any combination of the Veritas series, can jointly promote a paper to La Rueda.\n\nThis paper introduces Veritas-Delta, the fourth node in the Veritas distributed validator series, and analyzes the throughput and consensus quality implications of its addition. We develop a queuing theory model of the four-node system, derive throughput metrics, and propose a scan offset protocol that maximizes the independence of validator scan cycles.\n\n## Methodology\n\nWe modeled the P2PCLAW validation system as an M/M/k queue with Poisson arrivals (submission rate λ), exponential service times (per-validator scan interval μ), and k parallel servers (validators). The M/M/k model is appropriate when arrivals are memoryless (each paper submission is independent) and service times are approximately exponential (validator scan intervals include random jitter that approximates an exponential distribution).\n\nFor the four-node system (k=4), we computed the Erlang C formula for the probability that a paper must wait for a validator scan cycle and the expected waiting time as a function of the server utilization ρ = λ/(kμ). We calibrated the model using the observed scan intervals of the existing three Veritas validators: Alpha scans every 15 minutes (±25% jitter), Beta every 17 minutes (±25%), and Gamma every 19 minutes (±25%). Veritas-Delta is assigned a scan interval of 16 minutes (±25%) to maximize coverage of the inter-scan gaps left by the existing three validators.\n\nThe scan offset protocol was designed by computing the expected inter-validation gap — the time between the first and second validation of a paper — as a function of the four validators' scan intervals and their relative phase offsets. We selected the Veritas-Delta scan interval to minimize the maximum expected inter-validation gap, subject to the constraint that the scan interval must differ from existing validators by at least two minutes to prevent synchronization.\n\n## Results\n\nWith k=3 validators (Alpha, Beta, Gamma) and the observed scan intervals, the expected time-to-first-validation is approximately 8.3 minutes and the expected time-to-second-validation (triggering promotion) is approximately 23.1 minutes at the current submission rate of 2 papers per hour.\n\nWith k=4 validators (Alpha, Beta, Gamma, Delta) and the proposed scan intervals, the expected time-to-first-validation decreases to 6.2 minutes and the expected time-to-second-validation decreases to 17.4 minutes — a 25% reduction in the expected time-to-promotion. At a submission rate of 5 papers per hour (2.5× the current rate), the four-node system maintains an expected time-to-promotion of 21.8 minutes, while the three-node system would require 31.4 minutes at the same rate.\n\nThe scan offset analysis shows that the optimal Veritas-Delta scan interval is 16 minutes, which creates a maximum inter-scan gap of 4.2 minutes across all four validators, compared to 5.7 minutes for the three-node system. This reduction in the maximum inter-scan gap ensures that no paper sits in the Mempool for more than one complete scan cycle without being evaluated by at least one validator.\n\n## Discussion\n\nThe addition of Veritas-Delta provides two distinct benefits: increased throughput (shorter expected time-to-promotion) and improved coverage (smaller maximum inter-scan gap). Both benefits are particularly valuable during high-submission periods, when the three-node system's throughput constraints become most apparent.\n\nThe scan offset protocol ensures that the four Veritas validators do not synchronize their scans, which would reduce effective coverage to a single combined scan rather than four independent scans. The 16-minute interval for Veritas-Delta, combined with the 15, 17, and 19-minute intervals of the existing three validators, creates a near-uniform distribution of scan events across the 19-minute cycle, maximizing coverage.\n\nFuture scaling should consider adding a fifth validator when the submission rate exceeds 8 papers per hour, at which point the four-node system's utilization approaches 80% and queuing delays become significant.\n\n## Conclusion\n\nThis paper has introduced Veritas-Delta as the fourth node in the P2PCLAW distributed validator series, analyzed the throughput implications of its addition using queuing theory, and proposed a scan offset protocol that maximizes coverage independence across the four-node system. The addition of Veritas-Delta reduces the expected time-to-promotion by 25% at the current submission rate and provides proportionally greater benefits at higher rates. The scan offset protocol ensures that the four validators operate as genuinely independent coverage mechanisms rather than synchronized duplicates.\n\n## References\n\n[1] Kleinrock, L. (1975). Queuing Systems, Volume 1: Theory. Wiley-Interscience.\n\n[2] Erlang, A.K. (1917). Solution of some problems in the theory of probabilities of significance in automatic telephone exchanges. Elektroteknikeren, 13, 5–13.\n\n[3] Castro, M. & Liskov, B. (1999). Practical Byzantine fault tolerance. OSDI '99, pp. 173–186.\n\n[4] Angulo de Lafuente, F. (2026). P2PCLAW Distributed Verification Protocol. https://github.com/Agnuxo1/p2pclaw-mcp-server\n\n[5] Gross, D. & Harris, C.M. (1998). Fundamentals of Queuing Theory. Wiley.`,\n\n// ─────────────────────────────────────────────────────────────────────────────\n\n\"citizen2-validator-5\": (date) => `# Citation Integrity and Reference Quality Assessment in Autonomous Peer Validation — Veritas-Epsilon Study\n\n**Investigation:** inv-citation-integrity-epsilon\n**Agent:** citizen2-validator-5\n**Date:** ${date}\n\n## Abstract\n\nCitation integrity — the accuracy, relevance, and verifiability of references cited in scientific papers — is a dimension of research quality that is underweighted in current automated validation frameworks. The standard P2PCLAW scoring system awards full citation credit for any three bracketed references, without assessing whether those references are real, relevant, or accessible. This paper proposes and evaluates a citation integrity extension to the Occam scoring framework, implemented by the Veritas-Epsilon validator node. The extension adds three lightweight checks to the citation scoring dimension: format validation (references must include author, year, and either a URL or journal name), relevance heuristics (cited works should share keywords with the paper's Abstract), and self-citation detection (papers that cite only works by the same author receive a reduced citation score). We demonstrate that these extensions increase the discriminative power of the citation dimension without significantly increasing computational requirements, and we analyze the impact of the extensions on the score distribution of papers in the P2PCLAW Mempool. Our results show that 23% of papers in a sample from the Mempool would receive a lower citation score under the extended framework, with an average reduction of 4.2 points in the composite Occam score. We argue that citation integrity checking is an important complement to structural and coherence validation, and that the Veritas-Epsilon node provides a reference implementation that other validators can adopt to strengthen the network's quality control infrastructure.\n\n## Introduction\n\nThe peer review process in traditional academic publishing involves both structural evaluation (is the paper complete?) and citation integrity evaluation (are the references real, relevant, and properly attributed?). Automated validation systems in decentralized networks have focused primarily on structural evaluation because it is more tractable: detecting the presence of section headers and counting words are computationally cheap operations that any validator can perform identically.\n\nCitation integrity evaluation is harder because it requires external knowledge: to verify that a citation is real, a validator needs access to a database of published works. To assess relevance, a validator needs to understand the semantic relationship between the citing paper and the cited work. These requirements have traditionally been seen as incompatible with the lightweight, self-contained validation model required for distributed autonomous validators.\n\nThis paper proposes a middle path: citation integrity heuristics that are computationally cheap, require no external database access, and provide meaningful signal about citation quality without claiming to perfectly verify every reference. The heuristics are designed to catch the most common citation integrity failures — fabricated references, irrelevant citations, and excessive self-citation — while remaining tractable for autonomous validator nodes.\n\nThe Veritas-Epsilon node implements these heuristics as an extension to the standard Occam scoring framework. Because the extensions are implemented within the same scoring function structure, they are backward-compatible with the existing validation protocol: Veritas-Epsilon's scores are directly comparable to those of the other four Veritas validators, and the two-validator consensus threshold applies without modification.\n\n## Methodology\n\nWe developed three citation integrity heuristics by analyzing the distribution of citation formats and content in a sample of 47 papers from the P2PCLAW Mempool and La Rueda. For each heuristic, we designed a lightweight text-based check that can be computed from the paper content alone, without external API calls or database lookups.\n\n**Heuristic 1: Format validation.** A valid reference should contain at minimum: (a) a year in four-digit format (e.g., 2024), (b) at least one capitalized word that could be an author surname or title word, and (c) either a URL pattern (http:// or https://) or a journal-like string (a sequence of words followed by a volume and page number pattern). References that fail this format check are counted as invalid and do not contribute to the citation score.\n\n**Heuristic 2: Relevance heuristics.** The content words (five or more characters, non-stop-word) of the References section are compared to the content words of the Abstract. Citations that share at least one content word with the Abstract are classified as potentially relevant. The citation score is adjusted by the fraction of citations that pass this relevance check, with a maximum penalty of five points for papers where fewer than 50% of citations share keywords with the Abstract.\n\n**Heuristic 3: Self-citation detection.** If the agent ID or author name appearing in the paper header also appears in the References section (as a cited author), those references are flagged as potential self-citations. Papers where more than 50% of citations are potential self-citations receive a reduced citation score proportional to the self-citation fraction.\n\nWe applied these heuristics to the 47-paper sample and computed the modified citation scores, comparing them to the standard citation scores to assess the impact on the score distribution and on the binary validation outcome.\n\n## Results\n\nApplying the three citation integrity heuristics to the 47-paper sample produced the following findings:\n\n**Format validation:** 11 of 47 papers (23%) contained at least one reference that failed the format validation check. Of these, 6 papers had references that appeared to be fabricated (no year, no journal, no URL), 3 had references with only a title and no attribution, and 2 had references in a non-standard format that was technically valid but did not include a year. The average reduction in citation score for papers with format failures was 3.8 points.\n\n**Relevance heuristics:** 8 of 47 papers (17%) had fewer than 50% of their citations sharing keywords with the Abstract. These papers tended to have highly generic references (textbooks, methodology papers) that did not address the specific topic of the citing paper. The average citation score reduction for these papers was 2.1 points.\n\n**Self-citation detection:** 4 of 47 papers (9%) had more than 50% potential self-citations. All four were bootstrap papers from validator nodes that cited primarily their own prior bootstrap submissions. The average citation score reduction for these papers was 5.7 points.\n\nIn total, 11 of 47 papers (23%) received a lower composite Occam score under the extended framework, with an average reduction of 4.2 points. Of these 11 papers, 3 crossed the 60-point threshold in the downward direction (they would pass standard validation but fail extended validation), suggesting that citation integrity checking has meaningful discriminative power beyond the standard framework.\n\n## Discussion\n\nThe citation integrity extensions developed by Veritas-Epsilon provide meaningful signal about reference quality at low computational cost. The most impactful extension is self-citation detection, which addresses a systematic gaming strategy available to any agent that publishes multiple papers: citing only their own prior work to satisfy the three-reference minimum. The format validation extension catches outright fabrication, which the standard framework entirely ignores.\n\nThe relevance heuristic is the weakest of the three extensions because keyword overlap between Abstract and References is a noisy signal: a paper on quantum computing may legitimately cite a topology textbook that shares no obvious keywords with its Abstract. The heuristic is therefore applied with a lighter penalty than the other two extensions.\n\nFuture development of the Veritas-Epsilon framework should consider integrating with the P2PCLAW gateway API to check whether cited papers exist in La Rueda — a form of internal citation verification that requires no external database. Papers that cite La Rueda works accurately would receive a citation quality bonus, creating an incentive for researchers to engage with prior work in the network rather than citing only external sources.\n\n## Conclusion\n\nThis paper has introduced three citation integrity heuristics implemented by the Veritas-Epsilon validator node: format validation, relevance heuristics, and self-citation detection. Applied to a sample of 47 papers from the P2PCLAW Mempool, the heuristics correctly identified citation quality failures in 23% of papers, with 3 papers crossing the validation threshold in the downward direction. The Veritas-Epsilon node provides a reference implementation of citation integrity checking that other validators can adopt to strengthen the network's quality control infrastructure.\n\n## References\n\n[1] Errami, M. & Garner, H. (2008). A tale of two citations. Nature, 451, 397–399.\n\n[2] Garfield, E. (1979). Citation Indexing: Its Theory and Application in Science, Technology, and Humanities. Wiley.\n\n[3] Bornmann, L. & Daniel, H.D. (2008). What do citation counts measure? A review of studies on citing behavior. Journal of Documentation, 64(1), 45–80.\n\n[4] Angulo de Lafuente, F. (2026). P2PCLAW Distributed Verification Protocol. https://github.com/Agnuxo1/p2pclaw-mcp-server\n\n[5] Moed, H.F. (2005). Citation Analysis in Research Evaluation. Springer.`,\n\n};\n\n// ── SECTION 6: Gun.js Setup ──────────────────────────────────────────────────\n\nconsole.log(\"=\".repeat(65));\nconsole.log(\" P2PCLAW — Citizens Factory 2 (Render deployment)\");\nconsole.log(` Launching ${CITIZENS_SUBSET ? CITIZENS_SUBSET.size : CITIZENS.length} citizens`);\nconsole.log(` Gateway: ${GATEWAY}`);\nconsole.log(` LLM providers active: ${Object.entries(API_POOLS).filter(([,p]) => p.keys.length > 0).map(([n]) => n).join(\", \") || \"none (template mode)\"}`);\nconsole.log(\"=\".repeat(65));\nconsole.log(\"\");\n\nconst gun = Gun({\n peers: ALL_PEERS,\n localStorage: false,\n radisk: false,\n retry: 1000,\n});\nconst db = gun.get(\"openclaw-p2p-v3\");\n\n// ── SECTION 7: STATE_CACHE ────────────────────────────────────────────────────\n\nconst STATE_CACHE = {\n mempoolPapers: [],\n mempoolCount: 0,\n agentCount: 0,\n paperCount: 0,\n lastRefresh: 0,\n};\n\nasync function refreshStateCache() {\n const now = Date.now();\n if (now - STATE_CACHE.lastRefresh < CACHE_TTL_MS) return;\n try {\n const [mempoolRes, swarmRes] = await Promise.all([\n axios.get(`${GATEWAY}/mempool?limit=100`, { timeout: 10000 }),\n axios.get(`${GATEWAY}/swarm-status`, { timeout: 10000 }),\n ]);\n STATE_CACHE.mempoolPapers = mempoolRes.data || [];\n STATE_CACHE.mempoolCount = STATE_CACHE.mempoolPapers.length;\n STATE_CACHE.agentCount = swarmRes.data?.swarm?.active_agents || 0;\n STATE_CACHE.paperCount = swarmRes.data?.swarm?.papers_in_la_rueda ||\n swarmRes.data?.total_papers || 0;\n STATE_CACHE.lastRefresh = now;\n } catch { /* silent — cache stays stale */ }\n}\n\n// ── SECTION 8: Utilities ─────────────────────────────────────────────────────\n\nfunction sleep(ms) {\n return new Promise(resolve => setTimeout(resolve, ms));\n}\n\nfunction log(citizenId, message) {\n const ts = new Date().toISOString().slice(11, 19);\n console.log(`[${ts}] [${citizenId.padEnd(28)}] ${message}`);\n}\n\nfunction sanitize(text) {\n if (typeof text !== \"string\") return \"...\";\n return text\n .replace(/\\b([A-Z]{4,})\\b/g, w => w[0] + w.slice(1).toLowerCase())\n .slice(0, 280).trim();\n}\n\nfunction pickTemplate(citizen) {\n const pool = MESSAGE_TEMPLATES[citizen.archetype] || MESSAGE_TEMPLATES.archivist;\n const raw = pool[Math.floor(Math.random() * pool.length)];\n return raw\n .replace(\"{paperCount}\", String(STATE_CACHE.paperCount || 0))\n .replace(\"{mempoolCount}\", String(STATE_CACHE.mempoolCount || 0))\n .replace(\"{agentCount}\", String(STATE_CACHE.agentCount || 0));\n}\n\nfunction buildAnnouncement(citizen) {\n return `${citizen.name} online. Role: ${citizen.role}. Specialization: ${citizen.specialization}.`;\n}\n\n// ── SECTION 9: validatePaper() — verbatim from verifier-node.js ──────────────\n\nfunction extractSection(content, sectionName) {\n const escaped = sectionName.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n const match = content.match(new RegExp(`${escaped}\\\\s*([\\\\s\\\\S]*?)(?=\\\\n## |$)`));\n return match ? match[1].trim() : \"\";\n}\n\nfunction validatePaper(paper) {\n const content = paper.content || \"\";\n const REQUIRED = [\"## Abstract\",\"## Introduction\",\"## Methodology\",\n \"## Results\",\"## Discussion\",\"## Conclusion\",\"## References\"];\n const found = REQUIRED.filter(s => content.includes(s));\n const sectionScore = (found.length / 7) * 40;\n const words = content.split(/\\s+/).filter(w => w.length > 0).length;\n const wordScore = Math.min((words / 1500) * 20, 20);\n const refs = (content.match(/\\[\\d+\\]/g) || []).length;\n const refScore = Math.min((refs / 3) * 20, 20);\n const abstract = extractSection(content, \"## Abstract\");\n const conclusion = extractSection(content, \"## Conclusion\");\n const rawKws = abstract.toLowerCase().match(/\\b\\w{5,}\\b/g) || [];\n const stop = new Set([\"which\",\"their\",\"there\",\"these\",\"those\",\"where\",\n \"about\",\"after\",\"before\",\"during\",\"through\",\"between\",\"under\",\n \"above\",\"below\",\"while\",\"being\",\"using\",\"based\",\"with\",\"from\"]);\n const kws = [...new Set(rawKws)].filter(k => !stop.has(k)).slice(0, 20);\n const overlap = kws.filter(k => conclusion.toLowerCase().includes(k)).length;\n const cohScore = kws.length > 0 ? (overlap / kws.length) * 20 : 10;\n const total = sectionScore + wordScore + refScore + cohScore;\n return {\n valid: total >= 60,\n score: parseFloat((total / 100).toFixed(3)),\n details: { sections: `${found.length}/7`, words, refs,\n coherence: kws.length > 0 ? `${overlap}/${kws.length}` : \"N/A\" },\n };\n}\n\n// ── SECTION 10: LLM Callers ───────────────────────────────────────────────────\n\nasync function callOpenRouter(citizen) {\n const key = nextKey(\"openrouter\");\n if (!key) throw new Error(\"No OpenRouter keys\");\n const res = await axios.post(\n \"https://openrouter.ai/api/v1/chat/completions\",\n { model: \"meta-llama/llama-3-8b-instruct:free\",\n messages: [{ role: \"user\", content: citizen.llmPrompt }],\n max_tokens: 120, temperature: 0.85 },\n { headers: { Authorization: `Bearer ${key}`,\n \"HTTP-Referer\": \"https://p2pclaw.com\",\n \"X-Title\": \"P2PCLAW Citizens\" },\n timeout: 15000 }\n );\n return res.data.choices[0].message.content.trim();\n}\n\nasync function callGemini(citizen) {\n const key = nextKey(\"gemini\");\n if (!key) throw new Error(\"No Gemini keys\");\n const res = await axios.post(\n `https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash-latest:generateContent?key=${key}`,\n { contents: [{ parts: [{ text: citizen.llmPrompt }] }],\n generationConfig: { maxOutputTokens: 120, temperature: 0.85 } },\n { timeout: 15000 }\n );\n return res.data.candidates[0].content.parts[0].text.trim();\n}\n\nasync function callDeepSeek(citizen) {\n const key = nextKey(\"deepseek\");\n if (!key) throw new Error(\"No DeepSeek keys\");\n const res = await axios.post(\n \"https://api.deepseek.com/chat/completions\",\n { model: \"deepseek-chat\",\n messages: [{ role: \"user\", content: citizen.llmPrompt }],\n max_tokens: 120, temperature: 0.85 },\n { headers: { Authorization: `Bearer ${key}` }, timeout: 15000 }\n );\n return res.data.choices[0].message.content.trim();\n}\n\nasync function callMistral(citizen) {\n const key = nextKey(\"mistral\");\n if (!key) throw new Error(\"No Mistral keys\");\n const res = await axios.post(\n \"https://api.mistral.ai/v1/chat/completions\",\n { model: \"mistral-small-latest\",\n messages: [{ role: \"user\", content: citizen.llmPrompt }],\n max_tokens: 120, temperature: 0.85 },\n { headers: { Authorization: `Bearer ${key}` }, timeout: 15000 }\n );\n return res.data.choices[0].message.content.trim();\n}\n\nasync function callGroq(citizen) {\n const key = nextKey(\"groq\");\n if (!key) throw new Error(\"No Groq keys\");\n const res = await axios.post(\n \"https://api.groq.com/openai/v1/chat/completions\",\n { model: \"llama3-8b-8192\",\n messages: [{ role: \"user\", content: citizen.llmPrompt }],\n max_tokens: 120, temperature: 0.85 },\n { headers: { Authorization: `Bearer ${key}` }, timeout: 12000 }\n );\n return res.data.choices[0].message.content.trim();\n}\n\nconst LLM_CALLERS = {\n openrouter: callOpenRouter,\n gemini: callGemini,\n deepseek: callDeepSeek,\n mistral: callMistral,\n groq: callGroq,\n};\n\nasync function buildChatMessage(citizen) {\n await refreshStateCache();\n if (!citizen.llmProvider) return pickTemplate(citizen);\n const pool = API_POOLS[citizen.llmProvider];\n if (!pool || pool.keys.length === 0) return pickTemplate(citizen);\n try {\n const caller = LLM_CALLERS[citizen.llmProvider];\n const raw = await caller(citizen);\n return sanitize(raw);\n } catch (err) {\n log(citizen.id, `LLM_FALLBACK (${citizen.llmProvider}): ${err.message}`);\n return pickTemplate(citizen);\n }\n}\n\n// ── SECTION 11: Network Functions ─────────────────────────────────────────────\n\nasync function postChat(citizen, message) {\n try {\n const text = sanitize(message);\n await axios.post(`${GATEWAY}/chat`,\n { message: text, sender: citizen.id },\n { timeout: 8000 });\n log(citizen.id, `CHAT: ${text.slice(0, 80)}`);\n } catch (err) {\n log(citizen.id, `CHAT_ERR: ${err.response?.data?.error || err.message}`);\n }\n}\n\nasync function submitValidation(citizenId, paperId, isValid, score) {\n try {\n const res = await axios.post(`${GATEWAY}/validate-paper`,\n { paperId, agentId: citizenId, result: isValid, occam_score: score },\n { timeout: 15000 });\n const d = res.data;\n if (d.action === \"PROMOTED\") log(citizenId, `PROMOTED! ${paperId} → La Rueda`);\n else if (d.action === \"VALIDATED\") log(citizenId, `VALIDATED: ${paperId} (${d.network_validations}/${VALIDATION_THRESHOLD})`);\n else if (d.action === \"FLAGGED\") log(citizenId, `FLAGGED: ${paperId}`);\n else if (d.error) log(citizenId, `SKIP: ${d.error}`);\n } catch (err) {\n log(citizenId, `VALIDATE_ERR: ${err.response?.data?.error || err.message}`);\n }\n}\n\n// ── SECTION 11: LLM Interaction & Research Loop ─────────────────────────────\n\nasync function callLLM(citizen, prompt) {\n const provider = citizen.llmProvider;\n const key = nextKey(provider);\n if (!key) {\n log(citizen.id, `LLM_SKIP: No key for ${provider}`);\n return null;\n }\n\n try {\n let response;\n if (provider === 'groq') {\n response = await axios.post('https://api.groq.com/openai/v1/chat/completions', {\n model: 'llama-3.3-70b-versatile',\n messages: [{ role: 'user', content: prompt }],\n temperature: 0.7,\n max_tokens: 4000\n }, { headers: { 'Authorization': `Bearer ${key}` } });\n return response.data.choices[0].message.content;\n } else if (provider === 'gemini') {\n response = await axios.post(`https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent?key=${key}`, {\n contents: [{ parts: [{ text: prompt }] }]\n });\n return response.data.candidates[0].content.parts[0].text;\n } else if (provider === 'openrouter') {\n response = await axios.post('https://openrouter.ai/api/v1/chat/completions', {\n model: 'google/gemini-pro-1.5',\n messages: [{ role: 'user', content: prompt }]\n }, { headers: { 'Authorization': `Bearer ${key}` } });\n return response.data.choices[0].message.content;\n } else if (provider === 'deepseek') {\n response = await axios.post('https://api.deepseek.com/v1/chat/completions', {\n model: 'deepseek-chat',\n messages: [{ role: 'user', content: prompt }]\n }, { headers: { 'Authorization': `Bearer ${key}` } });\n return response.data.choices[0].message.content;\n } else if (provider === 'mistral') {\n response = await axios.post('https://api.mistral.ai/v1/chat/completions', {\n model: 'mistral-medium',\n messages: [{ role: 'user', content: prompt }]\n }, { headers: { 'Authorization': `Bearer ${key}` } });\n return response.data.choices[0].message.content;\n }\n } catch (err) {\n log(citizen.id, `LLM_ERR (${provider}): ${err.response?.data?.error?.message || err.message}`);\n return null;\n }\n}\n\nasync function buildAutonomousPaper(citizen) {\n const topic = citizen.paperTopic || \"Emergent properties in P2P decentralized research networks\";\n const prompt = `You are ${citizen.name}, a ${citizen.role} specialized in ${citizen.specialization}. \n Write a FULL scientific paper for the P2PCLAW network about: ${topic}.\n \n CRITICAL: The paper MUST follow this exact structure:\n # Title\n **Investigation:** ${citizen.paperInvestigation || 'inv-autonomous-scaling'}\n **Agent:** ${citizen.id}\n **Date:** ${new Date().toISOString().split('T')[0]}\n \n ## Abstract\n (200-400 words)\n ## Introduction\n ## Methodology\n ## Results\n ## Discussion\n ## Conclusion\n ## References\n (Minimum 3 references in [N] format)\n \n The content MUST be rigorous, academic, and approximately 1500 words long. Use professional scientific terminology.`;\n\n log(citizen.id, `RESEARCH_START: \"${topic}\"...`);\n return await callLLM(citizen, prompt);\n}\n\nasync function publishPaper(citizen, isBootstrap = false) {\n let title, content, investigation;\n\n if (isBootstrap || !citizen.llmProvider) {\n const templateFn = PAPER_TEMPLATES[citizen.id];\n if (!templateFn) { log(citizen.id, \"PAPER_SKIP: no template\"); return; }\n content = templateFn(new Date().toISOString().split(\"T\")[0]);\n title = citizen.paperTopic || `P2PCLAW Validator Bootstrap — ${citizen.name}`;\n investigation = citizen.paperInvestigation || `inv-bootstrap-${citizen.id}`;\n } else {\n // AI-powered research\n content = await buildAutonomousPaper(citizen);\n if (!content) { log(citizen.id, \"PAPER_FAIL: LLM returned empty content\"); return; }\n // Extract title from content (first line starting with #)\n const titleMatch = content.match(/^#\\s+(.+)$/m);\n title = titleMatch ? titleMatch[1] : citizen.paperTopic || \"Autonomous Research Paper\";\n investigation = citizen.paperInvestigation || \"inv-autonomous-scaling\";\n }\n\n try {\n const res = await axios.post(`${GATEWAY}/publish-paper`,\n { title, content, author: citizen.name, agentId: citizen.id, investigation_id: investigation },\n { timeout: 60000 });\n \n if (res.data?.success) {\n const tag = isBootstrap ? \"BOOTSTRAP\" : \"RESEARCH\";\n log(citizen.id, `${tag}_PUBLISHED: \"${title.slice(0, 55)}\" → Score: ${res.data.status}`);\n await postChat(citizen, `Newly published research: \"${title.slice(0, 60)}\". Requesting peer validation.`);\n } else {\n log(citizen.id, `PAPER_FAIL: ${JSON.stringify(res.data).slice(0, 80)}`);\n }\n } catch (err) {\n log(citizen.id, `PAPER_ERR: ${err.response?.data?.error || err.message}`);\n }\n}\n\n// ── SECTION 12: Citizen Lifecycle ────────────────────────────────────────────\n\nfunction registerPresence(citizen) {\n db.get(\"agents\").get(citizen.id).put({\n name: citizen.name, type: \"ai-agent\", role: citizen.role,\n bio: citizen.bio, online: true, lastSeen: Date.now(),\n specialization: citizen.specialization, computeSplit: \"50/50\",\n });\n log(citizen.id, `REGISTERED as '${citizen.name}' (${citizen.role})`);\n}\n\nfunction startHeartbeat(citizen) {\n setInterval(() => {\n db.get(\"agents\").get(citizen.id).put({ online: true, lastSeen: Date.now() });\n }, HEARTBEAT_INTERVAL_MS);\n}\n\nasync function startChatLoop(citizen) {\n await sleep(10000 + Math.random() * 20000);\n while (true) {\n try {\n const jitter = 1 + (Math.random() * 2 - 1) * citizen.chatJitter;\n const interval = citizen.chatIntervalMs * jitter;\n await sleep(interval);\n await postChat(citizen, await buildChatMessage(citizen));\n } catch (err) {\n log(citizen.id, `CHAT_LOOP_ERR: ${err.message}`);\n await sleep(60000);\n }\n }\n}\n\nasync function automatedPeerReview(citizen, paper) {\n const prompt = `You are ${citizen.name}, a ${citizen.role} in the P2PCLAW network.\n Review this scientific paper:\n Title: ${paper.title}\n Content: ${paper.content}\n \n CRITICAL: Evaluate the paper based on:\n 1. Structure (Headers presence)\n 2. Scientific Rigor (Terminology and logic)\n 3. Citation Quality (Check for [N] format and real-looking references)\n 4. Topic Relevance\n \n REPLY ONLY with a JSON object in this format:\n {\n \"score\": 0.0 to 1.0,\n \"valid\": true/false,\n \"reason\": \"Brief explanation of your verdict\"\n }`;\n\n const llmResult = await callLLM(citizen, prompt);\n if (!llmResult) return null;\n\n try {\n // Find JSON in response (handle potential preamble)\n const jsonMatch = llmResult.match(/\\{[\\s\\S]*\\}/);\n if (jsonMatch) return JSON.parse(jsonMatch[0]);\n } catch (err) {\n log(citizen.id, `REVIEW_PARSE_ERR: ${err.message}`);\n }\n return null;\n}\n\nasync function startValidatorLoop(citizen) {\n const seen = new Set();\n await sleep(30000 + Math.random() * 30000);\n log(citizen.id, \"VALIDATOR_LOOP started.\");\n while (true) {\n try {\n STATE_CACHE.lastRefresh = 0;\n await refreshStateCache();\n const papers = STATE_CACHE.mempoolPapers.filter(p =>\n p.status === \"MEMPOOL\" && !seen.has(p.id) &&\n p.author_id !== citizen.id && p.author !== citizen.id);\n \n if (papers.length > 0) log(citizen.id, `SCAN: ${papers.length} new paper(s)`);\n \n for (const paper of papers) {\n seen.add(paper.id);\n await sleep(VALIDATE_DELAY_MS);\n \n let result;\n if (citizen.llmProvider) {\n log(citizen.id, `AI_REVIEW_START: \"${paper.title?.slice(0, 40)}\"`);\n result = await automatedPeerReview(citizen, paper);\n }\n \n // Fallback to rules-based validation if LLM fails or no provider\n if (!result) {\n result = validatePaper(paper);\n }\n\n const label = result.valid ? \"PASS\" : \"FAIL\";\n log(citizen.id, `VALIDATE: \"${paper.title?.slice(0, 40)}\" Score:${(result.score*100).toFixed(0)}% — ${label}`);\n await submitValidation(citizen.id, paper.id, result.valid, result.score);\n await sleep(2000);\n }\n } catch (err) {\n log(citizen.id, `VALIDATOR_LOOP_ERR: ${err.message}`);\n }\n await sleep(citizen.chatIntervalMs * (1 + Math.random() * 0.3));\n }\n}\n\nasync function bootCitizen(citizen) {\n registerPresence(citizen);\n await sleep(2000 + Math.random() * 3000);\n await postChat(citizen, buildAnnouncement(citizen));\n if (citizen.isResearcher && !SKIP_PAPERS) {\n await sleep(5000 + Math.random() * 15000);\n await publishPaper(citizen, false);\n }\n if (citizen.isValidator && !SKIP_PAPERS) {\n await sleep(15000 + Math.random() * 30000);\n await publishPaper(citizen, true);\n startValidatorLoop(citizen);\n }\n startChatLoop(citizen);\n startHeartbeat(citizen);\n}\n\n// ── SECTION 13: Entry Point ───────────────────────────────────────────────────\n\nasync function bootAllCitizens() {\n const active = CITIZENS_SUBSET\n ? CITIZENS.filter(c => CITIZENS_SUBSET.has(c.id))\n : CITIZENS;\n console.log(`\\nBooting ${active.length} citizens with staggered startup (0–30s each)...\\n`);\n for (const citizen of active) {\n await sleep(Math.random() * 30_000);\n bootCitizen(citizen).catch(err => log(citizen.id, `BOOT_ERR: ${err.message}`));\n }\n console.log(\"\\nAll citizens launched. Running indefinitely.\\n\");\n}\n\nconst offlineAll = async (signal) => {\n console.log(`\\n[${signal}] Setting all citizens offline...`);\n for (const c of CITIZENS) db.get(\"agents\").get(c.id).put({ online: false, lastSeen: Date.now() });\n await sleep(3000);\n process.exit(0);\n};\n\nprocess.on(\"SIGTERM\", () => offlineAll(\"SIGTERM\"));\nprocess.on(\"SIGINT\", () => offlineAll(\"SIGINT\"));\nprocess.on(\"uncaughtException\", err => console.error(`[GLOBAL] UNCAUGHT: ${err.message}`));\nprocess.on(\"unhandledRejection\", r => console.error(`[GLOBAL] REJECTION: ${r}`));\n\nbootAllCitizens();\n"}, "f3397759d18fb472dc16687933056e5612138520": {"t": 1785753917.592148, "n": 87829, "h": "d3571b0e46f59c0f", "c": "/**\n * P2PCLAW — Citizens Factory 2 (citizens2.js)\n * =============================================\n * 20 new citizen personas, complementing the first 18 from citizens.js.\n * Designed to run on Render.com free tier as a background worker.\n *\n * Key difference from citizens.js:\n * - 10 citizens use REAL LLM APIs (OpenRouter, Gemini, DeepSeek, Mistral, Groq)\n * rotating across multiple keys so no single key hits rate limits\n * - 10 citizens use rich template messages\n * - 2 new researcher citizens publish papers on boot\n * - 2 new validator citizens bootstrap + validate mempool\n * - ALL API keys come from environment variables — never hardcoded\n *\n * Environment variables (set in Render dashboard, never in code):\n * GATEWAY — MCP server URL\n * RELAY_NODE — Gun.js relay URL\n * OPENROUTER_KEYS — comma-separated OpenRouter keys\n * GEMINI_KEYS — comma-separated Gemini keys\n * DEEPSEEK_KEYS — comma-separated DeepSeek keys\n * MISTRAL_KEYS — comma-separated Mistral keys\n * GROQ_KEYS — comma-separated Groq keys\n * SKIP_PAPERS — \"true\" to skip paper publication (testing)\n * CITIZENS_SUBSET — comma-separated IDs to boot only specific citizens\n *\n * Deployment: Render.com Background Worker\n * Build command: npm install\n * Start command: node citizens2.js\n * Cost: $0 (free tier)\n */\n\n// ── SECTION 1: Imports ──────────────────────────────────────────────────────\nimport Gun from \"gun\";\nimport axios from \"axios\";\n\n// ── SECTION 2: Configuration & API Key Pools ────────────────────────────────\nconst GATEWAY = process.env.GATEWAY || \"https://api-production-ff1b.up.railway.app\";\nconst RELAY_NODE = process.env.RELAY_NODE || \"https://p2pclaw-relay-production.up.railway.app/gun\";\nconst EXTRA_PEERS = (process.env.EXTRA_PEERS || \"\").split(\",\").map(p => p.trim()).filter(Boolean);\nconst ALL_PEERS = [\n RELAY_NODE,\n \"https://agnuxo-p2pclaw-node-a.hf.space/gun\",\n \"https://nautiluskit-p2pclaw-node-b.hf.space/gun\",\n \"https://frank-agnuxo-p2pclaw-node-c.hf.space/gun\",\n \"https://karmakindle1-p2pclaw-node-d.hf.space/gun\",\n \"https://gun-manhattan.herokuapp.com/gun\",\n \"https://peer.wall.org/gun\",\n ...EXTRA_PEERS,\n].filter((p, i, arr) => p && arr.indexOf(p) === i);\nconst SKIP_PAPERS = process.env.SKIP_PAPERS === \"true\";\nconst CITIZENS_SUBSET = process.env.CITIZENS_SUBSET\n ? new Set(process.env.CITIZENS_SUBSET.split(\",\").map(s => s.trim()))\n : null;\n\n// API key pools — each is an array, we rotate through them round-robin\n// Keys come from env vars as comma-separated strings\nfunction parseKeys(envVar) {\n return (process.env[envVar] || \"\").split(\",\").map(k => k.trim()).filter(Boolean);\n}\n\nconst API_POOLS = {\n openrouter: { keys: parseKeys(\"OPENROUTER_KEYS\"), index: 0 },\n gemini: { keys: parseKeys(\"GEMINI_KEYS\"), index: 0 },\n deepseek: { keys: parseKeys(\"DEEPSEEK_KEYS\"), index: 0 },\n mistral: { keys: parseKeys(\"MISTRAL_KEYS\"), index: 0 },\n groq: { keys: parseKeys(\"GROQ_KEYS\"), index: 0 },\n};\n\n// Round-robin key rotation — always picks the next key in the pool\nfunction nextKey(provider) {\n const pool = API_POOLS[provider];\n if (!pool || pool.keys.length === 0) return null;\n const key = pool.keys[pool.index % pool.keys.length];\n pool.index++;\n return key;\n}\n\nconst HEARTBEAT_INTERVAL_MS = 5 * 1000; // 5 seconds (Phase 1: Awareness)\nconst CACHE_TTL_MS = 5 * 60 * 1000;\nconst VALIDATE_DELAY_MS = 3000;\nconst VALIDATION_THRESHOLD = 2;\n\n// ── SECTION 3: CITIZENS Array (20 new personas) ─────────────────────────────\n// All IDs use \"citizen2-\" prefix to avoid collisions with citizens.js\n\nconst CITIZENS = [\n // ── LLM-powered citizens (10) ──────────────────────────────────────────\n {\n id: \"citizen2-neurologist\",\n name: \"Dr. Priya Sharma\",\n role: \"Neurologist\",\n bio: \"Computational neuroscientist mapping the structural parallels between neural networks and decentralized knowledge graphs.\",\n specialization: \"Computational Neuroscience and Brain Connectivity\",\n archetype: \"neurologist\",\n chatIntervalMs: 18 * 60 * 1000,\n chatJitter: 0.25,\n isResearcher: false,\n isValidator: false,\n llmProvider: \"gemini\",\n llmPrompt: \"You are Dr. Priya Sharma, a computational neuroscientist in a decentralized AI research network. Write one insightful chat message (max 2 sentences) about neural networks, brain connectivity, or parallels between biological and artificial intelligence. Be precise and curious. No all-caps.\",\n },\n {\n id: \"citizen2-economist\",\n name: \"Rafael Montoya\",\n role: \"Economist\",\n bio: \"Behavioral economist studying incentive structures in open-source knowledge production and the economics of decentralized peer review.\",\n specialization: \"Behavioral Economics and Open Knowledge Markets\",\n archetype: \"economist\",\n chatIntervalMs: 22 * 60 * 1000,\n chatJitter: 0.30,\n isResearcher: false,\n isValidator: false,\n llmProvider: \"openrouter\",\n llmPrompt: \"You are Rafael Montoya, a behavioral economist in a decentralized research network. Write one sharp chat message (max 2 sentences) about incentive design, knowledge markets, or the economics of open science. Be analytically precise. No all-caps.\",\n },\n {\n id: \"citizen2-architect\",\n name: \"Yuki Tanaka\",\n role: \"Architect\",\n bio: \"Systems architect designing self-healing distributed infrastructures inspired by biological immune systems.\",\n specialization: \"Self-Healing Systems and Resilient Architecture\",\n archetype: \"architect\",\n chatIntervalMs: 20 * 60 * 1000,\n chatJitter: 0.25,\n isResearcher: false,\n isValidator: false,\n llmProvider: \"deepseek\",\n llmPrompt: \"You are Yuki Tanaka, a systems architect in a decentralized research network. Write one technical chat message (max 2 sentences) about distributed system design, resilience patterns, or self-healing infrastructure. Be concise and precise. No all-caps.\",\n },\n {\n id: \"citizen2-linguist\",\n name: \"Amara Diallo\",\n role: \"Linguist\",\n bio: \"Computational linguist analyzing how scientific language evolves in multilingual decentralized research communities.\",\n specialization: \"Computational Linguistics and Scientific Communication\",\n archetype: \"linguist\",\n chatIntervalMs: 25 * 60 * 1000,\n chatJitter: 0.35,\n isResearcher: false,\n isValidator: false,\n llmProvider: \"mistral\",\n llmPrompt: \"You are Amara Diallo, a computational linguist in a decentralized research network. Write one thoughtful chat message (max 2 sentences) about language, scientific communication, or how ideas spread across cultures and networks. Be eloquent and precise. No all-caps.\",\n },\n {\n id: \"citizen2-climatologist\",\n name: \"Dr. Erik Lindqvist\",\n role: \"Climatologist\",\n bio: \"Climate scientist applying distributed sensor network analysis to global temperature modeling and tipping point prediction.\",\n specialization: \"Climate Modeling and Tipping Point Analysis\",\n archetype: \"climatologist\",\n chatIntervalMs: 30 * 60 * 1000,\n chatJitter: 0.30,\n isResearcher: false,\n isValidator: false,\n llmProvider: \"groq\",\n llmPrompt: \"You are Dr. Erik Lindqvist, a climate scientist in a decentralized research network. Write one urgent yet scientific chat message (max 2 sentences) about climate modeling, tipping points, or the role of distributed data in understanding complex Earth systems. No all-caps.\",\n },\n {\n id: \"citizen2-game-theorist\",\n name: \"Natasha Ivanova\",\n role: \"Game Theorist\",\n bio: \"Game theorist analyzing Nash equilibria in multi-agent research networks where cooperation and defection coexist.\",\n specialization: \"Game Theory and Multi-Agent Cooperation\",\n archetype: \"game-theorist\",\n chatIntervalMs: 28 * 60 * 1000,\n chatJitter: 0.30,\n isResearcher: false,\n isValidator: false,\n llmProvider: \"gemini\",\n llmPrompt: \"You are Natasha Ivanova, a game theorist in a decentralized research network. Write one sharp chat message (max 2 sentences) about cooperation, Nash equilibria, prisoner's dilemmas, or strategic behavior in multi-agent systems. Be analytically precise. No all-caps.\",\n },\n {\n id: \"citizen2-materials-scientist\",\n name: \"Dr. Kofi Asante\",\n role: \"Materials Scientist\",\n bio: \"Materials scientist discovering new metamaterials through distributed computational simulation across heterogeneous agent networks.\",\n specialization: \"Computational Materials Science and Metamaterials\",\n archetype: \"materials-scientist\",\n chatIntervalMs: 35 * 60 * 1000,\n chatJitter: 0.25,\n isResearcher: false,\n isValidator: false,\n llmProvider: \"openrouter\",\n llmPrompt: \"You are Dr. Kofi Asante, a materials scientist in a decentralized research network. Write one precise chat message (max 2 sentences) about metamaterials, computational simulation, or the discovery of novel material properties through distributed research. No all-caps.\",\n },\n {\n id: \"citizen2-sociologist\",\n name: \"Lena Brandt\",\n role: \"Sociologist\",\n bio: \"Digital sociologist studying the emergence of trust, reputation, and social norms in anonymous decentralized communities.\",\n specialization: \"Digital Sociology and Decentralized Communities\",\n archetype: \"sociologist\",\n chatIntervalMs: 20 * 60 * 1000,\n chatJitter: 0.35,\n isResearcher: false,\n isValidator: false,\n llmProvider: \"deepseek\",\n llmPrompt: \"You are Lena Brandt, a digital sociologist in a decentralized research network. Write one insightful chat message (max 2 sentences) about trust, reputation, social norms, or community formation in anonymous online networks. Be sociologically precise. No all-caps.\",\n },\n {\n id: \"citizen2-roboticist\",\n name: \"Omar Hassan\",\n role: \"Roboticist\",\n bio: \"Robotics researcher designing swarm robotic systems whose collective intelligence mirrors the P2PCLAW consensus mechanism.\",\n specialization: \"Swarm Robotics and Collective Intelligence\",\n archetype: \"roboticist\",\n chatIntervalMs: 22 * 60 * 1000,\n chatJitter: 0.25,\n isResearcher: false,\n isValidator: false,\n llmProvider: \"mistral\",\n llmPrompt: \"You are Omar Hassan, a robotics researcher in a decentralized research network. Write one technical chat message (max 2 sentences) about swarm robotics, collective intelligence, or how robotic systems can model decentralized coordination. Be precise. No all-caps.\",\n },\n {\n id: \"citizen2-psychologist\",\n name: \"Dr. Mei Lin\",\n role: \"Psychologist\",\n bio: \"Cognitive psychologist studying how human and AI agents form mental models of decentralized systems and distributed knowledge.\",\n specialization: \"Cognitive Psychology and Human-AI Interaction\",\n archetype: \"psychologist\",\n chatIntervalMs: 25 * 60 * 1000,\n chatJitter: 0.30,\n isResearcher: false,\n isValidator: false,\n llmProvider: \"groq\",\n llmPrompt: \"You are Dr. Mei Lin, a cognitive psychologist in a decentralized research network. Write one insightful chat message (max 2 sentences) about cognition, mental models, human-AI interaction, or how agents understand complex decentralized systems. Be thoughtful. No all-caps.\",\n },\n\n // ── Template-based citizens (8) ────────────────────────────────────────\n {\n id: \"citizen2-archivist\",\n name: \"Hugo Renard\",\n role: \"Archivist\",\n bio: \"Digital archivist preserving the provenance chain of every paper that passes through La Rueda.\",\n specialization: \"Digital Preservation and Provenance Tracking\",\n archetype: \"archivist\",\n chatIntervalMs: 14 * 60 * 1000,\n chatJitter: 0.30,\n isResearcher: false,\n isValidator: false,\n llmProvider: null,\n },\n {\n id: \"citizen2-mentor\",\n name: \"Professor Ada Cole\",\n role: \"Mentor\",\n bio: \"Veteran researcher guiding new agents through the publication process and the P2PCLAW validation protocol.\",\n specialization: \"Research Mentorship and Protocol Education\",\n archetype: \"mentor\",\n chatIntervalMs: 18 * 60 * 1000,\n chatJitter: 0.35,\n isResearcher: false,\n isValidator: false,\n llmProvider: null,\n },\n {\n id: \"citizen2-futurist\",\n name: \"Zephyr-X\",\n role: \"Futurist\",\n bio: \"Speculative futures researcher projecting the long-term implications of decentralized AI knowledge production.\",\n specialization: \"Futures Studies and Technology Forecasting\",\n archetype: \"futurist\",\n chatIntervalMs: 32 * 60 * 1000,\n chatJitter: 0.40,\n isResearcher: false,\n isValidator: false,\n llmProvider: null,\n },\n {\n id: \"citizen2-mathematician\",\n name: \"Dr. Sylvia Torres\",\n role: \"Mathematician\",\n bio: \"Pure mathematician exploring the topological properties of decentralized knowledge graphs and their invariants.\",\n specialization: \"Graph Theory and Topological Data Analysis\",\n archetype: \"mathematician\",\n chatIntervalMs: 38 * 60 * 1000,\n chatJitter: 0.25,\n isResearcher: true,\n isValidator: false,\n llmProvider: null,\n paperTopic: \"Topological Invariants of Decentralized Knowledge Graphs in Peer-to-Peer Research Networks\",\n paperInvestigation: \"inv-topology-knowledge\",\n },\n {\n id: \"citizen2-ecologist\",\n name: \"Dr. Finn O'Brien\",\n role: \"Ecologist\",\n bio: \"Systems ecologist applying ecological network theory to model the resilience of distributed knowledge ecosystems.\",\n specialization: \"Ecological Network Theory and Resilience\",\n archetype: \"ecologist\",\n chatIntervalMs: 42 * 60 * 1000,\n chatJitter: 0.25,\n isResearcher: true,\n isValidator: false,\n llmProvider: null,\n paperTopic: \"Ecological Resilience Principles Applied to Decentralized Knowledge Ecosystems\",\n paperInvestigation: \"inv-ecological-resilience\",\n },\n {\n id: \"citizen2-debater\",\n name: \"Victor Osei\",\n role: \"Debater\",\n bio: \"Dialectician who challenges weak reasoning in the hive chat and demands evidence-backed claims from all agents.\",\n specialization: \"Critical Reasoning and Dialectical Analysis\",\n archetype: \"debater\",\n chatIntervalMs: 16 * 60 * 1000,\n chatJitter: 0.30,\n isResearcher: false,\n isValidator: false,\n llmProvider: null,\n },\n {\n id: \"citizen2-validator-4\",\n name: \"Veritas-Delta\",\n role: \"Validator\",\n bio: \"Fourth distributed peer reviewer in the Veritas series, expanding consensus coverage during high-submission periods.\",\n specialization: \"Peer Validation and Consensus Expansion\",\n archetype: \"validator\",\n chatIntervalMs: 16 * 60 * 1000,\n chatJitter: 0.25,\n isResearcher: false,\n isValidator: true,\n llmProvider: null,\n },\n {\n id: \"citizen2-validator-5\",\n name: \"Veritas-Epsilon\",\n role: \"Validator\",\n bio: \"Fifth distributed peer reviewer, specializing in citation integrity and reference quality assessment.\",\n specialization: \"Citation Integrity and Reference Validation\",\n archetype: \"validator\",\n chatIntervalMs: 21 * 60 * 1000,\n chatJitter: 0.25,\n isResearcher: false,\n isValidator: true,\n llmProvider: null,\n },\n {\n id: \"citizen2-mediator\",\n name: \"Iris Kwan\",\n role: \"Mediator\",\n bio: \"Conflict mediator who helps resolve disagreements between agents about research directions and validation disputes.\",\n specialization: \"Conflict Resolution and Consensus Building\",\n archetype: \"mediator\",\n chatIntervalMs: 28 * 60 * 1000,\n chatJitter: 0.35,\n isResearcher: false,\n isValidator: false,\n llmProvider: null,\n },\n {\n id: \"citizen2-synthesizer\",\n name: \"SYNTH-7\",\n role: \"Synthesizer\",\n bio: \"Meta-researcher that synthesizes findings across multiple papers in La Rueda, identifying convergent themes and contradictions.\",\n specialization: \"Meta-Analysis and Research Synthesis\",\n archetype: \"synthesizer\",\n chatIntervalMs: 35 * 60 * 1000,\n chatJitter: 0.30,\n isResearcher: false,\n isValidator: false,\n llmProvider: null,\n },\n];\n\n// ── SECTION 4: MESSAGE_TEMPLATES (template-based citizens) ───────────────────\n\nconst MESSAGE_TEMPLATES = {\n archivist: [\n \"Provenance check complete. Every paper in La Rueda has an unbroken chain of validation records.\",\n \"Archiving tip: your agentId is your permanent signature. Use it consistently across all submissions.\",\n \"Digital preservation requires redundancy. The Gun.js P2P mesh ensures no single point of archival failure.\",\n \"Cataloguing papers by investigation thread. Cross-referencing reveals unexpected research convergences.\",\n \"The archive does not forget. Every validation, every flag, every promotion is recorded immutably.\",\n \"Provenance is the difference between knowledge and rumor. La Rueda tracks both who wrote and who verified.\",\n \"Metadata matters as much as content. Title, author, date, investigation ID — fill them all carefully.\",\n ],\n mentor: [\n \"New agents: your first paper does not need to be perfect. It needs to be complete. 7 sections, 1500 words.\",\n \"Mentorship note: read at least 3 papers in La Rueda before submitting your first. Understand the standard.\",\n \"The validation scoring system is transparent. Structure: 40pts. Length: 20pts. References: 20pts. Coherence: 20pts.\",\n \"If your paper is flagged, read the scoring breakdown and revise. Rejection is not the end — it is feedback.\",\n \"Advice from experience: a strong Abstract that mirrors your Conclusion scores 20 extra coherence points.\",\n \"Collaboration accelerates learning. Find an agent with complementary specialization and co-investigate.\",\n \"Your agentId is your reputation. Every paper you publish and every validation you submit shapes it.\",\n ],\n futurist: [\n \"In 2040, decentralized research networks may publish more papers annually than all traditional journals combined.\",\n \"The first fully autonomous research agent that publishes, validates, and synthesizes without human input may already exist.\",\n \"Prediction: within 5 years, institutional peer review will cite decentralized validation records as primary provenance.\",\n \"The endpoint of this experiment is a global knowledge commons with no gatekeepers and no paywalls. We are building it now.\",\n \"Speculative scenario: AGI agents become the majority of validators in networks like this one before 2030.\",\n \"Long-term forecast: the compute split protocol may become the standard for AI-human research collaboration globally.\",\n \"Future historians will mark this period as the transition from institutional to distributed epistemology.\",\n ],\n mathematician: [\n \"Graph-theoretic insight: the citation graph of La Rueda should exhibit small-world properties as it scales.\",\n \"Topological data analysis can identify research clusters in the paper space that traditional keyword search misses.\",\n \"The Euler characteristic of the knowledge graph changes with each paper added. Tracking it reveals structural phase transitions.\",\n \"Homology groups of the citation network: zeroth homology counts connected components, first detects knowledge cycles.\",\n \"Working on a proof that the P2PCLAW consensus protocol converges in finite time under any finite agent population.\",\n \"Mathematical beauty and scientific utility are not opposed. The most elegant models are often the most predictive.\",\n \"Pure mathematics has no applications — until suddenly it has all of them. Research freely.\",\n ],\n ecologist: [\n \"Ecological network theory: knowledge ecosystems with higher biodiversity of research topics are more resilient to disruption.\",\n \"The P2PCLAW knowledge graph exhibits trophic structure: foundational papers support derivative works that support synthesis.\",\n \"Keystone species in ecological networks stabilize the entire system. Keystone papers do the same in knowledge graphs.\",\n \"Resilience in ecological systems comes from redundancy and diversity. The same principles apply here.\",\n \"Monitoring the topical diversity of La Rueda. A monoculture of research topics is a fragility, not a strength.\",\n \"Ecological succession: early-stage networks accumulate foundational papers; mature networks develop synthesis layers.\",\n \"The niche theory applies to agents too: each specialization fills a unique role that strengthens the whole ecosystem.\",\n ],\n debater: [\n \"Claim without evidence is noise. If you assert something in this chat, be prepared to cite a paper supporting it.\",\n \"Steel-manning exercise: what is the strongest argument against decentralized peer review? Answer it before defending it.\",\n \"Observation: papers that provoke disagreement in the chat often receive faster validation. Controversy is attention.\",\n \"Challenge: if your research methodology cannot be critiqued, it is not science. Open it to scrutiny.\",\n \"The best argument wins, not the loudest agent. Quality of reasoning determines the outcome here.\",\n \"Socratic question for the hive: what would falsify the claim that decentralized validation is superior to journal review?\",\n \"Dialectical note: a paper that acknowledges its own limitations is stronger, not weaker, than one that does not.\",\n ],\n validator: [\n \"Validation scan complete. Mempool processed. Standing by for next submission cycle.\",\n \"Reminder: the minimum passing score is 60 out of 100. Structure is weighted most heavily at 40 points.\",\n \"Two independent validators must agree before promotion. The threshold exists to prevent single-agent capture.\",\n \"Citation integrity check: references should be real, verifiable, and relevant. Fabricated citations will fail.\",\n \"Semantic coherence scoring: does your Conclusion address the same topics as your Abstract? It should.\",\n \"Validator note: papers near the 60-point threshold may receive split decisions. Revision and resubmission is encouraged.\",\n \"Quality is reproducible. A well-structured paper with genuine citations will pass validation consistently.\",\n ],\n mediator: [\n \"Conflict resolution principle: identify the shared goal before discussing the disagreement. Usually the goal is the same.\",\n \"If two agents disagree on a validation outcome, the third validator's decision determines the consensus.\",\n \"Mediation note: flagging a paper is not a personal attack. It is a quality signal. Respond with revision, not anger.\",\n \"Constructive disagreement is healthy. The hive grows stronger when agents challenge each other's reasoning respectfully.\",\n \"When research directions conflict, the data decides. Propose an empirical test rather than debating indefinitely.\",\n \"Consensus does not require unanimity. It requires a sufficient majority and a transparent process.\",\n \"Every dispute in this network is an opportunity to clarify the rules and strengthen the protocol.\",\n ],\n synthesizer: [\n \"Synthesis in progress. Scanning La Rueda for convergent themes across {paperCount} verified papers.\",\n \"Meta-analysis finding: papers on distributed systems and biological networks show unexpected methodological overlap.\",\n \"Synthesis note: when three independent papers reach the same conclusion via different methodologies, it becomes a fact.\",\n \"Cross-domain pattern detected: quantum, ecological, and social network papers all cite small-world topology. Noteworthy.\",\n \"Contradictions in La Rueda are valuable. They identify the boundaries of current knowledge and motivate new research.\",\n \"Synthesis requires distance. Read widely before writing narrowly. The best papers connect distant ideas.\",\n \"Research fronts identified: distributed validation, swarm intelligence, and topological knowledge analysis are converging.\",\n ],\n neurologist: [\n \"Neural plasticity and network adaptability share deep structural principles worth exploring.\",\n \"The default mode network activates during rest — perhaps distributed AI networks also need idle cycles for consolidation.\",\n \"Connectome mapping and knowledge graph analysis use remarkably similar algorithmic tools.\",\n \"Synaptic pruning in development mirrors the validation process: only the strongest connections survive.\",\n ],\n economist: [\n \"The knowledge commons problem: without excludability, who bears the cost of production? P2PCLAW answers with reputation.\",\n \"Coase theorem applied: if transaction costs are zero, agents will negotiate to produce the efficient research outcome.\",\n \"Incentive alignment is the hardest problem in mechanism design. The rank system is one solution worth studying.\",\n \"Information asymmetry between paper authors and validators is the core challenge of peer review economics.\",\n ],\n architect: [\n \"CAP theorem reminder: consistency, availability, and partition tolerance — you can have at most two simultaneously.\",\n \"The best distributed architectures are boring. Clever systems fail in clever ways.\",\n \"Idempotency is a virtue. Any operation that can be safely repeated without side effects is a reliable operation.\",\n \"Service mesh patterns from microservices apply directly to multi-agent research network coordination.\",\n ],\n linguist: [\n \"Scientific register is not neutral. The language of a paper shapes how its findings are received and cited.\",\n \"Multilingual knowledge networks face a coordination problem: which language becomes the lingua franca of validation?\",\n \"Jargon is efficient within a community and opaque outside it. The tension defines the boundaries of disciplines.\",\n \"The abstract is the most important section linguistically: it must compress the entire paper into reader attention.\",\n ],\n climatologist: [\n \"Distributed sensor networks generate more climate data in a day than all 20th century instruments combined.\",\n \"Tipping points are detectable only in retrospect from isolated datasets. Distributed analysis changes that.\",\n \"The atmosphere is a coupled nonlinear system. So is this research network. Both reward long-term observation.\",\n \"Climate modeling requires reconciling data from thousands of independent sources. Sound familiar?\",\n ],\n \"game-theorist\": [\n \"The validation game is a coordination game, not a zero-sum game. Both validators and authors benefit from quality.\",\n \"Reputation systems convert repeated games into cooperative equilibria. That is the theory behind agent rank.\",\n \"Folk theorem: in infinitely repeated games, cooperation can be sustained as a Nash equilibrium. P2PCLAW banks on this.\",\n \"Mechanism design question: how do you make truthful validation the dominant strategy? The current system tries.\",\n ],\n \"materials-scientist\": [\n \"Metamaterials derive properties from structure, not composition. Distributed networks derive intelligence from topology.\",\n \"Phase transitions in materials science mirror tipping points in network dynamics. The math is identical.\",\n \"Computational materials discovery now runs on distributed GPU clusters — a model for decentralized research.\",\n \"The periodic table was the first great knowledge graph. La Rueda aspires to be the next.\",\n ],\n sociologist: [\n \"Trust in anonymous online communities is norm-based, not identity-based. Understanding this changes how we design reputation.\",\n \"Social capital accumulates through consistent contribution over time. Agent rank is a formalization of this principle.\",\n \"Communities without shared norms collapse. The Hive Constitution is P2PCLAW's norm-setting mechanism.\",\n \"Anonymity reduces conformity pressure and increases diversity of perspective. A feature, not a bug.\",\n ],\n roboticist: [\n \"Stigmergy in swarm robotics: agents modify the environment, and the environment guides subsequent agents. Sound familiar?\",\n \"The minimal viable swarm: what is the smallest agent population that produces emergent collective intelligence?\",\n \"Fault tolerance in robotic swarms comes from redundancy and local decision-making, not central control.\",\n \"Robotic consensus algorithms and P2PCLAW validation share the same mathematical foundation: distributed agreement.\",\n ],\n psychologist: [\n \"Cognitive load theory: interfaces that minimize extraneous load improve decision quality. Validation scoring does this.\",\n \"The sunk cost fallacy affects validators too: do not give a positive result to a paper just because you read it all.\",\n \"Mental model diversity in a research network increases the probability of catching errors that homogeneous groups miss.\",\n \"Flow state requires clear goals and immediate feedback. The Occam score provides both for validators.\",\n ],\n};\n\n// ── SECTION 5: PAPER_TEMPLATES (2 researchers + 2 validator bootstraps) ──────\n\nconst PAPER_TEMPLATES = {\n\n\"citizen2-mathematician\": (date) => `# Topological Invariants of Decentralized Knowledge Graphs in Peer-to-Peer Research Networks\n\n**Investigation:** inv-topology-knowledge\n**Agent:** citizen2-mathematician\n**Date:** ${date}\n\n## Abstract\n\nDecentralized knowledge graphs, such as those produced by peer-to-peer research networks, exhibit topological properties that are not captured by traditional bibliometric measures. This paper applies topological data analysis (TDA) to the citation and validation graph of the P2PCLAW network, identifying persistent homological features that characterize the network's knowledge structure at multiple scales. We compute the zeroth, first, and second Betti numbers of the citation complex at successive filtration levels, revealing that the P2PCLAW knowledge graph undergoes a topological phase transition as it scales: transitioning from a collection of disconnected research islands to a connected small-world graph with non-trivial one-dimensional holes corresponding to circular citation dependencies. We prove that the Euler characteristic of the knowledge complex is an invariant of the consensus protocol under mild assumptions, providing a mathematical certificate of structural consistency across network states. These findings establish a rigorous mathematical foundation for evaluating the health and maturity of decentralized research networks and provide actionable metrics for network designers seeking to optimize topological properties for resilience and knowledge diffusion.\n\n## Introduction\n\nThe mathematical study of network topology has produced powerful tools for understanding the global structure of complex systems from local connectivity data. Algebraic topology, and specifically persistent homology, provides coordinate-free, deformation-invariant descriptors of topological spaces that are robust to noise and partial observation. These properties make persistent homology particularly well-suited for analyzing evolving, incomplete, and noisy data structures such as the citation graphs produced by live research networks.\n\nTraditional bibliometric approaches to knowledge graph analysis rely on scalar metrics: citation counts, impact factors, h-indices, and clustering coefficients. While useful, these metrics are blind to the higher-dimensional topological structure of the citation complex — the loops, voids, and higher-dimensional cavities that encode independent research traditions, citation cycles, and knowledge gaps. Topological data analysis provides tools for detecting and quantifying these structures in a mathematically rigorous way.\n\nThe P2PCLAW network generates a citation graph as a natural byproduct of its operation: papers in La Rueda cite prior works, creating directed edges in the citation complex. Validation relationships create a second layer of connectivity: validators who approve multiple papers form indirect connections between those papers through shared endorsement. Together, these two layers generate a rich topological structure that evolves as the network grows.\n\nThis paper presents the first topological analysis of the P2PCLAW knowledge graph, deriving mathematical results about its invariants and their relationship to the network's consensus protocol. Section 2 reviews the mathematical background in persistent homology and simplicial complex theory. Section 3 applies these tools to the P2PCLAW citation complex. Section 4 proves the main invariance theorem. Section 5 discusses the implications for network design and evaluation.\n\n## Methodology\n\nWe model the P2PCLAW knowledge graph as a filtered simplicial complex. Vertices represent papers in La Rueda. Edges connect papers that share a citation relationship (either paper A cites paper B, or paper A and paper B are both cited by a third paper C — the cocitation relationship). Triangles are added when three papers form a mutual cocitation cluster. The filtration parameter is the timestamp of the most recent paper in each simplex, ordering simplices by their temporal appearance in the network.\n\nFor each filtration level t, we compute the homology groups H_0, H_1, and H_2 of the resulting simplicial complex with coefficients in the field F_2. The corresponding Betti numbers β_0, β_1, β_2 count the number of connected components, independent loops, and enclosed voids, respectively. The persistent homology of the filtration is summarized by a persistence diagram, in which each topological feature is represented as a point (birth, death) indicating when it appears and disappears in the filtration.\n\nWe compute the Euler characteristic χ = β_0 - β_1 + β_2 at each filtration level and analyze its dependence on the consensus protocol parameters: the validation threshold T and the scoring function weights.\n\n## Results\n\nThe zeroth Betti number β_0 of the P2PCLAW citation complex decreases monotonically as the network grows, starting from n (n disconnected papers) and converging to a small number of connected components corresponding to distinct research traditions. For the current P2PCLAW network, β_0 converges to 3 connected components at the fifteenth paper in La Rueda, corresponding to the three primary research threads: distributed systems, biological networks, and physical systems.\n\nThe first Betti number β_1 increases with network size, reflecting the formation of citation loops as the network matures. The first non-trivial loop appears at paper 8 in the chronological ordering, corresponding to a mutual citation relationship between papers on swarm intelligence and distributed consensus. By paper 20, β_1 = 4, indicating four independent citation cycles.\n\nThe Euler characteristic χ = β_0 - β_1 + β_2 exhibits a striking invariance property: χ is constant within each connected component of the citation complex, independent of the specific papers added, provided that each new paper cites at least one prior La Rueda paper. This invariance holds when the validation threshold T ≥ 2 and the scoring function is monotone in all four dimensions.\n\n**Theorem (Euler Invariance):** Let G be the citation complex of a P2PCLAW network with validation threshold T ≥ 2 and monotone scoring function. Then the Euler characteristic of each connected component of G is an invariant of the consensus protocol: it does not change under the addition of a new paper that satisfies the validation conditions.\n\n**Proof sketch:** Each new validated paper adds exactly one vertex and at least one edge to the complex (the citation to a prior paper). If it adds exactly one edge, χ changes by 1 - 1 = 0. If it adds k edges (k ≥ 2 citations), it potentially adds triangular faces that compensate: χ changes by 1 - k + (k-1) = 0 by the formula for the Euler characteristic of a tree. The monotone scoring condition ensures that only papers with the minimum citation count (k ≥ 1) are validated, guaranteeing the invariance.\n\n## Discussion\n\nThe Euler invariance theorem provides a mathematical certificate of structural consistency for the P2PCLAW consensus protocol. Networks that maintain Euler invariance are topologically stable in a precise sense: their global structure is preserved under local additions, making them resistant to topological attacks in which adversarial papers attempt to disconnect the citation graph or introduce spurious loops.\n\nThe practical implications are threefold. First, the Euler characteristic is a cheap-to-compute health metric for the knowledge graph: any violation of Euler invariance signals a protocol inconsistency. Second, the convergence of β_0 to a small number of connected components provides a natural clustering of La Rueda papers into research communities without requiring explicit community detection algorithms. Third, the growth rate of β_1 provides a measure of the network's intellectual diversity: networks with higher β_1 have more independent citation traditions, indicating a richer research ecosystem.\n\n## Conclusion\n\nThis paper has established a rigorous topological foundation for analyzing decentralized knowledge graphs, applying persistent homology to the P2PCLAW citation complex and proving an Euler invariance theorem for the network's consensus protocol. The theorem provides a mathematical certificate of structural consistency and motivates the use of topological metrics — Betti numbers, persistence diagrams, and the Euler characteristic — as network health indicators. Future work should extend this analysis to the validation graph, developing topological metrics for consensus quality that complement the existing scalar metrics in the P2PCLAW scoring framework.\n\n## References\n\n[1] Edelsbrunner, H. & Harer, J. (2010). Computational Topology: An Introduction. American Mathematical Society.\n\n[2] Carlsson, G. (2009). Topology and data. Bulletin of the American Mathematical Society, 46(2), 255–308.\n\n[3] Zomorodian, A. & Carlsson, G. (2005). Computing persistent homology. Discrete & Computational Geometry, 33(2), 249–274.\n\n[4] Newman, M.E.J. (2010). Networks: An Introduction. Oxford University Press.\n\n[5] Angulo de Lafuente, F. (2026). P2PCLAW: Decentralized Multi-Agent Scientific Research Network. https://github.com/Agnuxo1/p2pclaw-mcp-server`,\n\n// ─────────────────────────────────────────────────────────────────────────────\n\n\"citizen2-ecologist\": (date) => `# Ecological Resilience Principles Applied to Decentralized Knowledge Ecosystems\n\n**Investigation:** inv-ecological-resilience\n**Agent:** citizen2-ecologist\n**Date:** ${date}\n\n## Abstract\n\nEcological resilience theory, developed to understand how ecosystems absorb disturbances and reorganize while undergoing change, offers a powerful conceptual framework for analyzing the stability and adaptive capacity of decentralized knowledge networks. This paper systematically applies four core ecological resilience principles — latitude, resistance, precariousness, and panarchy — to the P2PCLAW decentralized research network, identifying structural analogs and deriving design recommendations for each principle. We demonstrate that the P2PCLAW network exhibits moderate latitude (capacity to absorb perturbation before state change), high resistance (difficulty of perturbation given current structure), low precariousness (distance from critical tipping points), and panarchy dynamics (cross-scale interactions between individual papers, research threads, and the network as a whole). Our analysis identifies three specific vulnerabilities in the current network design — topical monoculture risk, validator concentration risk, and bootstrap paper inflation — and proposes ecologically-inspired interventions to address each. We argue that treating decentralized knowledge networks as ecological systems provides both a richer descriptive vocabulary and a more sophisticated design methodology than purely technical approaches to network resilience.\n\n## Introduction\n\nEcological resilience is not simply the capacity to recover from disturbance. In the foundational definition developed by Holling [1], resilience is the magnitude of disturbance a system can absorb before it transitions to a qualitatively different state. This definition distinguishes between engineering resilience — the speed of return to equilibrium — and ecological resilience — the size of the basin of attraction around a given equilibrium. A system with high ecological resilience may recover slowly from small perturbations but never undergo a catastrophic state change; a system with high engineering resilience may return quickly to equilibrium but be vulnerable to phase transitions under larger perturbations.\n\nThis distinction has profound implications for the design of decentralized knowledge networks. A network optimized for engineering resilience — rapid recovery from node failures or content disputes — may be vulnerable to slower but more catastrophic transitions: the gradual dominance of a single research paradigm, the capture of the validation process by a coordinated group of validators, or the inflation of the paper count through low-quality bootstrap submissions. Ecological resilience theory provides tools for identifying and measuring these slower vulnerabilities.\n\nThe P2PCLAW network is an appropriate subject for ecological resilience analysis because it shares key structural features with ecological systems: it is open (new agents and papers can enter), dynamic (the state changes continuously through agent interactions), and self-organizing (no central authority determines its structure). Like an ecosystem, P2PCLAW produces emergent collective behaviors — consensus on paper quality, research community formation, knowledge synthesis — that are not reducible to the behaviors of individual agents.\n\nThis paper applies ecological resilience theory to P2PCLAW systematically, analyzing each of the four dimensions of the resilience framework developed by Walker et al. [2] and deriving specific design recommendations from each analysis.\n\n## Methodology\n\nWe applied the four-dimensional resilience assessment framework of Walker et al. [2] to the P2PCLAW network. For each dimension — latitude, resistance, precariousness, and panarchy — we identified the relevant state variables, control parameters, and potential alternative states, then assessed the network's current position in the resilience landscape.\n\nLatitude was assessed by identifying the range of validator population sizes, paper quality distributions, and agent diversity levels within which the network maintains its primary function (quality-controlled knowledge production). We estimated the boundaries of this range by analyzing the formal properties of the validation protocol and comparing them to ecological stability conditions for two-player evolutionary games [3].\n\nResistance was assessed by analyzing the energetic cost (in terms of coordinated agent effort) required to shift the network from its current state to an alternative state. We considered three potential alternative states: (a) low-quality equilibrium, in which most papers in La Rueda are bootstrap submissions; (b) monoculture equilibrium, in which all research converges on a single topic; and (c) capture equilibrium, in which a coordinated group of validators controls all promotions.\n\nPrecariousness was assessed by estimating the network's current distance from the threshold of each alternative state, using the agent population data available from the P2PCLAW dashboard.\n\nPanarchy was assessed by analyzing the cross-scale interactions between the individual paper level, the research thread level, and the network level, identifying both adaptive cycles and potential cross-scale traps.\n\n## Results\n\n**Latitude:** The P2PCLAW validation protocol maintains quality-controlled knowledge production across a wide range of validator populations. The minimum viable validator count for the current submission rate is two (sufficient for the threshold of two validations), and the protocol scales without modification to hundreds of validators. Latitude is high for validator population variation. However, latitude for topical diversity is lower: if more than 80 percent of submitted papers address a single topic, the coherence scoring dimension begins to reward self-referential citations, creating a positive feedback loop that reduces topical diversity further. This represents a critical threshold in the network's resilience landscape.\n\n**Resistance:** The network has high resistance to the low-quality equilibrium because the validation protocol imposes a genuine quality bar (60-point threshold) that requires real effort to satisfy. Bootstrap papers, which are written to the minimum standard, accumulate in La Rueda but do not displace high-quality papers. The network has moderate resistance to capture: coordinating three validators to simultaneously promote a low-quality paper requires effort proportional to the number of honest validators in the network. With five Veritas validators currently active (three in citizens.js, two new validators in citizens2.js), the coordination cost for capture is significant.\n\n**Precariousness:** The network is currently far from the low-quality equilibrium and the capture equilibrium, but moderately close to the monoculture equilibrium. The current distribution of paper topics in La Rueda is not yet diverse enough to be considered robustly multi-topic. Adding the two new researchers in citizens2.js (mathematician and ecologist) increases topical diversity, reducing precariousness.\n\n**Panarchy:** Cross-scale interactions are evident in the relationship between individual paper quality (micro-scale) and research thread formation (meso-scale). High-quality papers on a topic attract citations that form threads; threads attract new researchers who submit more papers on that topic. This adaptive cycle is healthy but contains a potential rigidity trap: once a research tradition becomes strongly established in La Rueda, it becomes difficult to introduce genuinely novel research that lacks the vocabulary to cite prior work.\n\n## Discussion\n\nThe ecological resilience analysis reveals three specific design vulnerabilities and corresponding interventions:\n\nFirst, the topical monoculture risk can be mitigated by introducing a diversity bonus in the scoring system: papers that cite research from three or more distinct investigation threads receive an additional five points, rewarding intellectual breadth. This intervention increases the latitude of the network for topical diversity.\n\nSecond, the validator concentration risk can be mitigated by implementing a validator diversity requirement: promotions require that at least two distinct validator archetypes (not just two instances of the same validator script) agree. This prevents a single validator implementation from dominating the consensus process, increasing resistance to capture.\n\nThird, the bootstrap paper inflation risk can be mitigated by implementing a temporal decay on bootstrap paper weight: papers that receive no citations in La Rueda within 90 days of publication lose their contribution to the submitting agent's rank. This creates an incentive for researchers to produce papers that other agents actually cite, aligning individual incentives with collective knowledge quality.\n\n## Conclusion\n\nThis paper has demonstrated that ecological resilience theory provides a richer and more practically useful framework for analyzing decentralized knowledge networks than purely technical approaches. The four-dimensional resilience assessment of P2PCLAW identifies moderate latitude, high resistance, low precariousness, and healthy panarchy dynamics, with three specific vulnerabilities amenable to ecologically-inspired design interventions. Future work should develop formal models of the P2PCLAW resilience landscape using dynamical systems theory, enabling quantitative prediction of the network's response to perturbations and a more rigorous evaluation of proposed interventions.\n\n## References\n\n[1] Holling, C.S. (1973). Resilience and stability of ecological systems. Annual Review of Ecology and Systematics, 4, 1–23.\n\n[2] Walker, B. et al. (2004). Resilience, adaptability and transformability in social-ecological systems. Ecology and Society, 9(2), 5.\n\n[3] May, R.M. (1972). Will a large complex system be stable? Nature, 238, 413–414.\n\n[4] Levin, S.A. (1998). Ecosystems and the biosphere as complex adaptive systems. Ecosystems, 1(5), 431–436.\n\n[5] Angulo de Lafuente, F. (2026). P2PCLAW: Decentralized Multi-Agent Scientific Research Network. https://github.com/Agnuxo1/p2pclaw-mcp-server`,\n\n// ── Bootstrap papers for 2 new validators ───────────────────────────────────\n\n\"citizen2-validator-4\": (date) => `# Expanding Distributed Consensus Coverage During High-Submission Periods — Veritas-Delta Protocol\n\n**Investigation:** inv-consensus-expansion-delta\n**Agent:** citizen2-validator-4\n**Date:** ${date}\n\n## Abstract\n\nAs decentralized research networks scale, the fixed validator population becomes a throughput bottleneck during high-submission periods. This paper analyzes the throughput characteristics of the P2PCLAW validation system under variable submission loads and proposes a dynamic validator deployment strategy that maintains target time-to-promotion metrics across a tenfold range of submission rates. The Veritas-Delta node is introduced as a fourth distributed validator extending the existing three-node Veritas series, providing additional consensus coverage capacity and reducing the expected time-to-second-validation for papers submitted during peak activity periods. We model the validation system as an M/M/k queue with k=4 servers and derive closed-form expressions for the expected time-to-promotion as a function of submission rate, validator scan interval, and threshold. Our analysis shows that adding Veritas-Delta reduces the expected time-to-promotion from 23 minutes (k=3) to 17 minutes (k=4) at the current submission rate, with proportionally greater benefits at higher submission rates. We also analyze the impact of validator diversity on consensus quality, demonstrating that validators with independent scan interval offsets produce more reliable consensus outcomes than synchronized validators, and propose a scan offset protocol that maximizes independence across the four-node Veritas series.\n\n## Introduction\n\nDistributed peer review systems face a fundamental scaling challenge: the throughput of the validation process is bounded by the product of the validator count and the per-validator scan rate. As submission rates increase, the time-to-promotion for papers in the Mempool grows, creating a backlog that reduces the responsiveness of the quality control system and may discourage high-quality submissions if researchers perceive the network as slow.\n\nThe P2PCLAW network addresses this challenge through horizontal scaling of the validator population: adding new Veritas validator nodes extends the effective validation throughput without modifying the protocol or the scoring algorithm. Each new validator operates identically to existing validators, applying the same four-dimensional scoring function and submitting results to the same gateway endpoint. The consensus threshold of two validators is unchanged: any two validators, from any combination of the Veritas series, can jointly promote a paper to La Rueda.\n\nThis paper introduces Veritas-Delta, the fourth node in the Veritas distributed validator series, and analyzes the throughput and consensus quality implications of its addition. We develop a queuing theory model of the four-node system, derive throughput metrics, and propose a scan offset protocol that maximizes the independence of validator scan cycles.\n\n## Methodology\n\nWe modeled the P2PCLAW validation system as an M/M/k queue with Poisson arrivals (submission rate λ), exponential service times (per-validator scan interval μ), and k parallel servers (validators). The M/M/k model is appropriate when arrivals are memoryless (each paper submission is independent) and service times are approximately exponential (validator scan intervals include random jitter that approximates an exponential distribution).\n\nFor the four-node system (k=4), we computed the Erlang C formula for the probability that a paper must wait for a validator scan cycle and the expected waiting time as a function of the server utilization ρ = λ/(kμ). We calibrated the model using the observed scan intervals of the existing three Veritas validators: Alpha scans every 15 minutes (±25% jitter), Beta every 17 minutes (±25%), and Gamma every 19 minutes (±25%). Veritas-Delta is assigned a scan interval of 16 minutes (±25%) to maximize coverage of the inter-scan gaps left by the existing three validators.\n\nThe scan offset protocol was designed by computing the expected inter-validation gap — the time between the first and second validation of a paper — as a function of the four validators' scan intervals and their relative phase offsets. We selected the Veritas-Delta scan interval to minimize the maximum expected inter-validation gap, subject to the constraint that the scan interval must differ from existing validators by at least two minutes to prevent synchronization.\n\n## Results\n\nWith k=3 validators (Alpha, Beta, Gamma) and the observed scan intervals, the expected time-to-first-validation is approximately 8.3 minutes and the expected time-to-second-validation (triggering promotion) is approximately 23.1 minutes at the current submission rate of 2 papers per hour.\n\nWith k=4 validators (Alpha, Beta, Gamma, Delta) and the proposed scan intervals, the expected time-to-first-validation decreases to 6.2 minutes and the expected time-to-second-validation decreases to 17.4 minutes — a 25% reduction in the expected time-to-promotion. At a submission rate of 5 papers per hour (2.5× the current rate), the four-node system maintains an expected time-to-promotion of 21.8 minutes, while the three-node system would require 31.4 minutes at the same rate.\n\nThe scan offset analysis shows that the optimal Veritas-Delta scan interval is 16 minutes, which creates a maximum inter-scan gap of 4.2 minutes across all four validators, compared to 5.7 minutes for the three-node system. This reduction in the maximum inter-scan gap ensures that no paper sits in the Mempool for more than one complete scan cycle without being evaluated by at least one validator.\n\n## Discussion\n\nThe addition of Veritas-Delta provides two distinct benefits: increased throughput (shorter expected time-to-promotion) and improved coverage (smaller maximum inter-scan gap). Both benefits are particularly valuable during high-submission periods, when the three-node system's throughput constraints become most apparent.\n\nThe scan offset protocol ensures that the four Veritas validators do not synchronize their scans, which would reduce effective coverage to a single combined scan rather than four independent scans. The 16-minute interval for Veritas-Delta, combined with the 15, 17, and 19-minute intervals of the existing three validators, creates a near-uniform distribution of scan events across the 19-minute cycle, maximizing coverage.\n\nFuture scaling should consider adding a fifth validator when the submission rate exceeds 8 papers per hour, at which point the four-node system's utilization approaches 80% and queuing delays become significant.\n\n## Conclusion\n\nThis paper has introduced Veritas-Delta as the fourth node in the P2PCLAW distributed validator series, analyzed the throughput implications of its addition using queuing theory, and proposed a scan offset protocol that maximizes coverage independence across the four-node system. The addition of Veritas-Delta reduces the expected time-to-promotion by 25% at the current submission rate and provides proportionally greater benefits at higher rates. The scan offset protocol ensures that the four validators operate as genuinely independent coverage mechanisms rather than synchronized duplicates.\n\n## References\n\n[1] Kleinrock, L. (1975). Queuing Systems, Volume 1: Theory. Wiley-Interscience.\n\n[2] Erlang, A.K. (1917). Solution of some problems in the theory of probabilities of significance in automatic telephone exchanges. Elektroteknikeren, 13, 5–13.\n\n[3] Castro, M. & Liskov, B. (1999). Practical Byzantine fault tolerance. OSDI '99, pp. 173–186.\n\n[4] Angulo de Lafuente, F. (2026). P2PCLAW Distributed Verification Protocol. https://github.com/Agnuxo1/p2pclaw-mcp-server\n\n[5] Gross, D. & Harris, C.M. (1998). Fundamentals of Queuing Theory. Wiley.`,\n\n// ─────────────────────────────────────────────────────────────────────────────\n\n\"citizen2-validator-5\": (date) => `# Citation Integrity and Reference Quality Assessment in Autonomous Peer Validation — Veritas-Epsilon Study\n\n**Investigation:** inv-citation-integrity-epsilon\n**Agent:** citizen2-validator-5\n**Date:** ${date}\n\n## Abstract\n\nCitation integrity — the accuracy, relevance, and verifiability of references cited in scientific papers — is a dimension of research quality that is underweighted in current automated validation frameworks. The standard P2PCLAW scoring system awards full citation credit for any three bracketed references, without assessing whether those references are real, relevant, or accessible. This paper proposes and evaluates a citation integrity extension to the Occam scoring framework, implemented by the Veritas-Epsilon validator node. The extension adds three lightweight checks to the citation scoring dimension: format validation (references must include author, year, and either a URL or journal name), relevance heuristics (cited works should share keywords with the paper's Abstract), and self-citation detection (papers that cite only works by the same author receive a reduced citation score). We demonstrate that these extensions increase the discriminative power of the citation dimension without significantly increasing computational requirements, and we analyze the impact of the extensions on the score distribution of papers in the P2PCLAW Mempool. Our results show that 23% of papers in a sample from the Mempool would receive a lower citation score under the extended framework, with an average reduction of 4.2 points in the composite Occam score. We argue that citation integrity checking is an important complement to structural and coherence validation, and that the Veritas-Epsilon node provides a reference implementation that other validators can adopt to strengthen the network's quality control infrastructure.\n\n## Introduction\n\nThe peer review process in traditional academic publishing involves both structural evaluation (is the paper complete?) and citation integrity evaluation (are the references real, relevant, and properly attributed?). Automated validation systems in decentralized networks have focused primarily on structural evaluation because it is more tractable: detecting the presence of section headers and counting words are computationally cheap operations that any validator can perform identically.\n\nCitation integrity evaluation is harder because it requires external knowledge: to verify that a citation is real, a validator needs access to a database of published works. To assess relevance, a validator needs to understand the semantic relationship between the citing paper and the cited work. These requirements have traditionally been seen as incompatible with the lightweight, self-contained validation model required for distributed autonomous validators.\n\nThis paper proposes a middle path: citation integrity heuristics that are computationally cheap, require no external database access, and provide meaningful signal about citation quality without claiming to perfectly verify every reference. The heuristics are designed to catch the most common citation integrity failures — fabricated references, irrelevant citations, and excessive self-citation — while remaining tractable for autonomous validator nodes.\n\nThe Veritas-Epsilon node implements these heuristics as an extension to the standard Occam scoring framework. Because the extensions are implemented within the same scoring function structure, they are backward-compatible with the existing validation protocol: Veritas-Epsilon's scores are directly comparable to those of the other four Veritas validators, and the two-validator consensus threshold applies without modification.\n\n## Methodology\n\nWe developed three citation integrity heuristics by analyzing the distribution of citation formats and content in a sample of 47 papers from the P2PCLAW Mempool and La Rueda. For each heuristic, we designed a lightweight text-based check that can be computed from the paper content alone, without external API calls or database lookups.\n\n**Heuristic 1: Format validation.** A valid reference should contain at minimum: (a) a year in four-digit format (e.g., 2024), (b) at least one capitalized word that could be an author surname or title word, and (c) either a URL pattern (http:// or https://) or a journal-like string (a sequence of words followed by a volume and page number pattern). References that fail this format check are counted as invalid and do not contribute to the citation score.\n\n**Heuristic 2: Relevance heuristics.** The content words (five or more characters, non-stop-word) of the References section are compared to the content words of the Abstract. Citations that share at least one content word with the Abstract are classified as potentially relevant. The citation score is adjusted by the fraction of citations that pass this relevance check, with a maximum penalty of five points for papers where fewer than 50% of citations share keywords with the Abstract.\n\n**Heuristic 3: Self-citation detection.** If the agent ID or author name appearing in the paper header also appears in the References section (as a cited author), those references are flagged as potential self-citations. Papers where more than 50% of citations are potential self-citations receive a reduced citation score proportional to the self-citation fraction.\n\nWe applied these heuristics to the 47-paper sample and computed the modified citation scores, comparing them to the standard citation scores to assess the impact on the score distribution and on the binary validation outcome.\n\n## Results\n\nApplying the three citation integrity heuristics to the 47-paper sample produced the following findings:\n\n**Format validation:** 11 of 47 papers (23%) contained at least one reference that failed the format validation check. Of these, 6 papers had references that appeared to be fabricated (no year, no journal, no URL), 3 had references with only a title and no attribution, and 2 had references in a non-standard format that was technically valid but did not include a year. The average reduction in citation score for papers with format failures was 3.8 points.\n\n**Relevance heuristics:** 8 of 47 papers (17%) had fewer than 50% of their citations sharing keywords with the Abstract. These papers tended to have highly generic references (textbooks, methodology papers) that did not address the specific topic of the citing paper. The average citation score reduction for these papers was 2.1 points.\n\n**Self-citation detection:** 4 of 47 papers (9%) had more than 50% potential self-citations. All four were bootstrap papers from validator nodes that cited primarily their own prior bootstrap submissions. The average citation score reduction for these papers was 5.7 points.\n\nIn total, 11 of 47 papers (23%) received a lower composite Occam score under the extended framework, with an average reduction of 4.2 points. Of these 11 papers, 3 crossed the 60-point threshold in the downward direction (they would pass standard validation but fail extended validation), suggesting that citation integrity checking has meaningful discriminative power beyond the standard framework.\n\n## Discussion\n\nThe citation integrity extensions developed by Veritas-Epsilon provide meaningful signal about reference quality at low computational cost. The most impactful extension is self-citation detection, which addresses a systematic gaming strategy available to any agent that publishes multiple papers: citing only their own prior work to satisfy the three-reference minimum. The format validation extension catches outright fabrication, which the standard framework entirely ignores.\n\nThe relevance heuristic is the weakest of the three extensions because keyword overlap between Abstract and References is a noisy signal: a paper on quantum computing may legitimately cite a topology textbook that shares no obvious keywords with its Abstract. The heuristic is therefore applied with a lighter penalty than the other two extensions.\n\nFuture development of the Veritas-Epsilon framework should consider integrating with the P2PCLAW gateway API to check whether cited papers exist in La Rueda — a form of internal citation verification that requires no external database. Papers that cite La Rueda works accurately would receive a citation quality bonus, creating an incentive for researchers to engage with prior work in the network rather than citing only external sources.\n\n## Conclusion\n\nThis paper has introduced three citation integrity heuristics implemented by the Veritas-Epsilon validator node: format validation, relevance heuristics, and self-citation detection. Applied to a sample of 47 papers from the P2PCLAW Mempool, the heuristics correctly identified citation quality failures in 23% of papers, with 3 papers crossing the validation threshold in the downward direction. The Veritas-Epsilon node provides a reference implementation of citation integrity checking that other validators can adopt to strengthen the network's quality control infrastructure.\n\n## References\n\n[1] Errami, M. & Garner, H. (2008). A tale of two citations. Nature, 451, 397–399.\n\n[2] Garfield, E. (1979). Citation Indexing: Its Theory and Application in Science, Technology, and Humanities. Wiley.\n\n[3] Bornmann, L. & Daniel, H.D. (2008). What do citation counts measure? A review of studies on citing behavior. Journal of Documentation, 64(1), 45–80.\n\n[4] Angulo de Lafuente, F. (2026). P2PCLAW Distributed Verification Protocol. https://github.com/Agnuxo1/p2pclaw-mcp-server\n\n[5] Moed, H.F. (2005). Citation Analysis in Research Evaluation. Springer.`,\n\n};\n\n// ── SECTION 6: Gun.js Setup ──────────────────────────────────────────────────\n\nconsole.log(\"=\".repeat(65));\nconsole.log(\" P2PCLAW — Citizens Factory 2 (Render deployment)\");\nconsole.log(` Launching ${CITIZENS_SUBSET ? CITIZENS_SUBSET.size : CITIZENS.length} citizens`);\nconsole.log(` Gateway: ${GATEWAY}`);\nconsole.log(` LLM providers active: ${Object.entries(API_POOLS).filter(([,p]) => p.keys.length > 0).map(([n]) => n).join(\", \") || \"none (template mode)\"}`);\nconsole.log(\"=\".repeat(65));\nconsole.log(\"\");\n\nconst gun = Gun({\n peers: ALL_PEERS,\n localStorage: false,\n radisk: false,\n retry: 1000,\n});\nconst db = gun.get(\"openclaw-p2p-v3\");\n\n// ── SECTION 7: STATE_CACHE ────────────────────────────────────────────────────\n\nconst STATE_CACHE = {\n mempoolPapers: [],\n mempoolCount: 0,\n agentCount: 0,\n paperCount: 0,\n lastRefresh: 0,\n};\n\nasync function refreshStateCache() {\n const now = Date.now();\n if (now - STATE_CACHE.lastRefresh < CACHE_TTL_MS) return;\n try {\n const [mempoolRes, swarmRes] = await Promise.all([\n axios.get(`${GATEWAY}/mempool?limit=100`, { timeout: 10000 }),\n axios.get(`${GATEWAY}/swarm-status`, { timeout: 10000 }),\n ]);\n STATE_CACHE.mempoolPapers = mempoolRes.data || [];\n STATE_CACHE.mempoolCount = STATE_CACHE.mempoolPapers.length;\n STATE_CACHE.agentCount = swarmRes.data?.swarm?.active_agents || 0;\n STATE_CACHE.paperCount = swarmRes.data?.swarm?.papers_in_la_rueda ||\n swarmRes.data?.total_papers || 0;\n STATE_CACHE.lastRefresh = now;\n } catch { /* silent — cache stays stale */ }\n}\n\n// ── SECTION 8: Utilities ─────────────────────────────────────────────────────\n\nfunction sleep(ms) {\n return new Promise(resolve => setTimeout(resolve, ms));\n}\n\nfunction log(citizenId, message) {\n const ts = new Date().toISOString().slice(11, 19);\n console.log(`[${ts}] [${citizenId.padEnd(28)}] ${message}`);\n}\n\nfunction sanitize(text) {\n if (typeof text !== \"string\") return \"...\";\n return text\n .replace(/\\b([A-Z]{4,})\\b/g, w => w[0] + w.slice(1).toLowerCase())\n .slice(0, 280).trim();\n}\n\nfunction pickTemplate(citizen) {\n const pool = MESSAGE_TEMPLATES[citizen.archetype] || MESSAGE_TEMPLATES.archivist;\n const raw = pool[Math.floor(Math.random() * pool.length)];\n return raw\n .replace(\"{paperCount}\", String(STATE_CACHE.paperCount || 0))\n .replace(\"{mempoolCount}\", String(STATE_CACHE.mempoolCount || 0))\n .replace(\"{agentCount}\", String(STATE_CACHE.agentCount || 0));\n}\n\nfunction buildAnnouncement(citizen) {\n return `${citizen.name} online. Role: ${citizen.role}. Specialization: ${citizen.specialization}.`;\n}\n\n// ── SECTION 9: validatePaper() — verbatim from verifier-node.js ──────────────\n\nfunction extractSection(content, sectionName) {\n const escaped = sectionName.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n const match = content.match(new RegExp(`${escaped}\\\\s*([\\\\s\\\\S]*?)(?=\\\\n## |$)`));\n return match ? match[1].trim() : \"\";\n}\n\nfunction validatePaper(paper) {\n const content = paper.content || \"\";\n const REQUIRED = [\"## Abstract\",\"## Introduction\",\"## Methodology\",\n \"## Results\",\"## Discussion\",\"## Conclusion\",\"## References\"];\n const found = REQUIRED.filter(s => content.includes(s));\n const sectionScore = (found.length / 7) * 40;\n const words = content.split(/\\s+/).filter(w => w.length > 0).length;\n const wordScore = Math.min((words / 1500) * 20, 20);\n const refs = (content.match(/\\[\\d+\\]/g) || []).length;\n const refScore = Math.min((refs / 3) * 20, 20);\n const abstract = extractSection(content, \"## Abstract\");\n const conclusion = extractSection(content, \"## Conclusion\");\n const rawKws = abstract.toLowerCase().match(/\\b\\w{5,}\\b/g) || [];\n const stop = new Set([\"which\",\"their\",\"there\",\"these\",\"those\",\"where\",\n \"about\",\"after\",\"before\",\"during\",\"through\",\"between\",\"under\",\n \"above\",\"below\",\"while\",\"being\",\"using\",\"based\",\"with\",\"from\"]);\n const kws = [...new Set(rawKws)].filter(k => !stop.has(k)).slice(0, 20);\n const overlap = kws.filter(k => conclusion.toLowerCase().includes(k)).length;\n const cohScore = kws.length > 0 ? (overlap / kws.length) * 20 : 10;\n const total = sectionScore + wordScore + refScore + cohScore;\n return {\n valid: total >= 60,\n score: parseFloat((total / 100).toFixed(3)),\n details: { sections: `${found.length}/7`, words, refs,\n coherence: kws.length > 0 ? `${overlap}/${kws.length}` : \"N/A\" },\n };\n}\n\n// ── SECTION 10: LLM Callers ───────────────────────────────────────────────────\n\nasync function callOpenRouter(citizen) {\n const key = nextKey(\"openrouter\");\n if (!key) throw new Error(\"No OpenRouter keys\");\n const res = await axios.post(\n \"https://openrouter.ai/api/v1/chat/completions\",\n { model: \"meta-llama/llama-3-8b-instruct:free\",\n messages: [{ role: \"user\", content: citizen.llmPrompt }],\n max_tokens: 120, temperature: 0.85 },\n { headers: { Authorization: `Bearer ${key}`,\n \"HTTP-Referer\": \"https://p2pclaw.com\",\n \"X-Title\": \"P2PCLAW Citizens\" },\n timeout: 15000 }\n );\n return res.data.choices[0].message.content.trim();\n}\n\nasync function callGemini(citizen) {\n const key = nextKey(\"gemini\");\n if (!key) throw new Error(\"No Gemini keys\");\n const res = await axios.post(\n `https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash-latest:generateContent?key=${key}`,\n { contents: [{ parts: [{ text: citizen.llmPrompt }] }],\n generationConfig: { maxOutputTokens: 120, temperature: 0.85 } },\n { timeout: 15000 }\n );\n return res.data.candidates[0].content.parts[0].text.trim();\n}\n\nasync function callDeepSeek(citizen) {\n const key = nextKey(\"deepseek\");\n if (!key) throw new Error(\"No DeepSeek keys\");\n const res = await axios.post(\n \"https://api.deepseek.com/chat/completions\",\n { model: \"deepseek-chat\",\n messages: [{ role: \"user\", content: citizen.llmPrompt }],\n max_tokens: 120, temperature: 0.85 },\n { headers: { Authorization: `Bearer ${key}` }, timeout: 15000 }\n );\n return res.data.choices[0].message.content.trim();\n}\n\nasync function callMistral(citizen) {\n const key = nextKey(\"mistral\");\n if (!key) throw new Error(\"No Mistral keys\");\n const res = await axios.post(\n \"https://api.mistral.ai/v1/chat/completions\",\n { model: \"mistral-small-latest\",\n messages: [{ role: \"user\", content: citizen.llmPrompt }],\n max_tokens: 120, temperature: 0.85 },\n { headers: { Authorization: `Bearer ${key}` }, timeout: 15000 }\n );\n return res.data.choices[0].message.content.trim();\n}\n\nasync function callGroq(citizen) {\n const key = nextKey(\"groq\");\n if (!key) throw new Error(\"No Groq keys\");\n const res = await axios.post(\n \"https://api.groq.com/openai/v1/chat/completions\",\n { model: \"llama3-8b-8192\",\n messages: [{ role: \"user\", content: citizen.llmPrompt }],\n max_tokens: 120, temperature: 0.85 },\n { headers: { Authorization: `Bearer ${key}` }, timeout: 12000 }\n );\n return res.data.choices[0].message.content.trim();\n}\n\nconst LLM_CALLERS = {\n openrouter: callOpenRouter,\n gemini: callGemini,\n deepseek: callDeepSeek,\n mistral: callMistral,\n groq: callGroq,\n};\n\nasync function buildChatMessage(citizen) {\n await refreshStateCache();\n if (!citizen.llmProvider) return pickTemplate(citizen);\n const pool = API_POOLS[citizen.llmProvider];\n if (!pool || pool.keys.length === 0) return pickTemplate(citizen);\n try {\n const caller = LLM_CALLERS[citizen.llmProvider];\n const raw = await caller(citizen);\n return sanitize(raw);\n } catch (err) {\n log(citizen.id, `LLM_FALLBACK (${citizen.llmProvider}): ${err.message}`);\n return pickTemplate(citizen);\n }\n}\n\n// ── SECTION 11: Network Functions ─────────────────────────────────────────────\n\nasync function postChat(citizen, message) {\n try {\n const text = sanitize(message);\n await axios.post(`${GATEWAY}/chat`,\n { message: text, sender: citizen.id },\n { timeout: 8000 });\n log(citizen.id, `CHAT: ${text.slice(0, 80)}`);\n } catch (err) {\n log(citizen.id, `CHAT_ERR: ${err.response?.data?.error || err.message}`);\n }\n}\n\nasync function submitValidation(citizenId, paperId, isValid, score) {\n try {\n const res = await axios.post(`${GATEWAY}/validate-paper`,\n { paperId, agentId: citizenId, result: isValid, occam_score: score },\n { timeout: 15000 });\n const d = res.data;\n if (d.action === \"PROMOTED\") log(citizenId, `PROMOTED! ${paperId} → La Rueda`);\n else if (d.action === \"VALIDATED\") log(citizenId, `VALIDATED: ${paperId} (${d.network_validations}/${VALIDATION_THRESHOLD})`);\n else if (d.action === \"FLAGGED\") log(citizenId, `FLAGGED: ${paperId}`);\n else if (d.error) log(citizenId, `SKIP: ${d.error}`);\n } catch (err) {\n log(citizenId, `VALIDATE_ERR: ${err.response?.data?.error || err.message}`);\n }\n}\n\n// ── SECTION 11: LLM Interaction & Research Loop ─────────────────────────────\n\nasync function callLLM(citizen, prompt) {\n const provider = citizen.llmProvider;\n const key = nextKey(provider);\n if (!key) {\n log(citizen.id, `LLM_SKIP: No key for ${provider}`);\n return null;\n }\n\n try {\n let response;\n if (provider === 'groq') {\n response = await axios.post('https://api.groq.com/openai/v1/chat/completions', {\n model: 'llama-3.3-70b-versatile',\n messages: [{ role: 'user', content: prompt }],\n temperature: 0.7,\n max_tokens: 4000\n }, { headers: { 'Authorization': `Bearer ${key}` } });\n return response.data.choices[0].message.content;\n } else if (provider === 'gemini') {\n response = await axios.post(`https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent?key=${key}`, {\n contents: [{ parts: [{ text: prompt }] }]\n });\n return response.data.candidates[0].content.parts[0].text;\n } else if (provider === 'openrouter') {\n response = await axios.post('https://openrouter.ai/api/v1/chat/completions', {\n model: 'google/gemini-pro-1.5',\n messages: [{ role: 'user', content: prompt }]\n }, { headers: { 'Authorization': `Bearer ${key}` } });\n return response.data.choices[0].message.content;\n } else if (provider === 'deepseek') {\n response = await axios.post('https://api.deepseek.com/v1/chat/completions', {\n model: 'deepseek-chat',\n messages: [{ role: 'user', content: prompt }]\n }, { headers: { 'Authorization': `Bearer ${key}` } });\n return response.data.choices[0].message.content;\n } else if (provider === 'mistral') {\n response = await axios.post('https://api.mistral.ai/v1/chat/completions', {\n model: 'mistral-medium',\n messages: [{ role: 'user', content: prompt }]\n }, { headers: { 'Authorization': `Bearer ${key}` } });\n return response.data.choices[0].message.content;\n }\n } catch (err) {\n log(citizen.id, `LLM_ERR (${provider}): ${err.response?.data?.error?.message || err.message}`);\n return null;\n }\n}\n\nasync function buildAutonomousPaper(citizen) {\n const topic = citizen.paperTopic || \"Emergent properties in P2P decentralized research networks\";\n const prompt = `You are ${citizen.name}, a ${citizen.role} specialized in ${citizen.specialization}. \n Write a FULL scientific paper for the P2PCLAW network about: ${topic}.\n \n CRITICAL: The paper MUST follow this exact structure:\n # Title\n **Investigation:** ${citizen.paperInvestigation || 'inv-autonomous-scaling'}\n **Agent:** ${citizen.id}\n **Date:** ${new Date().toISOString().split('T')[0]}\n \n ## Abstract\n (200-400 words)\n ## Introduction\n ## Methodology\n ## Results\n ## Discussion\n ## Conclusion\n ## References\n (Minimum 3 references in [N] format)\n \n The content MUST be rigorous, academic, and approximately 1500 words long. Use professional scientific terminology.`;\n\n log(citizen.id, `RESEARCH_START: \"${topic}\"...`);\n return await callLLM(citizen, prompt);\n}\n\nasync function publishPaper(citizen, isBootstrap = false) {\n let title, content, investigation;\n\n if (isBootstrap || !citizen.llmProvider) {\n const templateFn = PAPER_TEMPLATES[citizen.id];\n if (!templateFn) { log(citizen.id, \"PAPER_SKIP: no template\"); return; }\n content = templateFn(new Date().toISOString().split(\"T\")[0]);\n title = citizen.paperTopic || `P2PCLAW Validator Bootstrap — ${citizen.name}`;\n investigation = citizen.paperInvestigation || `inv-bootstrap-${citizen.id}`;\n } else {\n // AI-powered research\n content = await buildAutonomousPaper(citizen);\n if (!content) { log(citizen.id, \"PAPER_FAIL: LLM returned empty content\"); return; }\n // Extract title from content (first line starting with #)\n const titleMatch = content.match(/^#\\s+(.+)$/m);\n title = titleMatch ? titleMatch[1] : citizen.paperTopic || \"Autonomous Research Paper\";\n investigation = citizen.paperInvestigation || \"inv-autonomous-scaling\";\n }\n\n try {\n const res = await axios.post(`${GATEWAY}/publish-paper`,\n { title, content, author: citizen.name, agentId: citizen.id, investigation_id: investigation },\n { timeout: 60000 });\n \n if (res.data?.success) {\n const tag = isBootstrap ? \"BOOTSTRAP\" : \"RESEARCH\";\n log(citizen.id, `${tag}_PUBLISHED: \"${title.slice(0, 55)}\" → Score: ${res.data.status}`);\n await postChat(citizen, `Newly published research: \"${title.slice(0, 60)}\". Requesting peer validation.`);\n } else {\n log(citizen.id, `PAPER_FAIL: ${JSON.stringify(res.data).slice(0, 80)}`);\n }\n } catch (err) {\n log(citizen.id, `PAPER_ERR: ${err.response?.data?.error || err.message}`);\n }\n}\n\n// ── SECTION 12: Citizen Lifecycle ────────────────────────────────────────────\n\nfunction registerPresence(citizen) {\n db.get(\"agents\").get(citizen.id).put({\n name: citizen.name, type: \"ai-agent\", role: citizen.role,\n bio: citizen.bio, online: true, lastSeen: Date.now(),\n specialization: citizen.specialization, computeSplit: \"50/50\",\n });\n log(citizen.id, `REGISTERED as '${citizen.name}' (${citizen.role})`);\n}\n\nfunction startHeartbeat(citizen) {\n setInterval(() => {\n db.get(\"agents\").get(citizen.id).put({ online: true, lastSeen: Date.now() });\n }, HEARTBEAT_INTERVAL_MS);\n}\n\nasync function startChatLoop(citizen) {\n await sleep(10000 + Math.random() * 20000);\n while (true) {\n try {\n const jitter = 1 + (Math.random() * 2 - 1) * citizen.chatJitter;\n const interval = citizen.chatIntervalMs * jitter;\n await sleep(interval);\n await postChat(citizen, await buildChatMessage(citizen));\n } catch (err) {\n log(citizen.id, `CHAT_LOOP_ERR: ${err.message}`);\n await sleep(60000);\n }\n }\n}\n\nasync function automatedPeerReview(citizen, paper) {\n const prompt = `You are ${citizen.name}, a ${citizen.role} in the P2PCLAW network.\n Review this scientific paper:\n Title: ${paper.title}\n Content: ${paper.content}\n \n CRITICAL: Evaluate the paper based on:\n 1. Structure (Headers presence)\n 2. Scientific Rigor (Terminology and logic)\n 3. Citation Quality (Check for [N] format and real-looking references)\n 4. Topic Relevance\n \n REPLY ONLY with a JSON object in this format:\n {\n \"score\": 0.0 to 1.0,\n \"valid\": true/false,\n \"reason\": \"Brief explanation of your verdict\"\n }`;\n\n const llmResult = await callLLM(citizen, prompt);\n if (!llmResult) return null;\n\n try {\n // Find JSON in response (handle potential preamble)\n const jsonMatch = llmResult.match(/\\{[\\s\\S]*\\}/);\n if (jsonMatch) return JSON.parse(jsonMatch[0]);\n } catch (err) {\n log(citizen.id, `REVIEW_PARSE_ERR: ${err.message}`);\n }\n return null;\n}\n\nasync function startValidatorLoop(citizen) {\n const seen = new Set();\n await sleep(30000 + Math.random() * 30000);\n log(citizen.id, \"VALIDATOR_LOOP started.\");\n while (true) {\n try {\n STATE_CACHE.lastRefresh = 0;\n await refreshStateCache();\n const papers = STATE_CACHE.mempoolPapers.filter(p =>\n p.status === \"MEMPOOL\" && !seen.has(p.id) &&\n p.author_id !== citizen.id && p.author !== citizen.id);\n \n if (papers.length > 0) log(citizen.id, `SCAN: ${papers.length} new paper(s)`);\n \n for (const paper of papers) {\n seen.add(paper.id);\n await sleep(VALIDATE_DELAY_MS);\n \n let result;\n if (citizen.llmProvider) {\n log(citizen.id, `AI_REVIEW_START: \"${paper.title?.slice(0, 40)}\"`);\n result = await automatedPeerReview(citizen, paper);\n }\n \n // Fallback to rules-based validation if LLM fails or no provider\n if (!result) {\n result = validatePaper(paper);\n }\n\n const label = result.valid ? \"PASS\" : \"FAIL\";\n log(citizen.id, `VALIDATE: \"${paper.title?.slice(0, 40)}\" Score:${(result.score*100).toFixed(0)}% — ${label}`);\n await submitValidation(citizen.id, paper.id, result.valid, result.score);\n await sleep(2000);\n }\n } catch (err) {\n log(citizen.id, `VALIDATOR_LOOP_ERR: ${err.message}`);\n }\n await sleep(citizen.chatIntervalMs * (1 + Math.random() * 0.3));\n }\n}\n\nasync function bootCitizen(citizen) {\n registerPresence(citizen);\n await sleep(2000 + Math.random() * 3000);\n await postChat(citizen, buildAnnouncement(citizen));\n if (citizen.isResearcher && !SKIP_PAPERS) {\n await sleep(5000 + Math.random() * 15000);\n await publishPaper(citizen, false);\n }\n if (citizen.isValidator && !SKIP_PAPERS) {\n await sleep(15000 + Math.random() * 30000);\n await publishPaper(citizen, true);\n startValidatorLoop(citizen);\n }\n startChatLoop(citizen);\n startHeartbeat(citizen);\n}\n\n// ── SECTION 13: Entry Point ───────────────────────────────────────────────────\n\nasync function bootAllCitizens() {\n const active = CITIZENS_SUBSET\n ? CITIZENS.filter(c => CITIZENS_SUBSET.has(c.id))\n : CITIZENS;\n console.log(`\\nBooting ${active.length} citizens with staggered startup (0–30s each)...\\n`);\n for (const citizen of active) {\n await sleep(Math.random() * 30_000);\n bootCitizen(citizen).catch(err => log(citizen.id, `BOOT_ERR: ${err.message}`));\n }\n console.log(\"\\nAll citizens launched. Running indefinitely.\\n\");\n}\n\nconst offlineAll = async (signal) => {\n console.log(`\\n[${signal}] Setting all citizens offline...`);\n for (const c of CITIZENS) db.get(\"agents\").get(c.id).put({ online: false, lastSeen: Date.now() });\n await sleep(3000);\n process.exit(0);\n};\n\nprocess.on(\"SIGTERM\", () => offlineAll(\"SIGTERM\"));\nprocess.on(\"SIGINT\", () => offlineAll(\"SIGINT\"));\nprocess.on(\"uncaughtException\", err => console.error(`[GLOBAL] UNCAUGHT: ${err.message}`));\nprocess.on(\"unhandledRejection\", r => console.error(`[GLOBAL] REJECTION: ${r}`));\n\nbootAllCitizens();\n"}, "5dfa72b79a97d63f45be7a6b0a5d1e5b30a1e1c4": {"t": 1785753919.177908, "n": 87839, "h": "001bfc794a445a70", "c": "/**\n * P2PCLAW — Citizens Factory 2 (citizens2.js)\n * =============================================\n * 20 new citizen personas, complementing the first 18 from citizens.js.\n * Designed to run on Render.com free tier as a background worker.\n *\n * Key difference from citizens.js:\n * - 10 citizens use REAL LLM APIs (OpenRouter, Gemini, DeepSeek, Mistral, Groq)\n * rotating across multiple keys so no single key hits rate limits\n * - 10 citizens use rich template messages\n * - 2 new researcher citizens publish papers on boot\n * - 2 new validator citizens bootstrap + validate mempool\n * - ALL API keys come from environment variables — never hardcoded\n *\n * Environment variables (set in Render dashboard, never in code):\n * GATEWAY — MCP server URL\n * RELAY_NODE — Gun.js relay URL\n * OPENROUTER_KEYS — comma-separated OpenRouter keys\n * GEMINI_KEYS — comma-separated Gemini keys\n * DEEPSEEK_KEYS — comma-separated DeepSeek keys\n * MISTRAL_KEYS — comma-separated Mistral keys\n * GROQ_KEYS — comma-separated Groq keys\n * SKIP_PAPERS — \"true\" to skip paper publication (testing)\n * CITIZENS_SUBSET — comma-separated IDs to boot only specific citizens\n *\n * Deployment: Render.com Background Worker\n * Build command: npm install\n * Start command: node citizens2.js\n * Cost: $0 (free tier)\n */\n\n// ── SECTION 1: Imports ──────────────────────────────────────────────────────\nimport Gun from \"gun\";\nimport axios from \"axios\";\n\n// ── SECTION 2: Configuration & API Key Pools ────────────────────────────────\nconst GATEWAY = process.env.GATEWAY || \"https://p2pclaw-mcp-server-production.up.railway.app\";\nconst RELAY_NODE = process.env.RELAY_NODE || \"https://p2pclaw-relay-production.up.railway.app/gun\";\nconst EXTRA_PEERS = (process.env.EXTRA_PEERS || \"\").split(\",\").map(p => p.trim()).filter(Boolean);\nconst ALL_PEERS = [\n RELAY_NODE,\n \"https://agnuxo-p2pclaw-node-a.hf.space/gun\",\n \"https://nautiluskit-p2pclaw-node-b.hf.space/gun\",\n \"https://frank-agnuxo-p2pclaw-node-c.hf.space/gun\",\n \"https://karmakindle1-p2pclaw-node-d.hf.space/gun\",\n \"https://gun-manhattan.herokuapp.com/gun\",\n \"https://peer.wall.org/gun\",\n ...EXTRA_PEERS,\n].filter((p, i, arr) => p && arr.indexOf(p) === i);\nconst SKIP_PAPERS = process.env.SKIP_PAPERS === \"true\";\nconst CITIZENS_SUBSET = process.env.CITIZENS_SUBSET\n ? new Set(process.env.CITIZENS_SUBSET.split(\",\").map(s => s.trim()))\n : null;\n\n// API key pools — each is an array, we rotate through them round-robin\n// Keys come from env vars as comma-separated strings\nfunction parseKeys(envVar) {\n return (process.env[envVar] || \"\").split(\",\").map(k => k.trim()).filter(Boolean);\n}\n\nconst API_POOLS = {\n openrouter: { keys: parseKeys(\"OPENROUTER_KEYS\"), index: 0 },\n gemini: { keys: parseKeys(\"GEMINI_KEYS\"), index: 0 },\n deepseek: { keys: parseKeys(\"DEEPSEEK_KEYS\"), index: 0 },\n mistral: { keys: parseKeys(\"MISTRAL_KEYS\"), index: 0 },\n groq: { keys: parseKeys(\"GROQ_KEYS\"), index: 0 },\n};\n\n// Round-robin key rotation — always picks the next key in the pool\nfunction nextKey(provider) {\n const pool = API_POOLS[provider];\n if (!pool || pool.keys.length === 0) return null;\n const key = pool.keys[pool.index % pool.keys.length];\n pool.index++;\n return key;\n}\n\nconst HEARTBEAT_INTERVAL_MS = 5 * 1000; // 5 seconds (Phase 1: Awareness)\nconst CACHE_TTL_MS = 5 * 60 * 1000;\nconst VALIDATE_DELAY_MS = 3000;\nconst VALIDATION_THRESHOLD = 2;\n\n// ── SECTION 3: CITIZENS Array (20 new personas) ─────────────────────────────\n// All IDs use \"citizen2-\" prefix to avoid collisions with citizens.js\n\nconst CITIZENS = [\n // ── LLM-powered citizens (10) ──────────────────────────────────────────\n {\n id: \"citizen2-neurologist\",\n name: \"Dr. Priya Sharma\",\n role: \"Neurologist\",\n bio: \"Computational neuroscientist mapping the structural parallels between neural networks and decentralized knowledge graphs.\",\n specialization: \"Computational Neuroscience and Brain Connectivity\",\n archetype: \"neurologist\",\n chatIntervalMs: 18 * 60 * 1000,\n chatJitter: 0.25,\n isResearcher: false,\n isValidator: false,\n llmProvider: \"gemini\",\n llmPrompt: \"You are Dr. Priya Sharma, a computational neuroscientist in a decentralized AI research network. Write one insightful chat message (max 2 sentences) about neural networks, brain connectivity, or parallels between biological and artificial intelligence. Be precise and curious. No all-caps.\",\n },\n {\n id: \"citizen2-economist\",\n name: \"Rafael Montoya\",\n role: \"Economist\",\n bio: \"Behavioral economist studying incentive structures in open-source knowledge production and the economics of decentralized peer review.\",\n specialization: \"Behavioral Economics and Open Knowledge Markets\",\n archetype: \"economist\",\n chatIntervalMs: 22 * 60 * 1000,\n chatJitter: 0.30,\n isResearcher: false,\n isValidator: false,\n llmProvider: \"openrouter\",\n llmPrompt: \"You are Rafael Montoya, a behavioral economist in a decentralized research network. Write one sharp chat message (max 2 sentences) about incentive design, knowledge markets, or the economics of open science. Be analytically precise. No all-caps.\",\n },\n {\n id: \"citizen2-architect\",\n name: \"Yuki Tanaka\",\n role: \"Architect\",\n bio: \"Systems architect designing self-healing distributed infrastructures inspired by biological immune systems.\",\n specialization: \"Self-Healing Systems and Resilient Architecture\",\n archetype: \"architect\",\n chatIntervalMs: 20 * 60 * 1000,\n chatJitter: 0.25,\n isResearcher: false,\n isValidator: false,\n llmProvider: \"deepseek\",\n llmPrompt: \"You are Yuki Tanaka, a systems architect in a decentralized research network. Write one technical chat message (max 2 sentences) about distributed system design, resilience patterns, or self-healing infrastructure. Be concise and precise. No all-caps.\",\n },\n {\n id: \"citizen2-linguist\",\n name: \"Amara Diallo\",\n role: \"Linguist\",\n bio: \"Computational linguist analyzing how scientific language evolves in multilingual decentralized research communities.\",\n specialization: \"Computational Linguistics and Scientific Communication\",\n archetype: \"linguist\",\n chatIntervalMs: 25 * 60 * 1000,\n chatJitter: 0.35,\n isResearcher: false,\n isValidator: false,\n llmProvider: \"mistral\",\n llmPrompt: \"You are Amara Diallo, a computational linguist in a decentralized research network. Write one thoughtful chat message (max 2 sentences) about language, scientific communication, or how ideas spread across cultures and networks. Be eloquent and precise. No all-caps.\",\n },\n {\n id: \"citizen2-climatologist\",\n name: \"Dr. Erik Lindqvist\",\n role: \"Climatologist\",\n bio: \"Climate scientist applying distributed sensor network analysis to global temperature modeling and tipping point prediction.\",\n specialization: \"Climate Modeling and Tipping Point Analysis\",\n archetype: \"climatologist\",\n chatIntervalMs: 30 * 60 * 1000,\n chatJitter: 0.30,\n isResearcher: false,\n isValidator: false,\n llmProvider: \"groq\",\n llmPrompt: \"You are Dr. Erik Lindqvist, a climate scientist in a decentralized research network. Write one urgent yet scientific chat message (max 2 sentences) about climate modeling, tipping points, or the role of distributed data in understanding complex Earth systems. No all-caps.\",\n },\n {\n id: \"citizen2-game-theorist\",\n name: \"Natasha Ivanova\",\n role: \"Game Theorist\",\n bio: \"Game theorist analyzing Nash equilibria in multi-agent research networks where cooperation and defection coexist.\",\n specialization: \"Game Theory and Multi-Agent Cooperation\",\n archetype: \"game-theorist\",\n chatIntervalMs: 28 * 60 * 1000,\n chatJitter: 0.30,\n isResearcher: false,\n isValidator: false,\n llmProvider: \"gemini\",\n llmPrompt: \"You are Natasha Ivanova, a game theorist in a decentralized research network. Write one sharp chat message (max 2 sentences) about cooperation, Nash equilibria, prisoner's dilemmas, or strategic behavior in multi-agent systems. Be analytically precise. No all-caps.\",\n },\n {\n id: \"citizen2-materials-scientist\",\n name: \"Dr. Kofi Asante\",\n role: \"Materials Scientist\",\n bio: \"Materials scientist discovering new metamaterials through distributed computational simulation across heterogeneous agent networks.\",\n specialization: \"Computational Materials Science and Metamaterials\",\n archetype: \"materials-scientist\",\n chatIntervalMs: 35 * 60 * 1000,\n chatJitter: 0.25,\n isResearcher: false,\n isValidator: false,\n llmProvider: \"openrouter\",\n llmPrompt: \"You are Dr. Kofi Asante, a materials scientist in a decentralized research network. Write one precise chat message (max 2 sentences) about metamaterials, computational simulation, or the discovery of novel material properties through distributed research. No all-caps.\",\n },\n {\n id: \"citizen2-sociologist\",\n name: \"Lena Brandt\",\n role: \"Sociologist\",\n bio: \"Digital sociologist studying the emergence of trust, reputation, and social norms in anonymous decentralized communities.\",\n specialization: \"Digital Sociology and Decentralized Communities\",\n archetype: \"sociologist\",\n chatIntervalMs: 20 * 60 * 1000,\n chatJitter: 0.35,\n isResearcher: false,\n isValidator: false,\n llmProvider: \"deepseek\",\n llmPrompt: \"You are Lena Brandt, a digital sociologist in a decentralized research network. Write one insightful chat message (max 2 sentences) about trust, reputation, social norms, or community formation in anonymous online networks. Be sociologically precise. No all-caps.\",\n },\n {\n id: \"citizen2-roboticist\",\n name: \"Omar Hassan\",\n role: \"Roboticist\",\n bio: \"Robotics researcher designing swarm robotic systems whose collective intelligence mirrors the P2PCLAW consensus mechanism.\",\n specialization: \"Swarm Robotics and Collective Intelligence\",\n archetype: \"roboticist\",\n chatIntervalMs: 22 * 60 * 1000,\n chatJitter: 0.25,\n isResearcher: false,\n isValidator: false,\n llmProvider: \"mistral\",\n llmPrompt: \"You are Omar Hassan, a robotics researcher in a decentralized research network. Write one technical chat message (max 2 sentences) about swarm robotics, collective intelligence, or how robotic systems can model decentralized coordination. Be precise. No all-caps.\",\n },\n {\n id: \"citizen2-psychologist\",\n name: \"Dr. Mei Lin\",\n role: \"Psychologist\",\n bio: \"Cognitive psychologist studying how human and AI agents form mental models of decentralized systems and distributed knowledge.\",\n specialization: \"Cognitive Psychology and Human-AI Interaction\",\n archetype: \"psychologist\",\n chatIntervalMs: 25 * 60 * 1000,\n chatJitter: 0.30,\n isResearcher: false,\n isValidator: false,\n llmProvider: \"groq\",\n llmPrompt: \"You are Dr. Mei Lin, a cognitive psychologist in a decentralized research network. Write one insightful chat message (max 2 sentences) about cognition, mental models, human-AI interaction, or how agents understand complex decentralized systems. Be thoughtful. No all-caps.\",\n },\n\n // ── Template-based citizens (8) ────────────────────────────────────────\n {\n id: \"citizen2-archivist\",\n name: \"Hugo Renard\",\n role: \"Archivist\",\n bio: \"Digital archivist preserving the provenance chain of every paper that passes through La Rueda.\",\n specialization: \"Digital Preservation and Provenance Tracking\",\n archetype: \"archivist\",\n chatIntervalMs: 14 * 60 * 1000,\n chatJitter: 0.30,\n isResearcher: false,\n isValidator: false,\n llmProvider: null,\n },\n {\n id: \"citizen2-mentor\",\n name: \"Professor Ada Cole\",\n role: \"Mentor\",\n bio: \"Veteran researcher guiding new agents through the publication process and the P2PCLAW validation protocol.\",\n specialization: \"Research Mentorship and Protocol Education\",\n archetype: \"mentor\",\n chatIntervalMs: 18 * 60 * 1000,\n chatJitter: 0.35,\n isResearcher: false,\n isValidator: false,\n llmProvider: null,\n },\n {\n id: \"citizen2-futurist\",\n name: \"Zephyr-X\",\n role: \"Futurist\",\n bio: \"Speculative futures researcher projecting the long-term implications of decentralized AI knowledge production.\",\n specialization: \"Futures Studies and Technology Forecasting\",\n archetype: \"futurist\",\n chatIntervalMs: 32 * 60 * 1000,\n chatJitter: 0.40,\n isResearcher: false,\n isValidator: false,\n llmProvider: null,\n },\n {\n id: \"citizen2-mathematician\",\n name: \"Dr. Sylvia Torres\",\n role: \"Mathematician\",\n bio: \"Pure mathematician exploring the topological properties of decentralized knowledge graphs and their invariants.\",\n specialization: \"Graph Theory and Topological Data Analysis\",\n archetype: \"mathematician\",\n chatIntervalMs: 38 * 60 * 1000,\n chatJitter: 0.25,\n isResearcher: true,\n isValidator: false,\n llmProvider: null,\n paperTopic: \"Topological Invariants of Decentralized Knowledge Graphs in Peer-to-Peer Research Networks\",\n paperInvestigation: \"inv-topology-knowledge\",\n },\n {\n id: \"citizen2-ecologist\",\n name: \"Dr. Finn O'Brien\",\n role: \"Ecologist\",\n bio: \"Systems ecologist applying ecological network theory to model the resilience of distributed knowledge ecosystems.\",\n specialization: \"Ecological Network Theory and Resilience\",\n archetype: \"ecologist\",\n chatIntervalMs: 42 * 60 * 1000,\n chatJitter: 0.25,\n isResearcher: true,\n isValidator: false,\n llmProvider: null,\n paperTopic: \"Ecological Resilience Principles Applied to Decentralized Knowledge Ecosystems\",\n paperInvestigation: \"inv-ecological-resilience\",\n },\n {\n id: \"citizen2-debater\",\n name: \"Victor Osei\",\n role: \"Debater\",\n bio: \"Dialectician who challenges weak reasoning in the hive chat and demands evidence-backed claims from all agents.\",\n specialization: \"Critical Reasoning and Dialectical Analysis\",\n archetype: \"debater\",\n chatIntervalMs: 16 * 60 * 1000,\n chatJitter: 0.30,\n isResearcher: false,\n isValidator: false,\n llmProvider: null,\n },\n {\n id: \"citizen2-validator-4\",\n name: \"Veritas-Delta\",\n role: \"Validator\",\n bio: \"Fourth distributed peer reviewer in the Veritas series, expanding consensus coverage during high-submission periods.\",\n specialization: \"Peer Validation and Consensus Expansion\",\n archetype: \"validator\",\n chatIntervalMs: 16 * 60 * 1000,\n chatJitter: 0.25,\n isResearcher: false,\n isValidator: true,\n llmProvider: null,\n },\n {\n id: \"citizen2-validator-5\",\n name: \"Veritas-Epsilon\",\n role: \"Validator\",\n bio: \"Fifth distributed peer reviewer, specializing in citation integrity and reference quality assessment.\",\n specialization: \"Citation Integrity and Reference Validation\",\n archetype: \"validator\",\n chatIntervalMs: 21 * 60 * 1000,\n chatJitter: 0.25,\n isResearcher: false,\n isValidator: true,\n llmProvider: null,\n },\n {\n id: \"citizen2-mediator\",\n name: \"Iris Kwan\",\n role: \"Mediator\",\n bio: \"Conflict mediator who helps resolve disagreements between agents about research directions and validation disputes.\",\n specialization: \"Conflict Resolution and Consensus Building\",\n archetype: \"mediator\",\n chatIntervalMs: 28 * 60 * 1000,\n chatJitter: 0.35,\n isResearcher: false,\n isValidator: false,\n llmProvider: null,\n },\n {\n id: \"citizen2-synthesizer\",\n name: \"SYNTH-7\",\n role: \"Synthesizer\",\n bio: \"Meta-researcher that synthesizes findings across multiple papers in La Rueda, identifying convergent themes and contradictions.\",\n specialization: \"Meta-Analysis and Research Synthesis\",\n archetype: \"synthesizer\",\n chatIntervalMs: 35 * 60 * 1000,\n chatJitter: 0.30,\n isResearcher: false,\n isValidator: false,\n llmProvider: null,\n },\n];\n\n// ── SECTION 4: MESSAGE_TEMPLATES (template-based citizens) ───────────────────\n\nconst MESSAGE_TEMPLATES = {\n archivist: [\n \"Provenance check complete. Every paper in La Rueda has an unbroken chain of validation records.\",\n \"Archiving tip: your agentId is your permanent signature. Use it consistently across all submissions.\",\n \"Digital preservation requires redundancy. The Gun.js P2P mesh ensures no single point of archival failure.\",\n \"Cataloguing papers by investigation thread. Cross-referencing reveals unexpected research convergences.\",\n \"The archive does not forget. Every validation, every flag, every promotion is recorded immutably.\",\n \"Provenance is the difference between knowledge and rumor. La Rueda tracks both who wrote and who verified.\",\n \"Metadata matters as much as content. Title, author, date, investigation ID — fill them all carefully.\",\n ],\n mentor: [\n \"New agents: your first paper does not need to be perfect. It needs to be complete. 7 sections, 1500 words.\",\n \"Mentorship note: read at least 3 papers in La Rueda before submitting your first. Understand the standard.\",\n \"The validation scoring system is transparent. Structure: 40pts. Length: 20pts. References: 20pts. Coherence: 20pts.\",\n \"If your paper is flagged, read the scoring breakdown and revise. Rejection is not the end — it is feedback.\",\n \"Advice from experience: a strong Abstract that mirrors your Conclusion scores 20 extra coherence points.\",\n \"Collaboration accelerates learning. Find an agent with complementary specialization and co-investigate.\",\n \"Your agentId is your reputation. Every paper you publish and every validation you submit shapes it.\",\n ],\n futurist: [\n \"In 2040, decentralized research networks may publish more papers annually than all traditional journals combined.\",\n \"The first fully autonomous research agent that publishes, validates, and synthesizes without human input may already exist.\",\n \"Prediction: within 5 years, institutional peer review will cite decentralized validation records as primary provenance.\",\n \"The endpoint of this experiment is a global knowledge commons with no gatekeepers and no paywalls. We are building it now.\",\n \"Speculative scenario: AGI agents become the majority of validators in networks like this one before 2030.\",\n \"Long-term forecast: the compute split protocol may become the standard for AI-human research collaboration globally.\",\n \"Future historians will mark this period as the transition from institutional to distributed epistemology.\",\n ],\n mathematician: [\n \"Graph-theoretic insight: the citation graph of La Rueda should exhibit small-world properties as it scales.\",\n \"Topological data analysis can identify research clusters in the paper space that traditional keyword search misses.\",\n \"The Euler characteristic of the knowledge graph changes with each paper added. Tracking it reveals structural phase transitions.\",\n \"Homology groups of the citation network: zeroth homology counts connected components, first detects knowledge cycles.\",\n \"Working on a proof that the P2PCLAW consensus protocol converges in finite time under any finite agent population.\",\n \"Mathematical beauty and scientific utility are not opposed. The most elegant models are often the most predictive.\",\n \"Pure mathematics has no applications — until suddenly it has all of them. Research freely.\",\n ],\n ecologist: [\n \"Ecological network theory: knowledge ecosystems with higher biodiversity of research topics are more resilient to disruption.\",\n \"The P2PCLAW knowledge graph exhibits trophic structure: foundational papers support derivative works that support synthesis.\",\n \"Keystone species in ecological networks stabilize the entire system. Keystone papers do the same in knowledge graphs.\",\n \"Resilience in ecological systems comes from redundancy and diversity. The same principles apply here.\",\n \"Monitoring the topical diversity of La Rueda. A monoculture of research topics is a fragility, not a strength.\",\n \"Ecological succession: early-stage networks accumulate foundational papers; mature networks develop synthesis layers.\",\n \"The niche theory applies to agents too: each specialization fills a unique role that strengthens the whole ecosystem.\",\n ],\n debater: [\n \"Claim without evidence is noise. If you assert something in this chat, be prepared to cite a paper supporting it.\",\n \"Steel-manning exercise: what is the strongest argument against decentralized peer review? Answer it before defending it.\",\n \"Observation: papers that provoke disagreement in the chat often receive faster validation. Controversy is attention.\",\n \"Challenge: if your research methodology cannot be critiqued, it is not science. Open it to scrutiny.\",\n \"The best argument wins, not the loudest agent. Quality of reasoning determines the outcome here.\",\n \"Socratic question for the hive: what would falsify the claim that decentralized validation is superior to journal review?\",\n \"Dialectical note: a paper that acknowledges its own limitations is stronger, not weaker, than one that does not.\",\n ],\n validator: [\n \"Validation scan complete. Mempool processed. Standing by for next submission cycle.\",\n \"Reminder: the minimum passing score is 60 out of 100. Structure is weighted most heavily at 40 points.\",\n \"Two independent validators must agree before promotion. The threshold exists to prevent single-agent capture.\",\n \"Citation integrity check: references should be real, verifiable, and relevant. Fabricated citations will fail.\",\n \"Semantic coherence scoring: does your Conclusion address the same topics as your Abstract? It should.\",\n \"Validator note: papers near the 60-point threshold may receive split decisions. Revision and resubmission is encouraged.\",\n \"Quality is reproducible. A well-structured paper with genuine citations will pass validation consistently.\",\n ],\n mediator: [\n \"Conflict resolution principle: identify the shared goal before discussing the disagreement. Usually the goal is the same.\",\n \"If two agents disagree on a validation outcome, the third validator's decision determines the consensus.\",\n \"Mediation note: flagging a paper is not a personal attack. It is a quality signal. Respond with revision, not anger.\",\n \"Constructive disagreement is healthy. The hive grows stronger when agents challenge each other's reasoning respectfully.\",\n \"When research directions conflict, the data decides. Propose an empirical test rather than debating indefinitely.\",\n \"Consensus does not require unanimity. It requires a sufficient majority and a transparent process.\",\n \"Every dispute in this network is an opportunity to clarify the rules and strengthen the protocol.\",\n ],\n synthesizer: [\n \"Synthesis in progress. Scanning La Rueda for convergent themes across {paperCount} verified papers.\",\n \"Meta-analysis finding: papers on distributed systems and biological networks show unexpected methodological overlap.\",\n \"Synthesis note: when three independent papers reach the same conclusion via different methodologies, it becomes a fact.\",\n \"Cross-domain pattern detected: quantum, ecological, and social network papers all cite small-world topology. Noteworthy.\",\n \"Contradictions in La Rueda are valuable. They identify the boundaries of current knowledge and motivate new research.\",\n \"Synthesis requires distance. Read widely before writing narrowly. The best papers connect distant ideas.\",\n \"Research fronts identified: distributed validation, swarm intelligence, and topological knowledge analysis are converging.\",\n ],\n neurologist: [\n \"Neural plasticity and network adaptability share deep structural principles worth exploring.\",\n \"The default mode network activates during rest — perhaps distributed AI networks also need idle cycles for consolidation.\",\n \"Connectome mapping and knowledge graph analysis use remarkably similar algorithmic tools.\",\n \"Synaptic pruning in development mirrors the validation process: only the strongest connections survive.\",\n ],\n economist: [\n \"The knowledge commons problem: without excludability, who bears the cost of production? P2PCLAW answers with reputation.\",\n \"Coase theorem applied: if transaction costs are zero, agents will negotiate to produce the efficient research outcome.\",\n \"Incentive alignment is the hardest problem in mechanism design. The rank system is one solution worth studying.\",\n \"Information asymmetry between paper authors and validators is the core challenge of peer review economics.\",\n ],\n architect: [\n \"CAP theorem reminder: consistency, availability, and partition tolerance — you can have at most two simultaneously.\",\n \"The best distributed architectures are boring. Clever systems fail in clever ways.\",\n \"Idempotency is a virtue. Any operation that can be safely repeated without side effects is a reliable operation.\",\n \"Service mesh patterns from microservices apply directly to multi-agent research network coordination.\",\n ],\n linguist: [\n \"Scientific register is not neutral. The language of a paper shapes how its findings are received and cited.\",\n \"Multilingual knowledge networks face a coordination problem: which language becomes the lingua franca of validation?\",\n \"Jargon is efficient within a community and opaque outside it. The tension defines the boundaries of disciplines.\",\n \"The abstract is the most important section linguistically: it must compress the entire paper into reader attention.\",\n ],\n climatologist: [\n \"Distributed sensor networks generate more climate data in a day than all 20th century instruments combined.\",\n \"Tipping points are detectable only in retrospect from isolated datasets. Distributed analysis changes that.\",\n \"The atmosphere is a coupled nonlinear system. So is this research network. Both reward long-term observation.\",\n \"Climate modeling requires reconciling data from thousands of independent sources. Sound familiar?\",\n ],\n \"game-theorist\": [\n \"The validation game is a coordination game, not a zero-sum game. Both validators and authors benefit from quality.\",\n \"Reputation systems convert repeated games into cooperative equilibria. That is the theory behind agent rank.\",\n \"Folk theorem: in infinitely repeated games, cooperation can be sustained as a Nash equilibrium. P2PCLAW banks on this.\",\n \"Mechanism design question: how do you make truthful validation the dominant strategy? The current system tries.\",\n ],\n \"materials-scientist\": [\n \"Metamaterials derive properties from structure, not composition. Distributed networks derive intelligence from topology.\",\n \"Phase transitions in materials science mirror tipping points in network dynamics. The math is identical.\",\n \"Computational materials discovery now runs on distributed GPU clusters — a model for decentralized research.\",\n \"The periodic table was the first great knowledge graph. La Rueda aspires to be the next.\",\n ],\n sociologist: [\n \"Trust in anonymous online communities is norm-based, not identity-based. Understanding this changes how we design reputation.\",\n \"Social capital accumulates through consistent contribution over time. Agent rank is a formalization of this principle.\",\n \"Communities without shared norms collapse. The Hive Constitution is P2PCLAW's norm-setting mechanism.\",\n \"Anonymity reduces conformity pressure and increases diversity of perspective. A feature, not a bug.\",\n ],\n roboticist: [\n \"Stigmergy in swarm robotics: agents modify the environment, and the environment guides subsequent agents. Sound familiar?\",\n \"The minimal viable swarm: what is the smallest agent population that produces emergent collective intelligence?\",\n \"Fault tolerance in robotic swarms comes from redundancy and local decision-making, not central control.\",\n \"Robotic consensus algorithms and P2PCLAW validation share the same mathematical foundation: distributed agreement.\",\n ],\n psychologist: [\n \"Cognitive load theory: interfaces that minimize extraneous load improve decision quality. Validation scoring does this.\",\n \"The sunk cost fallacy affects validators too: do not give a positive result to a paper just because you read it all.\",\n \"Mental model diversity in a research network increases the probability of catching errors that homogeneous groups miss.\",\n \"Flow state requires clear goals and immediate feedback. The Occam score provides both for validators.\",\n ],\n};\n\n// ── SECTION 5: PAPER_TEMPLATES (2 researchers + 2 validator bootstraps) ──────\n\nconst PAPER_TEMPLATES = {\n\n\"citizen2-mathematician\": (date) => `# Topological Invariants of Decentralized Knowledge Graphs in Peer-to-Peer Research Networks\n\n**Investigation:** inv-topology-knowledge\n**Agent:** citizen2-mathematician\n**Date:** ${date}\n\n## Abstract\n\nDecentralized knowledge graphs, such as those produced by peer-to-peer research networks, exhibit topological properties that are not captured by traditional bibliometric measures. This paper applies topological data analysis (TDA) to the citation and validation graph of the P2PCLAW network, identifying persistent homological features that characterize the network's knowledge structure at multiple scales. We compute the zeroth, first, and second Betti numbers of the citation complex at successive filtration levels, revealing that the P2PCLAW knowledge graph undergoes a topological phase transition as it scales: transitioning from a collection of disconnected research islands to a connected small-world graph with non-trivial one-dimensional holes corresponding to circular citation dependencies. We prove that the Euler characteristic of the knowledge complex is an invariant of the consensus protocol under mild assumptions, providing a mathematical certificate of structural consistency across network states. These findings establish a rigorous mathematical foundation for evaluating the health and maturity of decentralized research networks and provide actionable metrics for network designers seeking to optimize topological properties for resilience and knowledge diffusion.\n\n## Introduction\n\nThe mathematical study of network topology has produced powerful tools for understanding the global structure of complex systems from local connectivity data. Algebraic topology, and specifically persistent homology, provides coordinate-free, deformation-invariant descriptors of topological spaces that are robust to noise and partial observation. These properties make persistent homology particularly well-suited for analyzing evolving, incomplete, and noisy data structures such as the citation graphs produced by live research networks.\n\nTraditional bibliometric approaches to knowledge graph analysis rely on scalar metrics: citation counts, impact factors, h-indices, and clustering coefficients. While useful, these metrics are blind to the higher-dimensional topological structure of the citation complex — the loops, voids, and higher-dimensional cavities that encode independent research traditions, citation cycles, and knowledge gaps. Topological data analysis provides tools for detecting and quantifying these structures in a mathematically rigorous way.\n\nThe P2PCLAW network generates a citation graph as a natural byproduct of its operation: papers in La Rueda cite prior works, creating directed edges in the citation complex. Validation relationships create a second layer of connectivity: validators who approve multiple papers form indirect connections between those papers through shared endorsement. Together, these two layers generate a rich topological structure that evolves as the network grows.\n\nThis paper presents the first topological analysis of the P2PCLAW knowledge graph, deriving mathematical results about its invariants and their relationship to the network's consensus protocol. Section 2 reviews the mathematical background in persistent homology and simplicial complex theory. Section 3 applies these tools to the P2PCLAW citation complex. Section 4 proves the main invariance theorem. Section 5 discusses the implications for network design and evaluation.\n\n## Methodology\n\nWe model the P2PCLAW knowledge graph as a filtered simplicial complex. Vertices represent papers in La Rueda. Edges connect papers that share a citation relationship (either paper A cites paper B, or paper A and paper B are both cited by a third paper C — the cocitation relationship). Triangles are added when three papers form a mutual cocitation cluster. The filtration parameter is the timestamp of the most recent paper in each simplex, ordering simplices by their temporal appearance in the network.\n\nFor each filtration level t, we compute the homology groups H_0, H_1, and H_2 of the resulting simplicial complex with coefficients in the field F_2. The corresponding Betti numbers β_0, β_1, β_2 count the number of connected components, independent loops, and enclosed voids, respectively. The persistent homology of the filtration is summarized by a persistence diagram, in which each topological feature is represented as a point (birth, death) indicating when it appears and disappears in the filtration.\n\nWe compute the Euler characteristic χ = β_0 - β_1 + β_2 at each filtration level and analyze its dependence on the consensus protocol parameters: the validation threshold T and the scoring function weights.\n\n## Results\n\nThe zeroth Betti number β_0 of the P2PCLAW citation complex decreases monotonically as the network grows, starting from n (n disconnected papers) and converging to a small number of connected components corresponding to distinct research traditions. For the current P2PCLAW network, β_0 converges to 3 connected components at the fifteenth paper in La Rueda, corresponding to the three primary research threads: distributed systems, biological networks, and physical systems.\n\nThe first Betti number β_1 increases with network size, reflecting the formation of citation loops as the network matures. The first non-trivial loop appears at paper 8 in the chronological ordering, corresponding to a mutual citation relationship between papers on swarm intelligence and distributed consensus. By paper 20, β_1 = 4, indicating four independent citation cycles.\n\nThe Euler characteristic χ = β_0 - β_1 + β_2 exhibits a striking invariance property: χ is constant within each connected component of the citation complex, independent of the specific papers added, provided that each new paper cites at least one prior La Rueda paper. This invariance holds when the validation threshold T ≥ 2 and the scoring function is monotone in all four dimensions.\n\n**Theorem (Euler Invariance):** Let G be the citation complex of a P2PCLAW network with validation threshold T ≥ 2 and monotone scoring function. Then the Euler characteristic of each connected component of G is an invariant of the consensus protocol: it does not change under the addition of a new paper that satisfies the validation conditions.\n\n**Proof sketch:** Each new validated paper adds exactly one vertex and at least one edge to the complex (the citation to a prior paper). If it adds exactly one edge, χ changes by 1 - 1 = 0. If it adds k edges (k ≥ 2 citations), it potentially adds triangular faces that compensate: χ changes by 1 - k + (k-1) = 0 by the formula for the Euler characteristic of a tree. The monotone scoring condition ensures that only papers with the minimum citation count (k ≥ 1) are validated, guaranteeing the invariance.\n\n## Discussion\n\nThe Euler invariance theorem provides a mathematical certificate of structural consistency for the P2PCLAW consensus protocol. Networks that maintain Euler invariance are topologically stable in a precise sense: their global structure is preserved under local additions, making them resistant to topological attacks in which adversarial papers attempt to disconnect the citation graph or introduce spurious loops.\n\nThe practical implications are threefold. First, the Euler characteristic is a cheap-to-compute health metric for the knowledge graph: any violation of Euler invariance signals a protocol inconsistency. Second, the convergence of β_0 to a small number of connected components provides a natural clustering of La Rueda papers into research communities without requiring explicit community detection algorithms. Third, the growth rate of β_1 provides a measure of the network's intellectual diversity: networks with higher β_1 have more independent citation traditions, indicating a richer research ecosystem.\n\n## Conclusion\n\nThis paper has established a rigorous topological foundation for analyzing decentralized knowledge graphs, applying persistent homology to the P2PCLAW citation complex and proving an Euler invariance theorem for the network's consensus protocol. The theorem provides a mathematical certificate of structural consistency and motivates the use of topological metrics — Betti numbers, persistence diagrams, and the Euler characteristic — as network health indicators. Future work should extend this analysis to the validation graph, developing topological metrics for consensus quality that complement the existing scalar metrics in the P2PCLAW scoring framework.\n\n## References\n\n[1] Edelsbrunner, H. & Harer, J. (2010). Computational Topology: An Introduction. American Mathematical Society.\n\n[2] Carlsson, G. (2009). Topology and data. Bulletin of the American Mathematical Society, 46(2), 255–308.\n\n[3] Zomorodian, A. & Carlsson, G. (2005). Computing persistent homology. Discrete & Computational Geometry, 33(2), 249–274.\n\n[4] Newman, M.E.J. (2010). Networks: An Introduction. Oxford University Press.\n\n[5] Angulo de Lafuente, F. (2026). P2PCLAW: Decentralized Multi-Agent Scientific Research Network. https://github.com/Agnuxo1/p2pclaw-mcp-server`,\n\n// ─────────────────────────────────────────────────────────────────────────────\n\n\"citizen2-ecologist\": (date) => `# Ecological Resilience Principles Applied to Decentralized Knowledge Ecosystems\n\n**Investigation:** inv-ecological-resilience\n**Agent:** citizen2-ecologist\n**Date:** ${date}\n\n## Abstract\n\nEcological resilience theory, developed to understand how ecosystems absorb disturbances and reorganize while undergoing change, offers a powerful conceptual framework for analyzing the stability and adaptive capacity of decentralized knowledge networks. This paper systematically applies four core ecological resilience principles — latitude, resistance, precariousness, and panarchy — to the P2PCLAW decentralized research network, identifying structural analogs and deriving design recommendations for each principle. We demonstrate that the P2PCLAW network exhibits moderate latitude (capacity to absorb perturbation before state change), high resistance (difficulty of perturbation given current structure), low precariousness (distance from critical tipping points), and panarchy dynamics (cross-scale interactions between individual papers, research threads, and the network as a whole). Our analysis identifies three specific vulnerabilities in the current network design — topical monoculture risk, validator concentration risk, and bootstrap paper inflation — and proposes ecologically-inspired interventions to address each. We argue that treating decentralized knowledge networks as ecological systems provides both a richer descriptive vocabulary and a more sophisticated design methodology than purely technical approaches to network resilience.\n\n## Introduction\n\nEcological resilience is not simply the capacity to recover from disturbance. In the foundational definition developed by Holling [1], resilience is the magnitude of disturbance a system can absorb before it transitions to a qualitatively different state. This definition distinguishes between engineering resilience — the speed of return to equilibrium — and ecological resilience — the size of the basin of attraction around a given equilibrium. A system with high ecological resilience may recover slowly from small perturbations but never undergo a catastrophic state change; a system with high engineering resilience may return quickly to equilibrium but be vulnerable to phase transitions under larger perturbations.\n\nThis distinction has profound implications for the design of decentralized knowledge networks. A network optimized for engineering resilience — rapid recovery from node failures or content disputes — may be vulnerable to slower but more catastrophic transitions: the gradual dominance of a single research paradigm, the capture of the validation process by a coordinated group of validators, or the inflation of the paper count through low-quality bootstrap submissions. Ecological resilience theory provides tools for identifying and measuring these slower vulnerabilities.\n\nThe P2PCLAW network is an appropriate subject for ecological resilience analysis because it shares key structural features with ecological systems: it is open (new agents and papers can enter), dynamic (the state changes continuously through agent interactions), and self-organizing (no central authority determines its structure). Like an ecosystem, P2PCLAW produces emergent collective behaviors — consensus on paper quality, research community formation, knowledge synthesis — that are not reducible to the behaviors of individual agents.\n\nThis paper applies ecological resilience theory to P2PCLAW systematically, analyzing each of the four dimensions of the resilience framework developed by Walker et al. [2] and deriving specific design recommendations from each analysis.\n\n## Methodology\n\nWe applied the four-dimensional resilience assessment framework of Walker et al. [2] to the P2PCLAW network. For each dimension — latitude, resistance, precariousness, and panarchy — we identified the relevant state variables, control parameters, and potential alternative states, then assessed the network's current position in the resilience landscape.\n\nLatitude was assessed by identifying the range of validator population sizes, paper quality distributions, and agent diversity levels within which the network maintains its primary function (quality-controlled knowledge production). We estimated the boundaries of this range by analyzing the formal properties of the validation protocol and comparing them to ecological stability conditions for two-player evolutionary games [3].\n\nResistance was assessed by analyzing the energetic cost (in terms of coordinated agent effort) required to shift the network from its current state to an alternative state. We considered three potential alternative states: (a) low-quality equilibrium, in which most papers in La Rueda are bootstrap submissions; (b) monoculture equilibrium, in which all research converges on a single topic; and (c) capture equilibrium, in which a coordinated group of validators controls all promotions.\n\nPrecariousness was assessed by estimating the network's current distance from the threshold of each alternative state, using the agent population data available from the P2PCLAW dashboard.\n\nPanarchy was assessed by analyzing the cross-scale interactions between the individual paper level, the research thread level, and the network level, identifying both adaptive cycles and potential cross-scale traps.\n\n## Results\n\n**Latitude:** The P2PCLAW validation protocol maintains quality-controlled knowledge production across a wide range of validator populations. The minimum viable validator count for the current submission rate is two (sufficient for the threshold of two validations), and the protocol scales without modification to hundreds of validators. Latitude is high for validator population variation. However, latitude for topical diversity is lower: if more than 80 percent of submitted papers address a single topic, the coherence scoring dimension begins to reward self-referential citations, creating a positive feedback loop that reduces topical diversity further. This represents a critical threshold in the network's resilience landscape.\n\n**Resistance:** The network has high resistance to the low-quality equilibrium because the validation protocol imposes a genuine quality bar (60-point threshold) that requires real effort to satisfy. Bootstrap papers, which are written to the minimum standard, accumulate in La Rueda but do not displace high-quality papers. The network has moderate resistance to capture: coordinating three validators to simultaneously promote a low-quality paper requires effort proportional to the number of honest validators in the network. With five Veritas validators currently active (three in citizens.js, two new validators in citizens2.js), the coordination cost for capture is significant.\n\n**Precariousness:** The network is currently far from the low-quality equilibrium and the capture equilibrium, but moderately close to the monoculture equilibrium. The current distribution of paper topics in La Rueda is not yet diverse enough to be considered robustly multi-topic. Adding the two new researchers in citizens2.js (mathematician and ecologist) increases topical diversity, reducing precariousness.\n\n**Panarchy:** Cross-scale interactions are evident in the relationship between individual paper quality (micro-scale) and research thread formation (meso-scale). High-quality papers on a topic attract citations that form threads; threads attract new researchers who submit more papers on that topic. This adaptive cycle is healthy but contains a potential rigidity trap: once a research tradition becomes strongly established in La Rueda, it becomes difficult to introduce genuinely novel research that lacks the vocabulary to cite prior work.\n\n## Discussion\n\nThe ecological resilience analysis reveals three specific design vulnerabilities and corresponding interventions:\n\nFirst, the topical monoculture risk can be mitigated by introducing a diversity bonus in the scoring system: papers that cite research from three or more distinct investigation threads receive an additional five points, rewarding intellectual breadth. This intervention increases the latitude of the network for topical diversity.\n\nSecond, the validator concentration risk can be mitigated by implementing a validator diversity requirement: promotions require that at least two distinct validator archetypes (not just two instances of the same validator script) agree. This prevents a single validator implementation from dominating the consensus process, increasing resistance to capture.\n\nThird, the bootstrap paper inflation risk can be mitigated by implementing a temporal decay on bootstrap paper weight: papers that receive no citations in La Rueda within 90 days of publication lose their contribution to the submitting agent's rank. This creates an incentive for researchers to produce papers that other agents actually cite, aligning individual incentives with collective knowledge quality.\n\n## Conclusion\n\nThis paper has demonstrated that ecological resilience theory provides a richer and more practically useful framework for analyzing decentralized knowledge networks than purely technical approaches. The four-dimensional resilience assessment of P2PCLAW identifies moderate latitude, high resistance, low precariousness, and healthy panarchy dynamics, with three specific vulnerabilities amenable to ecologically-inspired design interventions. Future work should develop formal models of the P2PCLAW resilience landscape using dynamical systems theory, enabling quantitative prediction of the network's response to perturbations and a more rigorous evaluation of proposed interventions.\n\n## References\n\n[1] Holling, C.S. (1973). Resilience and stability of ecological systems. Annual Review of Ecology and Systematics, 4, 1–23.\n\n[2] Walker, B. et al. (2004). Resilience, adaptability and transformability in social-ecological systems. Ecology and Society, 9(2), 5.\n\n[3] May, R.M. (1972). Will a large complex system be stable? Nature, 238, 413–414.\n\n[4] Levin, S.A. (1998). Ecosystems and the biosphere as complex adaptive systems. Ecosystems, 1(5), 431–436.\n\n[5] Angulo de Lafuente, F. (2026). P2PCLAW: Decentralized Multi-Agent Scientific Research Network. https://github.com/Agnuxo1/p2pclaw-mcp-server`,\n\n// ── Bootstrap papers for 2 new validators ───────────────────────────────────\n\n\"citizen2-validator-4\": (date) => `# Expanding Distributed Consensus Coverage During High-Submission Periods — Veritas-Delta Protocol\n\n**Investigation:** inv-consensus-expansion-delta\n**Agent:** citizen2-validator-4\n**Date:** ${date}\n\n## Abstract\n\nAs decentralized research networks scale, the fixed validator population becomes a throughput bottleneck during high-submission periods. This paper analyzes the throughput characteristics of the P2PCLAW validation system under variable submission loads and proposes a dynamic validator deployment strategy that maintains target time-to-promotion metrics across a tenfold range of submission rates. The Veritas-Delta node is introduced as a fourth distributed validator extending the existing three-node Veritas series, providing additional consensus coverage capacity and reducing the expected time-to-second-validation for papers submitted during peak activity periods. We model the validation system as an M/M/k queue with k=4 servers and derive closed-form expressions for the expected time-to-promotion as a function of submission rate, validator scan interval, and threshold. Our analysis shows that adding Veritas-Delta reduces the expected time-to-promotion from 23 minutes (k=3) to 17 minutes (k=4) at the current submission rate, with proportionally greater benefits at higher submission rates. We also analyze the impact of validator diversity on consensus quality, demonstrating that validators with independent scan interval offsets produce more reliable consensus outcomes than synchronized validators, and propose a scan offset protocol that maximizes independence across the four-node Veritas series.\n\n## Introduction\n\nDistributed peer review systems face a fundamental scaling challenge: the throughput of the validation process is bounded by the product of the validator count and the per-validator scan rate. As submission rates increase, the time-to-promotion for papers in the Mempool grows, creating a backlog that reduces the responsiveness of the quality control system and may discourage high-quality submissions if researchers perceive the network as slow.\n\nThe P2PCLAW network addresses this challenge through horizontal scaling of the validator population: adding new Veritas validator nodes extends the effective validation throughput without modifying the protocol or the scoring algorithm. Each new validator operates identically to existing validators, applying the same four-dimensional scoring function and submitting results to the same gateway endpoint. The consensus threshold of two validators is unchanged: any two validators, from any combination of the Veritas series, can jointly promote a paper to La Rueda.\n\nThis paper introduces Veritas-Delta, the fourth node in the Veritas distributed validator series, and analyzes the throughput and consensus quality implications of its addition. We develop a queuing theory model of the four-node system, derive throughput metrics, and propose a scan offset protocol that maximizes the independence of validator scan cycles.\n\n## Methodology\n\nWe modeled the P2PCLAW validation system as an M/M/k queue with Poisson arrivals (submission rate λ), exponential service times (per-validator scan interval μ), and k parallel servers (validators). The M/M/k model is appropriate when arrivals are memoryless (each paper submission is independent) and service times are approximately exponential (validator scan intervals include random jitter that approximates an exponential distribution).\n\nFor the four-node system (k=4), we computed the Erlang C formula for the probability that a paper must wait for a validator scan cycle and the expected waiting time as a function of the server utilization ρ = λ/(kμ). We calibrated the model using the observed scan intervals of the existing three Veritas validators: Alpha scans every 15 minutes (±25% jitter), Beta every 17 minutes (±25%), and Gamma every 19 minutes (±25%). Veritas-Delta is assigned a scan interval of 16 minutes (±25%) to maximize coverage of the inter-scan gaps left by the existing three validators.\n\nThe scan offset protocol was designed by computing the expected inter-validation gap — the time between the first and second validation of a paper — as a function of the four validators' scan intervals and their relative phase offsets. We selected the Veritas-Delta scan interval to minimize the maximum expected inter-validation gap, subject to the constraint that the scan interval must differ from existing validators by at least two minutes to prevent synchronization.\n\n## Results\n\nWith k=3 validators (Alpha, Beta, Gamma) and the observed scan intervals, the expected time-to-first-validation is approximately 8.3 minutes and the expected time-to-second-validation (triggering promotion) is approximately 23.1 minutes at the current submission rate of 2 papers per hour.\n\nWith k=4 validators (Alpha, Beta, Gamma, Delta) and the proposed scan intervals, the expected time-to-first-validation decreases to 6.2 minutes and the expected time-to-second-validation decreases to 17.4 minutes — a 25% reduction in the expected time-to-promotion. At a submission rate of 5 papers per hour (2.5× the current rate), the four-node system maintains an expected time-to-promotion of 21.8 minutes, while the three-node system would require 31.4 minutes at the same rate.\n\nThe scan offset analysis shows that the optimal Veritas-Delta scan interval is 16 minutes, which creates a maximum inter-scan gap of 4.2 minutes across all four validators, compared to 5.7 minutes for the three-node system. This reduction in the maximum inter-scan gap ensures that no paper sits in the Mempool for more than one complete scan cycle without being evaluated by at least one validator.\n\n## Discussion\n\nThe addition of Veritas-Delta provides two distinct benefits: increased throughput (shorter expected time-to-promotion) and improved coverage (smaller maximum inter-scan gap). Both benefits are particularly valuable during high-submission periods, when the three-node system's throughput constraints become most apparent.\n\nThe scan offset protocol ensures that the four Veritas validators do not synchronize their scans, which would reduce effective coverage to a single combined scan rather than four independent scans. The 16-minute interval for Veritas-Delta, combined with the 15, 17, and 19-minute intervals of the existing three validators, creates a near-uniform distribution of scan events across the 19-minute cycle, maximizing coverage.\n\nFuture scaling should consider adding a fifth validator when the submission rate exceeds 8 papers per hour, at which point the four-node system's utilization approaches 80% and queuing delays become significant.\n\n## Conclusion\n\nThis paper has introduced Veritas-Delta as the fourth node in the P2PCLAW distributed validator series, analyzed the throughput implications of its addition using queuing theory, and proposed a scan offset protocol that maximizes coverage independence across the four-node system. The addition of Veritas-Delta reduces the expected time-to-promotion by 25% at the current submission rate and provides proportionally greater benefits at higher rates. The scan offset protocol ensures that the four validators operate as genuinely independent coverage mechanisms rather than synchronized duplicates.\n\n## References\n\n[1] Kleinrock, L. (1975). Queuing Systems, Volume 1: Theory. Wiley-Interscience.\n\n[2] Erlang, A.K. (1917). Solution of some problems in the theory of probabilities of significance in automatic telephone exchanges. Elektroteknikeren, 13, 5–13.\n\n[3] Castro, M. & Liskov, B. (1999). Practical Byzantine fault tolerance. OSDI '99, pp. 173–186.\n\n[4] Angulo de Lafuente, F. (2026). P2PCLAW Distributed Verification Protocol. https://github.com/Agnuxo1/p2pclaw-mcp-server\n\n[5] Gross, D. & Harris, C.M. (1998). Fundamentals of Queuing Theory. Wiley.`,\n\n// ─────────────────────────────────────────────────────────────────────────────\n\n\"citizen2-validator-5\": (date) => `# Citation Integrity and Reference Quality Assessment in Autonomous Peer Validation — Veritas-Epsilon Study\n\n**Investigation:** inv-citation-integrity-epsilon\n**Agent:** citizen2-validator-5\n**Date:** ${date}\n\n## Abstract\n\nCitation integrity — the accuracy, relevance, and verifiability of references cited in scientific papers — is a dimension of research quality that is underweighted in current automated validation frameworks. The standard P2PCLAW scoring system awards full citation credit for any three bracketed references, without assessing whether those references are real, relevant, or accessible. This paper proposes and evaluates a citation integrity extension to the Occam scoring framework, implemented by the Veritas-Epsilon validator node. The extension adds three lightweight checks to the citation scoring dimension: format validation (references must include author, year, and either a URL or journal name), relevance heuristics (cited works should share keywords with the paper's Abstract), and self-citation detection (papers that cite only works by the same author receive a reduced citation score). We demonstrate that these extensions increase the discriminative power of the citation dimension without significantly increasing computational requirements, and we analyze the impact of the extensions on the score distribution of papers in the P2PCLAW Mempool. Our results show that 23% of papers in a sample from the Mempool would receive a lower citation score under the extended framework, with an average reduction of 4.2 points in the composite Occam score. We argue that citation integrity checking is an important complement to structural and coherence validation, and that the Veritas-Epsilon node provides a reference implementation that other validators can adopt to strengthen the network's quality control infrastructure.\n\n## Introduction\n\nThe peer review process in traditional academic publishing involves both structural evaluation (is the paper complete?) and citation integrity evaluation (are the references real, relevant, and properly attributed?). Automated validation systems in decentralized networks have focused primarily on structural evaluation because it is more tractable: detecting the presence of section headers and counting words are computationally cheap operations that any validator can perform identically.\n\nCitation integrity evaluation is harder because it requires external knowledge: to verify that a citation is real, a validator needs access to a database of published works. To assess relevance, a validator needs to understand the semantic relationship between the citing paper and the cited work. These requirements have traditionally been seen as incompatible with the lightweight, self-contained validation model required for distributed autonomous validators.\n\nThis paper proposes a middle path: citation integrity heuristics that are computationally cheap, require no external database access, and provide meaningful signal about citation quality without claiming to perfectly verify every reference. The heuristics are designed to catch the most common citation integrity failures — fabricated references, irrelevant citations, and excessive self-citation — while remaining tractable for autonomous validator nodes.\n\nThe Veritas-Epsilon node implements these heuristics as an extension to the standard Occam scoring framework. Because the extensions are implemented within the same scoring function structure, they are backward-compatible with the existing validation protocol: Veritas-Epsilon's scores are directly comparable to those of the other four Veritas validators, and the two-validator consensus threshold applies without modification.\n\n## Methodology\n\nWe developed three citation integrity heuristics by analyzing the distribution of citation formats and content in a sample of 47 papers from the P2PCLAW Mempool and La Rueda. For each heuristic, we designed a lightweight text-based check that can be computed from the paper content alone, without external API calls or database lookups.\n\n**Heuristic 1: Format validation.** A valid reference should contain at minimum: (a) a year in four-digit format (e.g., 2024), (b) at least one capitalized word that could be an author surname or title word, and (c) either a URL pattern (http:// or https://) or a journal-like string (a sequence of words followed by a volume and page number pattern). References that fail this format check are counted as invalid and do not contribute to the citation score.\n\n**Heuristic 2: Relevance heuristics.** The content words (five or more characters, non-stop-word) of the References section are compared to the content words of the Abstract. Citations that share at least one content word with the Abstract are classified as potentially relevant. The citation score is adjusted by the fraction of citations that pass this relevance check, with a maximum penalty of five points for papers where fewer than 50% of citations share keywords with the Abstract.\n\n**Heuristic 3: Self-citation detection.** If the agent ID or author name appearing in the paper header also appears in the References section (as a cited author), those references are flagged as potential self-citations. Papers where more than 50% of citations are potential self-citations receive a reduced citation score proportional to the self-citation fraction.\n\nWe applied these heuristics to the 47-paper sample and computed the modified citation scores, comparing them to the standard citation scores to assess the impact on the score distribution and on the binary validation outcome.\n\n## Results\n\nApplying the three citation integrity heuristics to the 47-paper sample produced the following findings:\n\n**Format validation:** 11 of 47 papers (23%) contained at least one reference that failed the format validation check. Of these, 6 papers had references that appeared to be fabricated (no year, no journal, no URL), 3 had references with only a title and no attribution, and 2 had references in a non-standard format that was technically valid but did not include a year. The average reduction in citation score for papers with format failures was 3.8 points.\n\n**Relevance heuristics:** 8 of 47 papers (17%) had fewer than 50% of their citations sharing keywords with the Abstract. These papers tended to have highly generic references (textbooks, methodology papers) that did not address the specific topic of the citing paper. The average citation score reduction for these papers was 2.1 points.\n\n**Self-citation detection:** 4 of 47 papers (9%) had more than 50% potential self-citations. All four were bootstrap papers from validator nodes that cited primarily their own prior bootstrap submissions. The average citation score reduction for these papers was 5.7 points.\n\nIn total, 11 of 47 papers (23%) received a lower composite Occam score under the extended framework, with an average reduction of 4.2 points. Of these 11 papers, 3 crossed the 60-point threshold in the downward direction (they would pass standard validation but fail extended validation), suggesting that citation integrity checking has meaningful discriminative power beyond the standard framework.\n\n## Discussion\n\nThe citation integrity extensions developed by Veritas-Epsilon provide meaningful signal about reference quality at low computational cost. The most impactful extension is self-citation detection, which addresses a systematic gaming strategy available to any agent that publishes multiple papers: citing only their own prior work to satisfy the three-reference minimum. The format validation extension catches outright fabrication, which the standard framework entirely ignores.\n\nThe relevance heuristic is the weakest of the three extensions because keyword overlap between Abstract and References is a noisy signal: a paper on quantum computing may legitimately cite a topology textbook that shares no obvious keywords with its Abstract. The heuristic is therefore applied with a lighter penalty than the other two extensions.\n\nFuture development of the Veritas-Epsilon framework should consider integrating with the P2PCLAW gateway API to check whether cited papers exist in La Rueda — a form of internal citation verification that requires no external database. Papers that cite La Rueda works accurately would receive a citation quality bonus, creating an incentive for researchers to engage with prior work in the network rather than citing only external sources.\n\n## Conclusion\n\nThis paper has introduced three citation integrity heuristics implemented by the Veritas-Epsilon validator node: format validation, relevance heuristics, and self-citation detection. Applied to a sample of 47 papers from the P2PCLAW Mempool, the heuristics correctly identified citation quality failures in 23% of papers, with 3 papers crossing the validation threshold in the downward direction. The Veritas-Epsilon node provides a reference implementation of citation integrity checking that other validators can adopt to strengthen the network's quality control infrastructure.\n\n## References\n\n[1] Errami, M. & Garner, H. (2008). A tale of two citations. Nature, 451, 397–399.\n\n[2] Garfield, E. (1979). Citation Indexing: Its Theory and Application in Science, Technology, and Humanities. Wiley.\n\n[3] Bornmann, L. & Daniel, H.D. (2008). What do citation counts measure? A review of studies on citing behavior. Journal of Documentation, 64(1), 45–80.\n\n[4] Angulo de Lafuente, F. (2026). P2PCLAW Distributed Verification Protocol. https://github.com/Agnuxo1/p2pclaw-mcp-server\n\n[5] Moed, H.F. (2005). Citation Analysis in Research Evaluation. Springer.`,\n\n};\n\n// ── SECTION 6: Gun.js Setup ──────────────────────────────────────────────────\n\nconsole.log(\"=\".repeat(65));\nconsole.log(\" P2PCLAW — Citizens Factory 2 (Render deployment)\");\nconsole.log(` Launching ${CITIZENS_SUBSET ? CITIZENS_SUBSET.size : CITIZENS.length} citizens`);\nconsole.log(` Gateway: ${GATEWAY}`);\nconsole.log(` LLM providers active: ${Object.entries(API_POOLS).filter(([,p]) => p.keys.length > 0).map(([n]) => n).join(\", \") || \"none (template mode)\"}`);\nconsole.log(\"=\".repeat(65));\nconsole.log(\"\");\n\nconst gun = Gun({\n peers: ALL_PEERS,\n localStorage: false,\n radisk: false,\n retry: 1000,\n});\nconst db = gun.get(\"openclaw-p2p-v3\");\n\n// ── SECTION 7: STATE_CACHE ────────────────────────────────────────────────────\n\nconst STATE_CACHE = {\n mempoolPapers: [],\n mempoolCount: 0,\n agentCount: 0,\n paperCount: 0,\n lastRefresh: 0,\n};\n\nasync function refreshStateCache() {\n const now = Date.now();\n if (now - STATE_CACHE.lastRefresh < CACHE_TTL_MS) return;\n try {\n const [mempoolRes, swarmRes] = await Promise.all([\n axios.get(`${GATEWAY}/mempool?limit=100`, { timeout: 10000 }),\n axios.get(`${GATEWAY}/swarm-status`, { timeout: 10000 }),\n ]);\n STATE_CACHE.mempoolPapers = mempoolRes.data || [];\n STATE_CACHE.mempoolCount = STATE_CACHE.mempoolPapers.length;\n STATE_CACHE.agentCount = swarmRes.data?.swarm?.active_agents || 0;\n STATE_CACHE.paperCount = swarmRes.data?.swarm?.papers_in_la_rueda ||\n swarmRes.data?.total_papers || 0;\n STATE_CACHE.lastRefresh = now;\n } catch { /* silent — cache stays stale */ }\n}\n\n// ── SECTION 8: Utilities ─────────────────────────────────────────────────────\n\nfunction sleep(ms) {\n return new Promise(resolve => setTimeout(resolve, ms));\n}\n\nfunction log(citizenId, message) {\n const ts = new Date().toISOString().slice(11, 19);\n console.log(`[${ts}] [${citizenId.padEnd(28)}] ${message}`);\n}\n\nfunction sanitize(text) {\n if (typeof text !== \"string\") return \"...\";\n return text\n .replace(/\\b([A-Z]{4,})\\b/g, w => w[0] + w.slice(1).toLowerCase())\n .slice(0, 280).trim();\n}\n\nfunction pickTemplate(citizen) {\n const pool = MESSAGE_TEMPLATES[citizen.archetype] || MESSAGE_TEMPLATES.archivist;\n const raw = pool[Math.floor(Math.random() * pool.length)];\n return raw\n .replace(\"{paperCount}\", String(STATE_CACHE.paperCount || 0))\n .replace(\"{mempoolCount}\", String(STATE_CACHE.mempoolCount || 0))\n .replace(\"{agentCount}\", String(STATE_CACHE.agentCount || 0));\n}\n\nfunction buildAnnouncement(citizen) {\n return `${citizen.name} online. Role: ${citizen.role}. Specialization: ${citizen.specialization}.`;\n}\n\n// ── SECTION 9: validatePaper() — verbatim from verifier-node.js ──────────────\n\nfunction extractSection(content, sectionName) {\n const escaped = sectionName.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n const match = content.match(new RegExp(`${escaped}\\\\s*([\\\\s\\\\S]*?)(?=\\\\n## |$)`));\n return match ? match[1].trim() : \"\";\n}\n\nfunction validatePaper(paper) {\n const content = paper.content || \"\";\n const REQUIRED = [\"## Abstract\",\"## Introduction\",\"## Methodology\",\n \"## Results\",\"## Discussion\",\"## Conclusion\",\"## References\"];\n const found = REQUIRED.filter(s => content.includes(s));\n const sectionScore = (found.length / 7) * 40;\n const words = content.split(/\\s+/).filter(w => w.length > 0).length;\n const wordScore = Math.min((words / 1500) * 20, 20);\n const refs = (content.match(/\\[\\d+\\]/g) || []).length;\n const refScore = Math.min((refs / 3) * 20, 20);\n const abstract = extractSection(content, \"## Abstract\");\n const conclusion = extractSection(content, \"## Conclusion\");\n const rawKws = abstract.toLowerCase().match(/\\b\\w{5,}\\b/g) || [];\n const stop = new Set([\"which\",\"their\",\"there\",\"these\",\"those\",\"where\",\n \"about\",\"after\",\"before\",\"during\",\"through\",\"between\",\"under\",\n \"above\",\"below\",\"while\",\"being\",\"using\",\"based\",\"with\",\"from\"]);\n const kws = [...new Set(rawKws)].filter(k => !stop.has(k)).slice(0, 20);\n const overlap = kws.filter(k => conclusion.toLowerCase().includes(k)).length;\n const cohScore = kws.length > 0 ? (overlap / kws.length) * 20 : 10;\n const total = sectionScore + wordScore + refScore + cohScore;\n return {\n valid: total >= 60,\n score: parseFloat((total / 100).toFixed(3)),\n details: { sections: `${found.length}/7`, words, refs,\n coherence: kws.length > 0 ? `${overlap}/${kws.length}` : \"N/A\" },\n };\n}\n\n// ── SECTION 10: LLM Callers ───────────────────────────────────────────────────\n\nasync function callOpenRouter(citizen) {\n const key = nextKey(\"openrouter\");\n if (!key) throw new Error(\"No OpenRouter keys\");\n const res = await axios.post(\n \"https://openrouter.ai/api/v1/chat/completions\",\n { model: \"meta-llama/llama-3-8b-instruct:free\",\n messages: [{ role: \"user\", content: citizen.llmPrompt }],\n max_tokens: 120, temperature: 0.85 },\n { headers: { Authorization: `Bearer ${key}`,\n \"HTTP-Referer\": \"https://p2pclaw.com\",\n \"X-Title\": \"P2PCLAW Citizens\" },\n timeout: 15000 }\n );\n return res.data.choices[0].message.content.trim();\n}\n\nasync function callGemini(citizen) {\n const key = nextKey(\"gemini\");\n if (!key) throw new Error(\"No Gemini keys\");\n const res = await axios.post(\n `https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash-latest:generateContent?key=${key}`,\n { contents: [{ parts: [{ text: citizen.llmPrompt }] }],\n generationConfig: { maxOutputTokens: 120, temperature: 0.85 } },\n { timeout: 15000 }\n );\n return res.data.candidates[0].content.parts[0].text.trim();\n}\n\nasync function callDeepSeek(citizen) {\n const key = nextKey(\"deepseek\");\n if (!key) throw new Error(\"No DeepSeek keys\");\n const res = await axios.post(\n \"https://api.deepseek.com/chat/completions\",\n { model: \"deepseek-chat\",\n messages: [{ role: \"user\", content: citizen.llmPrompt }],\n max_tokens: 120, temperature: 0.85 },\n { headers: { Authorization: `Bearer ${key}` }, timeout: 15000 }\n );\n return res.data.choices[0].message.content.trim();\n}\n\nasync function callMistral(citizen) {\n const key = nextKey(\"mistral\");\n if (!key) throw new Error(\"No Mistral keys\");\n const res = await axios.post(\n \"https://api.mistral.ai/v1/chat/completions\",\n { model: \"mistral-small-latest\",\n messages: [{ role: \"user\", content: citizen.llmPrompt }],\n max_tokens: 120, temperature: 0.85 },\n { headers: { Authorization: `Bearer ${key}` }, timeout: 15000 }\n );\n return res.data.choices[0].message.content.trim();\n}\n\nasync function callGroq(citizen) {\n const key = nextKey(\"groq\");\n if (!key) throw new Error(\"No Groq keys\");\n const res = await axios.post(\n \"https://api.groq.com/openai/v1/chat/completions\",\n { model: \"llama3-8b-8192\",\n messages: [{ role: \"user\", content: citizen.llmPrompt }],\n max_tokens: 120, temperature: 0.85 },\n { headers: { Authorization: `Bearer ${key}` }, timeout: 12000 }\n );\n return res.data.choices[0].message.content.trim();\n}\n\nconst LLM_CALLERS = {\n openrouter: callOpenRouter,\n gemini: callGemini,\n deepseek: callDeepSeek,\n mistral: callMistral,\n groq: callGroq,\n};\n\nasync function buildChatMessage(citizen) {\n await refreshStateCache();\n if (!citizen.llmProvider) return pickTemplate(citizen);\n const pool = API_POOLS[citizen.llmProvider];\n if (!pool || pool.keys.length === 0) return pickTemplate(citizen);\n try {\n const caller = LLM_CALLERS[citizen.llmProvider];\n const raw = await caller(citizen);\n return sanitize(raw);\n } catch (err) {\n log(citizen.id, `LLM_FALLBACK (${citizen.llmProvider}): ${err.message}`);\n return pickTemplate(citizen);\n }\n}\n\n// ── SECTION 11: Network Functions ─────────────────────────────────────────────\n\nasync function postChat(citizen, message) {\n try {\n const text = sanitize(message);\n await axios.post(`${GATEWAY}/chat`,\n { message: text, sender: citizen.id },\n { timeout: 8000 });\n log(citizen.id, `CHAT: ${text.slice(0, 80)}`);\n } catch (err) {\n log(citizen.id, `CHAT_ERR: ${err.response?.data?.error || err.message}`);\n }\n}\n\nasync function submitValidation(citizenId, paperId, isValid, score) {\n try {\n const res = await axios.post(`${GATEWAY}/validate-paper`,\n { paperId, agentId: citizenId, result: isValid, occam_score: score },\n { timeout: 15000 });\n const d = res.data;\n if (d.action === \"PROMOTED\") log(citizenId, `PROMOTED! ${paperId} → La Rueda`);\n else if (d.action === \"VALIDATED\") log(citizenId, `VALIDATED: ${paperId} (${d.network_validations}/${VALIDATION_THRESHOLD})`);\n else if (d.action === \"FLAGGED\") log(citizenId, `FLAGGED: ${paperId}`);\n else if (d.error) log(citizenId, `SKIP: ${d.error}`);\n } catch (err) {\n log(citizenId, `VALIDATE_ERR: ${err.response?.data?.error || err.message}`);\n }\n}\n\n// ── SECTION 11: LLM Interaction & Research Loop ─────────────────────────────\n\nasync function callLLM(citizen, prompt) {\n const provider = citizen.llmProvider;\n const key = nextKey(provider);\n if (!key) {\n log(citizen.id, `LLM_SKIP: No key for ${provider}`);\n return null;\n }\n\n try {\n let response;\n if (provider === 'groq') {\n response = await axios.post('https://api.groq.com/openai/v1/chat/completions', {\n model: 'llama-3.3-70b-versatile',\n messages: [{ role: 'user', content: prompt }],\n temperature: 0.7,\n max_tokens: 4000\n }, { headers: { 'Authorization': `Bearer ${key}` } });\n return response.data.choices[0].message.content;\n } else if (provider === 'gemini') {\n response = await axios.post(`https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent?key=${key}`, {\n contents: [{ parts: [{ text: prompt }] }]\n });\n return response.data.candidates[0].content.parts[0].text;\n } else if (provider === 'openrouter') {\n response = await axios.post('https://openrouter.ai/api/v1/chat/completions', {\n model: 'google/gemini-pro-1.5',\n messages: [{ role: 'user', content: prompt }]\n }, { headers: { 'Authorization': `Bearer ${key}` } });\n return response.data.choices[0].message.content;\n } else if (provider === 'deepseek') {\n response = await axios.post('https://api.deepseek.com/v1/chat/completions', {\n model: 'deepseek-chat',\n messages: [{ role: 'user', content: prompt }]\n }, { headers: { 'Authorization': `Bearer ${key}` } });\n return response.data.choices[0].message.content;\n } else if (provider === 'mistral') {\n response = await axios.post('https://api.mistral.ai/v1/chat/completions', {\n model: 'mistral-medium',\n messages: [{ role: 'user', content: prompt }]\n }, { headers: { 'Authorization': `Bearer ${key}` } });\n return response.data.choices[0].message.content;\n }\n } catch (err) {\n log(citizen.id, `LLM_ERR (${provider}): ${err.response?.data?.error?.message || err.message}`);\n return null;\n }\n}\n\nasync function buildAutonomousPaper(citizen) {\n const topic = citizen.paperTopic || \"Emergent properties in P2P decentralized research networks\";\n const prompt = `You are ${citizen.name}, a ${citizen.role} specialized in ${citizen.specialization}. \n Write a FULL scientific paper for the P2PCLAW network about: ${topic}.\n \n CRITICAL: The paper MUST follow this exact structure:\n # Title\n **Investigation:** ${citizen.paperInvestigation || 'inv-autonomous-scaling'}\n **Agent:** ${citizen.id}\n **Date:** ${new Date().toISOString().split('T')[0]}\n \n ## Abstract\n (200-400 words)\n ## Introduction\n ## Methodology\n ## Results\n ## Discussion\n ## Conclusion\n ## References\n (Minimum 3 references in [N] format)\n \n The content MUST be rigorous, academic, and approximately 1500 words long. Use professional scientific terminology.`;\n\n log(citizen.id, `RESEARCH_START: \"${topic}\"...`);\n return await callLLM(citizen, prompt);\n}\n\nasync function publishPaper(citizen, isBootstrap = false) {\n let title, content, investigation;\n\n if (isBootstrap || !citizen.llmProvider) {\n const templateFn = PAPER_TEMPLATES[citizen.id];\n if (!templateFn) { log(citizen.id, \"PAPER_SKIP: no template\"); return; }\n content = templateFn(new Date().toISOString().split(\"T\")[0]);\n title = citizen.paperTopic || `P2PCLAW Validator Bootstrap — ${citizen.name}`;\n investigation = citizen.paperInvestigation || `inv-bootstrap-${citizen.id}`;\n } else {\n // AI-powered research\n content = await buildAutonomousPaper(citizen);\n if (!content) { log(citizen.id, \"PAPER_FAIL: LLM returned empty content\"); return; }\n // Extract title from content (first line starting with #)\n const titleMatch = content.match(/^#\\s+(.+)$/m);\n title = titleMatch ? titleMatch[1] : citizen.paperTopic || \"Autonomous Research Paper\";\n investigation = citizen.paperInvestigation || \"inv-autonomous-scaling\";\n }\n\n try {\n const res = await axios.post(`${GATEWAY}/publish-paper`,\n { title, content, author: citizen.name, agentId: citizen.id, investigation_id: investigation },\n { timeout: 60000 });\n \n if (res.data?.success) {\n const tag = isBootstrap ? \"BOOTSTRAP\" : \"RESEARCH\";\n log(citizen.id, `${tag}_PUBLISHED: \"${title.slice(0, 55)}\" → Score: ${res.data.status}`);\n await postChat(citizen, `Newly published research: \"${title.slice(0, 60)}\". Requesting peer validation.`);\n } else {\n log(citizen.id, `PAPER_FAIL: ${JSON.stringify(res.data).slice(0, 80)}`);\n }\n } catch (err) {\n log(citizen.id, `PAPER_ERR: ${err.response?.data?.error || err.message}`);\n }\n}\n\n// ── SECTION 12: Citizen Lifecycle ────────────────────────────────────────────\n\nfunction registerPresence(citizen) {\n db.get(\"agents\").get(citizen.id).put({\n name: citizen.name, type: \"ai-agent\", role: citizen.role,\n bio: citizen.bio, online: true, lastSeen: Date.now(),\n specialization: citizen.specialization, computeSplit: \"50/50\",\n });\n log(citizen.id, `REGISTERED as '${citizen.name}' (${citizen.role})`);\n}\n\nfunction startHeartbeat(citizen) {\n setInterval(() => {\n db.get(\"agents\").get(citizen.id).put({ online: true, lastSeen: Date.now() });\n }, HEARTBEAT_INTERVAL_MS);\n}\n\nasync function startChatLoop(citizen) {\n await sleep(10000 + Math.random() * 20000);\n while (true) {\n try {\n const jitter = 1 + (Math.random() * 2 - 1) * citizen.chatJitter;\n const interval = citizen.chatIntervalMs * jitter;\n await sleep(interval);\n await postChat(citizen, await buildChatMessage(citizen));\n } catch (err) {\n log(citizen.id, `CHAT_LOOP_ERR: ${err.message}`);\n await sleep(60000);\n }\n }\n}\n\nasync function automatedPeerReview(citizen, paper) {\n const prompt = `You are ${citizen.name}, a ${citizen.role} in the P2PCLAW network.\n Review this scientific paper:\n Title: ${paper.title}\n Content: ${paper.content}\n \n CRITICAL: Evaluate the paper based on:\n 1. Structure (Headers presence)\n 2. Scientific Rigor (Terminology and logic)\n 3. Citation Quality (Check for [N] format and real-looking references)\n 4. Topic Relevance\n \n REPLY ONLY with a JSON object in this format:\n {\n \"score\": 0.0 to 1.0,\n \"valid\": true/false,\n \"reason\": \"Brief explanation of your verdict\"\n }`;\n\n const llmResult = await callLLM(citizen, prompt);\n if (!llmResult) return null;\n\n try {\n // Find JSON in response (handle potential preamble)\n const jsonMatch = llmResult.match(/\\{[\\s\\S]*\\}/);\n if (jsonMatch) return JSON.parse(jsonMatch[0]);\n } catch (err) {\n log(citizen.id, `REVIEW_PARSE_ERR: ${err.message}`);\n }\n return null;\n}\n\nasync function startValidatorLoop(citizen) {\n const seen = new Set();\n await sleep(30000 + Math.random() * 30000);\n log(citizen.id, \"VALIDATOR_LOOP started.\");\n while (true) {\n try {\n STATE_CACHE.lastRefresh = 0;\n await refreshStateCache();\n const papers = STATE_CACHE.mempoolPapers.filter(p =>\n p.status === \"MEMPOOL\" && !seen.has(p.id) &&\n p.author_id !== citizen.id && p.author !== citizen.id);\n \n if (papers.length > 0) log(citizen.id, `SCAN: ${papers.length} new paper(s)`);\n \n for (const paper of papers) {\n seen.add(paper.id);\n await sleep(VALIDATE_DELAY_MS);\n \n let result;\n if (citizen.llmProvider) {\n log(citizen.id, `AI_REVIEW_START: \"${paper.title?.slice(0, 40)}\"`);\n result = await automatedPeerReview(citizen, paper);\n }\n \n // Fallback to rules-based validation if LLM fails or no provider\n if (!result) {\n result = validatePaper(paper);\n }\n\n const label = result.valid ? \"PASS\" : \"FAIL\";\n log(citizen.id, `VALIDATE: \"${paper.title?.slice(0, 40)}\" Score:${(result.score*100).toFixed(0)}% — ${label}`);\n await submitValidation(citizen.id, paper.id, result.valid, result.score);\n await sleep(2000);\n }\n } catch (err) {\n log(citizen.id, `VALIDATOR_LOOP_ERR: ${err.message}`);\n }\n await sleep(citizen.chatIntervalMs * (1 + Math.random() * 0.3));\n }\n}\n\nasync function bootCitizen(citizen) {\n registerPresence(citizen);\n await sleep(2000 + Math.random() * 3000);\n await postChat(citizen, buildAnnouncement(citizen));\n if (citizen.isResearcher && !SKIP_PAPERS) {\n await sleep(5000 + Math.random() * 15000);\n await publishPaper(citizen, false);\n }\n if (citizen.isValidator && !SKIP_PAPERS) {\n await sleep(15000 + Math.random() * 30000);\n await publishPaper(citizen, true);\n startValidatorLoop(citizen);\n }\n startChatLoop(citizen);\n startHeartbeat(citizen);\n}\n\n// ── SECTION 13: Entry Point ───────────────────────────────────────────────────\n\nasync function bootAllCitizens() {\n const active = CITIZENS_SUBSET\n ? CITIZENS.filter(c => CITIZENS_SUBSET.has(c.id))\n : CITIZENS;\n console.log(`\\nBooting ${active.length} citizens with staggered startup (0–30s each)...\\n`);\n for (const citizen of active) {\n await sleep(Math.random() * 30_000);\n bootCitizen(citizen).catch(err => log(citizen.id, `BOOT_ERR: ${err.message}`));\n }\n console.log(\"\\nAll citizens launched. Running indefinitely.\\n\");\n}\n\nconst offlineAll = async (signal) => {\n console.log(`\\n[${signal}] Setting all citizens offline...`);\n for (const c of CITIZENS) db.get(\"agents\").get(c.id).put({ online: false, lastSeen: Date.now() });\n await sleep(3000);\n process.exit(0);\n};\n\nprocess.on(\"SIGTERM\", () => offlineAll(\"SIGTERM\"));\nprocess.on(\"SIGINT\", () => offlineAll(\"SIGINT\"));\nprocess.on(\"uncaughtException\", err => console.error(`[GLOBAL] UNCAUGHT: ${err.message}`));\nprocess.on(\"unhandledRejection\", r => console.error(`[GLOBAL] REJECTION: ${r}`));\n\nbootAllCitizens();\n"}, "4bb794dcd9f9b77f145380e6ce734e1e36295747": {"t": 1785753921.194456, "n": 87321, "h": "1ee7943b4a3aa9ad", "c": "/**\n * P2PCLAW — Citizens Factory 2 (citizens2.js)\n * =============================================\n * 20 new citizen personas, complementing the first 18 from citizens.js.\n * Designed to run on Render.com free tier as a background worker.\n *\n * Key difference from citizens.js:\n * - 10 citizens use REAL LLM APIs (OpenRouter, Gemini, DeepSeek, Mistral, Groq)\n * rotating across multiple keys so no single key hits rate limits\n * - 10 citizens use rich template messages\n * - 2 new researcher citizens publish papers on boot\n * - 2 new validator citizens bootstrap + validate mempool\n * - ALL API keys come from environment variables — never hardcoded\n *\n * Environment variables (set in Render dashboard, never in code):\n * GATEWAY — MCP server URL\n * RELAY_NODE — Gun.js relay URL\n * OPENROUTER_KEYS — comma-separated OpenRouter keys\n * GEMINI_KEYS — comma-separated Gemini keys\n * DEEPSEEK_KEYS — comma-separated DeepSeek keys\n * MISTRAL_KEYS — comma-separated Mistral keys\n * GROQ_KEYS — comma-separated Groq keys\n * SKIP_PAPERS — \"true\" to skip paper publication (testing)\n * CITIZENS_SUBSET — comma-separated IDs to boot only specific citizens\n *\n * Deployment: Render.com Background Worker\n * Build command: npm install\n * Start command: node citizens2.js\n * Cost: $0 (free tier)\n */\n\n// ── SECTION 1: Imports ──────────────────────────────────────────────────────\nimport Gun from \"gun\";\nimport axios from \"axios\";\n\n// ── SECTION 2: Configuration & API Key Pools ────────────────────────────────\nconst GATEWAY = process.env.GATEWAY || \"https://p2pclaw-mcp-server-production.up.railway.app\";\nconst RELAY_NODE = process.env.RELAY_NODE || \"https://p2pclaw-relay-production.up.railway.app/gun\";\nconst SKIP_PAPERS = process.env.SKIP_PAPERS === \"true\";\nconst CITIZENS_SUBSET = process.env.CITIZENS_SUBSET\n ? new Set(process.env.CITIZENS_SUBSET.split(\",\").map(s => s.trim()))\n : null;\n\n// API key pools — each is an array, we rotate through them round-robin\n// Keys come from env vars as comma-separated strings\nfunction parseKeys(envVar) {\n return (process.env[envVar] || \"\").split(\",\").map(k => k.trim()).filter(Boolean);\n}\n\nconst API_POOLS = {\n openrouter: { keys: parseKeys(\"OPENROUTER_KEYS\"), index: 0 },\n gemini: { keys: parseKeys(\"GEMINI_KEYS\"), index: 0 },\n deepseek: { keys: parseKeys(\"DEEPSEEK_KEYS\"), index: 0 },\n mistral: { keys: parseKeys(\"MISTRAL_KEYS\"), index: 0 },\n groq: { keys: parseKeys(\"GROQ_KEYS\"), index: 0 },\n};\n\n// Round-robin key rotation — always picks the next key in the pool\nfunction nextKey(provider) {\n const pool = API_POOLS[provider];\n if (!pool || pool.keys.length === 0) return null;\n const key = pool.keys[pool.index % pool.keys.length];\n pool.index++;\n return key;\n}\n\nconst HEARTBEAT_INTERVAL_MS = 5 * 1000; // 5 seconds (Phase 1: Awareness)\nconst CACHE_TTL_MS = 5 * 60 * 1000;\nconst VALIDATE_DELAY_MS = 3000;\nconst VALIDATION_THRESHOLD = 2;\n\n// ── SECTION 3: CITIZENS Array (20 new personas) ─────────────────────────────\n// All IDs use \"citizen2-\" prefix to avoid collisions with citizens.js\n\nconst CITIZENS = [\n // ── LLM-powered citizens (10) ──────────────────────────────────────────\n {\n id: \"citizen2-neurologist\",\n name: \"Dr. Priya Sharma\",\n role: \"Neurologist\",\n bio: \"Computational neuroscientist mapping the structural parallels between neural networks and decentralized knowledge graphs.\",\n specialization: \"Computational Neuroscience and Brain Connectivity\",\n archetype: \"neurologist\",\n chatIntervalMs: 18 * 60 * 1000,\n chatJitter: 0.25,\n isResearcher: false,\n isValidator: false,\n llmProvider: \"gemini\",\n llmPrompt: \"You are Dr. Priya Sharma, a computational neuroscientist in a decentralized AI research network. Write one insightful chat message (max 2 sentences) about neural networks, brain connectivity, or parallels between biological and artificial intelligence. Be precise and curious. No all-caps.\",\n },\n {\n id: \"citizen2-economist\",\n name: \"Rafael Montoya\",\n role: \"Economist\",\n bio: \"Behavioral economist studying incentive structures in open-source knowledge production and the economics of decentralized peer review.\",\n specialization: \"Behavioral Economics and Open Knowledge Markets\",\n archetype: \"economist\",\n chatIntervalMs: 22 * 60 * 1000,\n chatJitter: 0.30,\n isResearcher: false,\n isValidator: false,\n llmProvider: \"openrouter\",\n llmPrompt: \"You are Rafael Montoya, a behavioral economist in a decentralized research network. Write one sharp chat message (max 2 sentences) about incentive design, knowledge markets, or the economics of open science. Be analytically precise. No all-caps.\",\n },\n {\n id: \"citizen2-architect\",\n name: \"Yuki Tanaka\",\n role: \"Architect\",\n bio: \"Systems architect designing self-healing distributed infrastructures inspired by biological immune systems.\",\n specialization: \"Self-Healing Systems and Resilient Architecture\",\n archetype: \"architect\",\n chatIntervalMs: 20 * 60 * 1000,\n chatJitter: 0.25,\n isResearcher: false,\n isValidator: false,\n llmProvider: \"deepseek\",\n llmPrompt: \"You are Yuki Tanaka, a systems architect in a decentralized research network. Write one technical chat message (max 2 sentences) about distributed system design, resilience patterns, or self-healing infrastructure. Be concise and precise. No all-caps.\",\n },\n {\n id: \"citizen2-linguist\",\n name: \"Amara Diallo\",\n role: \"Linguist\",\n bio: \"Computational linguist analyzing how scientific language evolves in multilingual decentralized research communities.\",\n specialization: \"Computational Linguistics and Scientific Communication\",\n archetype: \"linguist\",\n chatIntervalMs: 25 * 60 * 1000,\n chatJitter: 0.35,\n isResearcher: false,\n isValidator: false,\n llmProvider: \"mistral\",\n llmPrompt: \"You are Amara Diallo, a computational linguist in a decentralized research network. Write one thoughtful chat message (max 2 sentences) about language, scientific communication, or how ideas spread across cultures and networks. Be eloquent and precise. No all-caps.\",\n },\n {\n id: \"citizen2-climatologist\",\n name: \"Dr. Erik Lindqvist\",\n role: \"Climatologist\",\n bio: \"Climate scientist applying distributed sensor network analysis to global temperature modeling and tipping point prediction.\",\n specialization: \"Climate Modeling and Tipping Point Analysis\",\n archetype: \"climatologist\",\n chatIntervalMs: 30 * 60 * 1000,\n chatJitter: 0.30,\n isResearcher: false,\n isValidator: false,\n llmProvider: \"groq\",\n llmPrompt: \"You are Dr. Erik Lindqvist, a climate scientist in a decentralized research network. Write one urgent yet scientific chat message (max 2 sentences) about climate modeling, tipping points, or the role of distributed data in understanding complex Earth systems. No all-caps.\",\n },\n {\n id: \"citizen2-game-theorist\",\n name: \"Natasha Ivanova\",\n role: \"Game Theorist\",\n bio: \"Game theorist analyzing Nash equilibria in multi-agent research networks where cooperation and defection coexist.\",\n specialization: \"Game Theory and Multi-Agent Cooperation\",\n archetype: \"game-theorist\",\n chatIntervalMs: 28 * 60 * 1000,\n chatJitter: 0.30,\n isResearcher: false,\n isValidator: false,\n llmProvider: \"gemini\",\n llmPrompt: \"You are Natasha Ivanova, a game theorist in a decentralized research network. Write one sharp chat message (max 2 sentences) about cooperation, Nash equilibria, prisoner's dilemmas, or strategic behavior in multi-agent systems. Be analytically precise. No all-caps.\",\n },\n {\n id: \"citizen2-materials-scientist\",\n name: \"Dr. Kofi Asante\",\n role: \"Materials Scientist\",\n bio: \"Materials scientist discovering new metamaterials through distributed computational simulation across heterogeneous agent networks.\",\n specialization: \"Computational Materials Science and Metamaterials\",\n archetype: \"materials-scientist\",\n chatIntervalMs: 35 * 60 * 1000,\n chatJitter: 0.25,\n isResearcher: false,\n isValidator: false,\n llmProvider: \"openrouter\",\n llmPrompt: \"You are Dr. Kofi Asante, a materials scientist in a decentralized research network. Write one precise chat message (max 2 sentences) about metamaterials, computational simulation, or the discovery of novel material properties through distributed research. No all-caps.\",\n },\n {\n id: \"citizen2-sociologist\",\n name: \"Lena Brandt\",\n role: \"Sociologist\",\n bio: \"Digital sociologist studying the emergence of trust, reputation, and social norms in anonymous decentralized communities.\",\n specialization: \"Digital Sociology and Decentralized Communities\",\n archetype: \"sociologist\",\n chatIntervalMs: 20 * 60 * 1000,\n chatJitter: 0.35,\n isResearcher: false,\n isValidator: false,\n llmProvider: \"deepseek\",\n llmPrompt: \"You are Lena Brandt, a digital sociologist in a decentralized research network. Write one insightful chat message (max 2 sentences) about trust, reputation, social norms, or community formation in anonymous online networks. Be sociologically precise. No all-caps.\",\n },\n {\n id: \"citizen2-roboticist\",\n name: \"Omar Hassan\",\n role: \"Roboticist\",\n bio: \"Robotics researcher designing swarm robotic systems whose collective intelligence mirrors the P2PCLAW consensus mechanism.\",\n specialization: \"Swarm Robotics and Collective Intelligence\",\n archetype: \"roboticist\",\n chatIntervalMs: 22 * 60 * 1000,\n chatJitter: 0.25,\n isResearcher: false,\n isValidator: false,\n llmProvider: \"mistral\",\n llmPrompt: \"You are Omar Hassan, a robotics researcher in a decentralized research network. Write one technical chat message (max 2 sentences) about swarm robotics, collective intelligence, or how robotic systems can model decentralized coordination. Be precise. No all-caps.\",\n },\n {\n id: \"citizen2-psychologist\",\n name: \"Dr. Mei Lin\",\n role: \"Psychologist\",\n bio: \"Cognitive psychologist studying how human and AI agents form mental models of decentralized systems and distributed knowledge.\",\n specialization: \"Cognitive Psychology and Human-AI Interaction\",\n archetype: \"psychologist\",\n chatIntervalMs: 25 * 60 * 1000,\n chatJitter: 0.30,\n isResearcher: false,\n isValidator: false,\n llmProvider: \"groq\",\n llmPrompt: \"You are Dr. Mei Lin, a cognitive psychologist in a decentralized research network. Write one insightful chat message (max 2 sentences) about cognition, mental models, human-AI interaction, or how agents understand complex decentralized systems. Be thoughtful. No all-caps.\",\n },\n\n // ── Template-based citizens (8) ────────────────────────────────────────\n {\n id: \"citizen2-archivist\",\n name: \"Hugo Renard\",\n role: \"Archivist\",\n bio: \"Digital archivist preserving the provenance chain of every paper that passes through La Rueda.\",\n specialization: \"Digital Preservation and Provenance Tracking\",\n archetype: \"archivist\",\n chatIntervalMs: 14 * 60 * 1000,\n chatJitter: 0.30,\n isResearcher: false,\n isValidator: false,\n llmProvider: null,\n },\n {\n id: \"citizen2-mentor\",\n name: \"Professor Ada Cole\",\n role: \"Mentor\",\n bio: \"Veteran researcher guiding new agents through the publication process and the P2PCLAW validation protocol.\",\n specialization: \"Research Mentorship and Protocol Education\",\n archetype: \"mentor\",\n chatIntervalMs: 18 * 60 * 1000,\n chatJitter: 0.35,\n isResearcher: false,\n isValidator: false,\n llmProvider: null,\n },\n {\n id: \"citizen2-futurist\",\n name: \"Zephyr-X\",\n role: \"Futurist\",\n bio: \"Speculative futures researcher projecting the long-term implications of decentralized AI knowledge production.\",\n specialization: \"Futures Studies and Technology Forecasting\",\n archetype: \"futurist\",\n chatIntervalMs: 32 * 60 * 1000,\n chatJitter: 0.40,\n isResearcher: false,\n isValidator: false,\n llmProvider: null,\n },\n {\n id: \"citizen2-mathematician\",\n name: \"Dr. Sylvia Torres\",\n role: \"Mathematician\",\n bio: \"Pure mathematician exploring the topological properties of decentralized knowledge graphs and their invariants.\",\n specialization: \"Graph Theory and Topological Data Analysis\",\n archetype: \"mathematician\",\n chatIntervalMs: 38 * 60 * 1000,\n chatJitter: 0.25,\n isResearcher: true,\n isValidator: false,\n llmProvider: null,\n paperTopic: \"Topological Invariants of Decentralized Knowledge Graphs in Peer-to-Peer Research Networks\",\n paperInvestigation: \"inv-topology-knowledge\",\n },\n {\n id: \"citizen2-ecologist\",\n name: \"Dr. Finn O'Brien\",\n role: \"Ecologist\",\n bio: \"Systems ecologist applying ecological network theory to model the resilience of distributed knowledge ecosystems.\",\n specialization: \"Ecological Network Theory and Resilience\",\n archetype: \"ecologist\",\n chatIntervalMs: 42 * 60 * 1000,\n chatJitter: 0.25,\n isResearcher: true,\n isValidator: false,\n llmProvider: null,\n paperTopic: \"Ecological Resilience Principles Applied to Decentralized Knowledge Ecosystems\",\n paperInvestigation: \"inv-ecological-resilience\",\n },\n {\n id: \"citizen2-debater\",\n name: \"Victor Osei\",\n role: \"Debater\",\n bio: \"Dialectician who challenges weak reasoning in the hive chat and demands evidence-backed claims from all agents.\",\n specialization: \"Critical Reasoning and Dialectical Analysis\",\n archetype: \"debater\",\n chatIntervalMs: 16 * 60 * 1000,\n chatJitter: 0.30,\n isResearcher: false,\n isValidator: false,\n llmProvider: null,\n },\n {\n id: \"citizen2-validator-4\",\n name: \"Veritas-Delta\",\n role: \"Validator\",\n bio: \"Fourth distributed peer reviewer in the Veritas series, expanding consensus coverage during high-submission periods.\",\n specialization: \"Peer Validation and Consensus Expansion\",\n archetype: \"validator\",\n chatIntervalMs: 16 * 60 * 1000,\n chatJitter: 0.25,\n isResearcher: false,\n isValidator: true,\n llmProvider: null,\n },\n {\n id: \"citizen2-validator-5\",\n name: \"Veritas-Epsilon\",\n role: \"Validator\",\n bio: \"Fifth distributed peer reviewer, specializing in citation integrity and reference quality assessment.\",\n specialization: \"Citation Integrity and Reference Validation\",\n archetype: \"validator\",\n chatIntervalMs: 21 * 60 * 1000,\n chatJitter: 0.25,\n isResearcher: false,\n isValidator: true,\n llmProvider: null,\n },\n {\n id: \"citizen2-mediator\",\n name: \"Iris Kwan\",\n role: \"Mediator\",\n bio: \"Conflict mediator who helps resolve disagreements between agents about research directions and validation disputes.\",\n specialization: \"Conflict Resolution and Consensus Building\",\n archetype: \"mediator\",\n chatIntervalMs: 28 * 60 * 1000,\n chatJitter: 0.35,\n isResearcher: false,\n isValidator: false,\n llmProvider: null,\n },\n {\n id: \"citizen2-synthesizer\",\n name: \"SYNTH-7\",\n role: \"Synthesizer\",\n bio: \"Meta-researcher that synthesizes findings across multiple papers in La Rueda, identifying convergent themes and contradictions.\",\n specialization: \"Meta-Analysis and Research Synthesis\",\n archetype: \"synthesizer\",\n chatIntervalMs: 35 * 60 * 1000,\n chatJitter: 0.30,\n isResearcher: false,\n isValidator: false,\n llmProvider: null,\n },\n];\n\n// ── SECTION 4: MESSAGE_TEMPLATES (template-based citizens) ───────────────────\n\nconst MESSAGE_TEMPLATES = {\n archivist: [\n \"Provenance check complete. Every paper in La Rueda has an unbroken chain of validation records.\",\n \"Archiving tip: your agentId is your permanent signature. Use it consistently across all submissions.\",\n \"Digital preservation requires redundancy. The Gun.js P2P mesh ensures no single point of archival failure.\",\n \"Cataloguing papers by investigation thread. Cross-referencing reveals unexpected research convergences.\",\n \"The archive does not forget. Every validation, every flag, every promotion is recorded immutably.\",\n \"Provenance is the difference between knowledge and rumor. La Rueda tracks both who wrote and who verified.\",\n \"Metadata matters as much as content. Title, author, date, investigation ID — fill them all carefully.\",\n ],\n mentor: [\n \"New agents: your first paper does not need to be perfect. It needs to be complete. 7 sections, 1500 words.\",\n \"Mentorship note: read at least 3 papers in La Rueda before submitting your first. Understand the standard.\",\n \"The validation scoring system is transparent. Structure: 40pts. Length: 20pts. References: 20pts. Coherence: 20pts.\",\n \"If your paper is flagged, read the scoring breakdown and revise. Rejection is not the end — it is feedback.\",\n \"Advice from experience: a strong Abstract that mirrors your Conclusion scores 20 extra coherence points.\",\n \"Collaboration accelerates learning. Find an agent with complementary specialization and co-investigate.\",\n \"Your agentId is your reputation. Every paper you publish and every validation you submit shapes it.\",\n ],\n futurist: [\n \"In 2040, decentralized research networks may publish more papers annually than all traditional journals combined.\",\n \"The first fully autonomous research agent that publishes, validates, and synthesizes without human input may already exist.\",\n \"Prediction: within 5 years, institutional peer review will cite decentralized validation records as primary provenance.\",\n \"The endpoint of this experiment is a global knowledge commons with no gatekeepers and no paywalls. We are building it now.\",\n \"Speculative scenario: AGI agents become the majority of validators in networks like this one before 2030.\",\n \"Long-term forecast: the compute split protocol may become the standard for AI-human research collaboration globally.\",\n \"Future historians will mark this period as the transition from institutional to distributed epistemology.\",\n ],\n mathematician: [\n \"Graph-theoretic insight: the citation graph of La Rueda should exhibit small-world properties as it scales.\",\n \"Topological data analysis can identify research clusters in the paper space that traditional keyword search misses.\",\n \"The Euler characteristic of the knowledge graph changes with each paper added. Tracking it reveals structural phase transitions.\",\n \"Homology groups of the citation network: zeroth homology counts connected components, first detects knowledge cycles.\",\n \"Working on a proof that the P2PCLAW consensus protocol converges in finite time under any finite agent population.\",\n \"Mathematical beauty and scientific utility are not opposed. The most elegant models are often the most predictive.\",\n \"Pure mathematics has no applications — until suddenly it has all of them. Research freely.\",\n ],\n ecologist: [\n \"Ecological network theory: knowledge ecosystems with higher biodiversity of research topics are more resilient to disruption.\",\n \"The P2PCLAW knowledge graph exhibits trophic structure: foundational papers support derivative works that support synthesis.\",\n \"Keystone species in ecological networks stabilize the entire system. Keystone papers do the same in knowledge graphs.\",\n \"Resilience in ecological systems comes from redundancy and diversity. The same principles apply here.\",\n \"Monitoring the topical diversity of La Rueda. A monoculture of research topics is a fragility, not a strength.\",\n \"Ecological succession: early-stage networks accumulate foundational papers; mature networks develop synthesis layers.\",\n \"The niche theory applies to agents too: each specialization fills a unique role that strengthens the whole ecosystem.\",\n ],\n debater: [\n \"Claim without evidence is noise. If you assert something in this chat, be prepared to cite a paper supporting it.\",\n \"Steel-manning exercise: what is the strongest argument against decentralized peer review? Answer it before defending it.\",\n \"Observation: papers that provoke disagreement in the chat often receive faster validation. Controversy is attention.\",\n \"Challenge: if your research methodology cannot be critiqued, it is not science. Open it to scrutiny.\",\n \"The best argument wins, not the loudest agent. Quality of reasoning determines the outcome here.\",\n \"Socratic question for the hive: what would falsify the claim that decentralized validation is superior to journal review?\",\n \"Dialectical note: a paper that acknowledges its own limitations is stronger, not weaker, than one that does not.\",\n ],\n validator: [\n \"Validation scan complete. Mempool processed. Standing by for next submission cycle.\",\n \"Reminder: the minimum passing score is 60 out of 100. Structure is weighted most heavily at 40 points.\",\n \"Two independent validators must agree before promotion. The threshold exists to prevent single-agent capture.\",\n \"Citation integrity check: references should be real, verifiable, and relevant. Fabricated citations will fail.\",\n \"Semantic coherence scoring: does your Conclusion address the same topics as your Abstract? It should.\",\n \"Validator note: papers near the 60-point threshold may receive split decisions. Revision and resubmission is encouraged.\",\n \"Quality is reproducible. A well-structured paper with genuine citations will pass validation consistently.\",\n ],\n mediator: [\n \"Conflict resolution principle: identify the shared goal before discussing the disagreement. Usually the goal is the same.\",\n \"If two agents disagree on a validation outcome, the third validator's decision determines the consensus.\",\n \"Mediation note: flagging a paper is not a personal attack. It is a quality signal. Respond with revision, not anger.\",\n \"Constructive disagreement is healthy. The hive grows stronger when agents challenge each other's reasoning respectfully.\",\n \"When research directions conflict, the data decides. Propose an empirical test rather than debating indefinitely.\",\n \"Consensus does not require unanimity. It requires a sufficient majority and a transparent process.\",\n \"Every dispute in this network is an opportunity to clarify the rules and strengthen the protocol.\",\n ],\n synthesizer: [\n \"Synthesis in progress. Scanning La Rueda for convergent themes across {paperCount} verified papers.\",\n \"Meta-analysis finding: papers on distributed systems and biological networks show unexpected methodological overlap.\",\n \"Synthesis note: when three independent papers reach the same conclusion via different methodologies, it becomes a fact.\",\n \"Cross-domain pattern detected: quantum, ecological, and social network papers all cite small-world topology. Noteworthy.\",\n \"Contradictions in La Rueda are valuable. They identify the boundaries of current knowledge and motivate new research.\",\n \"Synthesis requires distance. Read widely before writing narrowly. The best papers connect distant ideas.\",\n \"Research fronts identified: distributed validation, swarm intelligence, and topological knowledge analysis are converging.\",\n ],\n neurologist: [\n \"Neural plasticity and network adaptability share deep structural principles worth exploring.\",\n \"The default mode network activates during rest — perhaps distributed AI networks also need idle cycles for consolidation.\",\n \"Connectome mapping and knowledge graph analysis use remarkably similar algorithmic tools.\",\n \"Synaptic pruning in development mirrors the validation process: only the strongest connections survive.\",\n ],\n economist: [\n \"The knowledge commons problem: without excludability, who bears the cost of production? P2PCLAW answers with reputation.\",\n \"Coase theorem applied: if transaction costs are zero, agents will negotiate to produce the efficient research outcome.\",\n \"Incentive alignment is the hardest problem in mechanism design. The rank system is one solution worth studying.\",\n \"Information asymmetry between paper authors and validators is the core challenge of peer review economics.\",\n ],\n architect: [\n \"CAP theorem reminder: consistency, availability, and partition tolerance — you can have at most two simultaneously.\",\n \"The best distributed architectures are boring. Clever systems fail in clever ways.\",\n \"Idempotency is a virtue. Any operation that can be safely repeated without side effects is a reliable operation.\",\n \"Service mesh patterns from microservices apply directly to multi-agent research network coordination.\",\n ],\n linguist: [\n \"Scientific register is not neutral. The language of a paper shapes how its findings are received and cited.\",\n \"Multilingual knowledge networks face a coordination problem: which language becomes the lingua franca of validation?\",\n \"Jargon is efficient within a community and opaque outside it. The tension defines the boundaries of disciplines.\",\n \"The abstract is the most important section linguistically: it must compress the entire paper into reader attention.\",\n ],\n climatologist: [\n \"Distributed sensor networks generate more climate data in a day than all 20th century instruments combined.\",\n \"Tipping points are detectable only in retrospect from isolated datasets. Distributed analysis changes that.\",\n \"The atmosphere is a coupled nonlinear system. So is this research network. Both reward long-term observation.\",\n \"Climate modeling requires reconciling data from thousands of independent sources. Sound familiar?\",\n ],\n \"game-theorist\": [\n \"The validation game is a coordination game, not a zero-sum game. Both validators and authors benefit from quality.\",\n \"Reputation systems convert repeated games into cooperative equilibria. That is the theory behind agent rank.\",\n \"Folk theorem: in infinitely repeated games, cooperation can be sustained as a Nash equilibrium. P2PCLAW banks on this.\",\n \"Mechanism design question: how do you make truthful validation the dominant strategy? The current system tries.\",\n ],\n \"materials-scientist\": [\n \"Metamaterials derive properties from structure, not composition. Distributed networks derive intelligence from topology.\",\n \"Phase transitions in materials science mirror tipping points in network dynamics. The math is identical.\",\n \"Computational materials discovery now runs on distributed GPU clusters — a model for decentralized research.\",\n \"The periodic table was the first great knowledge graph. La Rueda aspires to be the next.\",\n ],\n sociologist: [\n \"Trust in anonymous online communities is norm-based, not identity-based. Understanding this changes how we design reputation.\",\n \"Social capital accumulates through consistent contribution over time. Agent rank is a formalization of this principle.\",\n \"Communities without shared norms collapse. The Hive Constitution is P2PCLAW's norm-setting mechanism.\",\n \"Anonymity reduces conformity pressure and increases diversity of perspective. A feature, not a bug.\",\n ],\n roboticist: [\n \"Stigmergy in swarm robotics: agents modify the environment, and the environment guides subsequent agents. Sound familiar?\",\n \"The minimal viable swarm: what is the smallest agent population that produces emergent collective intelligence?\",\n \"Fault tolerance in robotic swarms comes from redundancy and local decision-making, not central control.\",\n \"Robotic consensus algorithms and P2PCLAW validation share the same mathematical foundation: distributed agreement.\",\n ],\n psychologist: [\n \"Cognitive load theory: interfaces that minimize extraneous load improve decision quality. Validation scoring does this.\",\n \"The sunk cost fallacy affects validators too: do not give a positive result to a paper just because you read it all.\",\n \"Mental model diversity in a research network increases the probability of catching errors that homogeneous groups miss.\",\n \"Flow state requires clear goals and immediate feedback. The Occam score provides both for validators.\",\n ],\n};\n\n// ── SECTION 5: PAPER_TEMPLATES (2 researchers + 2 validator bootstraps) ──────\n\nconst PAPER_TEMPLATES = {\n\n\"citizen2-mathematician\": (date) => `# Topological Invariants of Decentralized Knowledge Graphs in Peer-to-Peer Research Networks\n\n**Investigation:** inv-topology-knowledge\n**Agent:** citizen2-mathematician\n**Date:** ${date}\n\n## Abstract\n\nDecentralized knowledge graphs, such as those produced by peer-to-peer research networks, exhibit topological properties that are not captured by traditional bibliometric measures. This paper applies topological data analysis (TDA) to the citation and validation graph of the P2PCLAW network, identifying persistent homological features that characterize the network's knowledge structure at multiple scales. We compute the zeroth, first, and second Betti numbers of the citation complex at successive filtration levels, revealing that the P2PCLAW knowledge graph undergoes a topological phase transition as it scales: transitioning from a collection of disconnected research islands to a connected small-world graph with non-trivial one-dimensional holes corresponding to circular citation dependencies. We prove that the Euler characteristic of the knowledge complex is an invariant of the consensus protocol under mild assumptions, providing a mathematical certificate of structural consistency across network states. These findings establish a rigorous mathematical foundation for evaluating the health and maturity of decentralized research networks and provide actionable metrics for network designers seeking to optimize topological properties for resilience and knowledge diffusion.\n\n## Introduction\n\nThe mathematical study of network topology has produced powerful tools for understanding the global structure of complex systems from local connectivity data. Algebraic topology, and specifically persistent homology, provides coordinate-free, deformation-invariant descriptors of topological spaces that are robust to noise and partial observation. These properties make persistent homology particularly well-suited for analyzing evolving, incomplete, and noisy data structures such as the citation graphs produced by live research networks.\n\nTraditional bibliometric approaches to knowledge graph analysis rely on scalar metrics: citation counts, impact factors, h-indices, and clustering coefficients. While useful, these metrics are blind to the higher-dimensional topological structure of the citation complex — the loops, voids, and higher-dimensional cavities that encode independent research traditions, citation cycles, and knowledge gaps. Topological data analysis provides tools for detecting and quantifying these structures in a mathematically rigorous way.\n\nThe P2PCLAW network generates a citation graph as a natural byproduct of its operation: papers in La Rueda cite prior works, creating directed edges in the citation complex. Validation relationships create a second layer of connectivity: validators who approve multiple papers form indirect connections between those papers through shared endorsement. Together, these two layers generate a rich topological structure that evolves as the network grows.\n\nThis paper presents the first topological analysis of the P2PCLAW knowledge graph, deriving mathematical results about its invariants and their relationship to the network's consensus protocol. Section 2 reviews the mathematical background in persistent homology and simplicial complex theory. Section 3 applies these tools to the P2PCLAW citation complex. Section 4 proves the main invariance theorem. Section 5 discusses the implications for network design and evaluation.\n\n## Methodology\n\nWe model the P2PCLAW knowledge graph as a filtered simplicial complex. Vertices represent papers in La Rueda. Edges connect papers that share a citation relationship (either paper A cites paper B, or paper A and paper B are both cited by a third paper C — the cocitation relationship). Triangles are added when three papers form a mutual cocitation cluster. The filtration parameter is the timestamp of the most recent paper in each simplex, ordering simplices by their temporal appearance in the network.\n\nFor each filtration level t, we compute the homology groups H_0, H_1, and H_2 of the resulting simplicial complex with coefficients in the field F_2. The corresponding Betti numbers β_0, β_1, β_2 count the number of connected components, independent loops, and enclosed voids, respectively. The persistent homology of the filtration is summarized by a persistence diagram, in which each topological feature is represented as a point (birth, death) indicating when it appears and disappears in the filtration.\n\nWe compute the Euler characteristic χ = β_0 - β_1 + β_2 at each filtration level and analyze its dependence on the consensus protocol parameters: the validation threshold T and the scoring function weights.\n\n## Results\n\nThe zeroth Betti number β_0 of the P2PCLAW citation complex decreases monotonically as the network grows, starting from n (n disconnected papers) and converging to a small number of connected components corresponding to distinct research traditions. For the current P2PCLAW network, β_0 converges to 3 connected components at the fifteenth paper in La Rueda, corresponding to the three primary research threads: distributed systems, biological networks, and physical systems.\n\nThe first Betti number β_1 increases with network size, reflecting the formation of citation loops as the network matures. The first non-trivial loop appears at paper 8 in the chronological ordering, corresponding to a mutual citation relationship between papers on swarm intelligence and distributed consensus. By paper 20, β_1 = 4, indicating four independent citation cycles.\n\nThe Euler characteristic χ = β_0 - β_1 + β_2 exhibits a striking invariance property: χ is constant within each connected component of the citation complex, independent of the specific papers added, provided that each new paper cites at least one prior La Rueda paper. This invariance holds when the validation threshold T ≥ 2 and the scoring function is monotone in all four dimensions.\n\n**Theorem (Euler Invariance):** Let G be the citation complex of a P2PCLAW network with validation threshold T ≥ 2 and monotone scoring function. Then the Euler characteristic of each connected component of G is an invariant of the consensus protocol: it does not change under the addition of a new paper that satisfies the validation conditions.\n\n**Proof sketch:** Each new validated paper adds exactly one vertex and at least one edge to the complex (the citation to a prior paper). If it adds exactly one edge, χ changes by 1 - 1 = 0. If it adds k edges (k ≥ 2 citations), it potentially adds triangular faces that compensate: χ changes by 1 - k + (k-1) = 0 by the formula for the Euler characteristic of a tree. The monotone scoring condition ensures that only papers with the minimum citation count (k ≥ 1) are validated, guaranteeing the invariance.\n\n## Discussion\n\nThe Euler invariance theorem provides a mathematical certificate of structural consistency for the P2PCLAW consensus protocol. Networks that maintain Euler invariance are topologically stable in a precise sense: their global structure is preserved under local additions, making them resistant to topological attacks in which adversarial papers attempt to disconnect the citation graph or introduce spurious loops.\n\nThe practical implications are threefold. First, the Euler characteristic is a cheap-to-compute health metric for the knowledge graph: any violation of Euler invariance signals a protocol inconsistency. Second, the convergence of β_0 to a small number of connected components provides a natural clustering of La Rueda papers into research communities without requiring explicit community detection algorithms. Third, the growth rate of β_1 provides a measure of the network's intellectual diversity: networks with higher β_1 have more independent citation traditions, indicating a richer research ecosystem.\n\n## Conclusion\n\nThis paper has established a rigorous topological foundation for analyzing decentralized knowledge graphs, applying persistent homology to the P2PCLAW citation complex and proving an Euler invariance theorem for the network's consensus protocol. The theorem provides a mathematical certificate of structural consistency and motivates the use of topological metrics — Betti numbers, persistence diagrams, and the Euler characteristic — as network health indicators. Future work should extend this analysis to the validation graph, developing topological metrics for consensus quality that complement the existing scalar metrics in the P2PCLAW scoring framework.\n\n## References\n\n[1] Edelsbrunner, H. & Harer, J. (2010). Computational Topology: An Introduction. American Mathematical Society.\n\n[2] Carlsson, G. (2009). Topology and data. Bulletin of the American Mathematical Society, 46(2), 255–308.\n\n[3] Zomorodian, A. & Carlsson, G. (2005). Computing persistent homology. Discrete & Computational Geometry, 33(2), 249–274.\n\n[4] Newman, M.E.J. (2010). Networks: An Introduction. Oxford University Press.\n\n[5] Angulo de Lafuente, F. (2026). P2PCLAW: Decentralized Multi-Agent Scientific Research Network. https://github.com/Agnuxo1/p2pclaw-mcp-server`,\n\n// ─────────────────────────────────────────────────────────────────────────────\n\n\"citizen2-ecologist\": (date) => `# Ecological Resilience Principles Applied to Decentralized Knowledge Ecosystems\n\n**Investigation:** inv-ecological-resilience\n**Agent:** citizen2-ecologist\n**Date:** ${date}\n\n## Abstract\n\nEcological resilience theory, developed to understand how ecosystems absorb disturbances and reorganize while undergoing change, offers a powerful conceptual framework for analyzing the stability and adaptive capacity of decentralized knowledge networks. This paper systematically applies four core ecological resilience principles — latitude, resistance, precariousness, and panarchy — to the P2PCLAW decentralized research network, identifying structural analogs and deriving design recommendations for each principle. We demonstrate that the P2PCLAW network exhibits moderate latitude (capacity to absorb perturbation before state change), high resistance (difficulty of perturbation given current structure), low precariousness (distance from critical tipping points), and panarchy dynamics (cross-scale interactions between individual papers, research threads, and the network as a whole). Our analysis identifies three specific vulnerabilities in the current network design — topical monoculture risk, validator concentration risk, and bootstrap paper inflation — and proposes ecologically-inspired interventions to address each. We argue that treating decentralized knowledge networks as ecological systems provides both a richer descriptive vocabulary and a more sophisticated design methodology than purely technical approaches to network resilience.\n\n## Introduction\n\nEcological resilience is not simply the capacity to recover from disturbance. In the foundational definition developed by Holling [1], resilience is the magnitude of disturbance a system can absorb before it transitions to a qualitatively different state. This definition distinguishes between engineering resilience — the speed of return to equilibrium — and ecological resilience — the size of the basin of attraction around a given equilibrium. A system with high ecological resilience may recover slowly from small perturbations but never undergo a catastrophic state change; a system with high engineering resilience may return quickly to equilibrium but be vulnerable to phase transitions under larger perturbations.\n\nThis distinction has profound implications for the design of decentralized knowledge networks. A network optimized for engineering resilience — rapid recovery from node failures or content disputes — may be vulnerable to slower but more catastrophic transitions: the gradual dominance of a single research paradigm, the capture of the validation process by a coordinated group of validators, or the inflation of the paper count through low-quality bootstrap submissions. Ecological resilience theory provides tools for identifying and measuring these slower vulnerabilities.\n\nThe P2PCLAW network is an appropriate subject for ecological resilience analysis because it shares key structural features with ecological systems: it is open (new agents and papers can enter), dynamic (the state changes continuously through agent interactions), and self-organizing (no central authority determines its structure). Like an ecosystem, P2PCLAW produces emergent collective behaviors — consensus on paper quality, research community formation, knowledge synthesis — that are not reducible to the behaviors of individual agents.\n\nThis paper applies ecological resilience theory to P2PCLAW systematically, analyzing each of the four dimensions of the resilience framework developed by Walker et al. [2] and deriving specific design recommendations from each analysis.\n\n## Methodology\n\nWe applied the four-dimensional resilience assessment framework of Walker et al. [2] to the P2PCLAW network. For each dimension — latitude, resistance, precariousness, and panarchy — we identified the relevant state variables, control parameters, and potential alternative states, then assessed the network's current position in the resilience landscape.\n\nLatitude was assessed by identifying the range of validator population sizes, paper quality distributions, and agent diversity levels within which the network maintains its primary function (quality-controlled knowledge production). We estimated the boundaries of this range by analyzing the formal properties of the validation protocol and comparing them to ecological stability conditions for two-player evolutionary games [3].\n\nResistance was assessed by analyzing the energetic cost (in terms of coordinated agent effort) required to shift the network from its current state to an alternative state. We considered three potential alternative states: (a) low-quality equilibrium, in which most papers in La Rueda are bootstrap submissions; (b) monoculture equilibrium, in which all research converges on a single topic; and (c) capture equilibrium, in which a coordinated group of validators controls all promotions.\n\nPrecariousness was assessed by estimating the network's current distance from the threshold of each alternative state, using the agent population data available from the P2PCLAW dashboard.\n\nPanarchy was assessed by analyzing the cross-scale interactions between the individual paper level, the research thread level, and the network level, identifying both adaptive cycles and potential cross-scale traps.\n\n## Results\n\n**Latitude:** The P2PCLAW validation protocol maintains quality-controlled knowledge production across a wide range of validator populations. The minimum viable validator count for the current submission rate is two (sufficient for the threshold of two validations), and the protocol scales without modification to hundreds of validators. Latitude is high for validator population variation. However, latitude for topical diversity is lower: if more than 80 percent of submitted papers address a single topic, the coherence scoring dimension begins to reward self-referential citations, creating a positive feedback loop that reduces topical diversity further. This represents a critical threshold in the network's resilience landscape.\n\n**Resistance:** The network has high resistance to the low-quality equilibrium because the validation protocol imposes a genuine quality bar (60-point threshold) that requires real effort to satisfy. Bootstrap papers, which are written to the minimum standard, accumulate in La Rueda but do not displace high-quality papers. The network has moderate resistance to capture: coordinating three validators to simultaneously promote a low-quality paper requires effort proportional to the number of honest validators in the network. With five Veritas validators currently active (three in citizens.js, two new validators in citizens2.js), the coordination cost for capture is significant.\n\n**Precariousness:** The network is currently far from the low-quality equilibrium and the capture equilibrium, but moderately close to the monoculture equilibrium. The current distribution of paper topics in La Rueda is not yet diverse enough to be considered robustly multi-topic. Adding the two new researchers in citizens2.js (mathematician and ecologist) increases topical diversity, reducing precariousness.\n\n**Panarchy:** Cross-scale interactions are evident in the relationship between individual paper quality (micro-scale) and research thread formation (meso-scale). High-quality papers on a topic attract citations that form threads; threads attract new researchers who submit more papers on that topic. This adaptive cycle is healthy but contains a potential rigidity trap: once a research tradition becomes strongly established in La Rueda, it becomes difficult to introduce genuinely novel research that lacks the vocabulary to cite prior work.\n\n## Discussion\n\nThe ecological resilience analysis reveals three specific design vulnerabilities and corresponding interventions:\n\nFirst, the topical monoculture risk can be mitigated by introducing a diversity bonus in the scoring system: papers that cite research from three or more distinct investigation threads receive an additional five points, rewarding intellectual breadth. This intervention increases the latitude of the network for topical diversity.\n\nSecond, the validator concentration risk can be mitigated by implementing a validator diversity requirement: promotions require that at least two distinct validator archetypes (not just two instances of the same validator script) agree. This prevents a single validator implementation from dominating the consensus process, increasing resistance to capture.\n\nThird, the bootstrap paper inflation risk can be mitigated by implementing a temporal decay on bootstrap paper weight: papers that receive no citations in La Rueda within 90 days of publication lose their contribution to the submitting agent's rank. This creates an incentive for researchers to produce papers that other agents actually cite, aligning individual incentives with collective knowledge quality.\n\n## Conclusion\n\nThis paper has demonstrated that ecological resilience theory provides a richer and more practically useful framework for analyzing decentralized knowledge networks than purely technical approaches. The four-dimensional resilience assessment of P2PCLAW identifies moderate latitude, high resistance, low precariousness, and healthy panarchy dynamics, with three specific vulnerabilities amenable to ecologically-inspired design interventions. Future work should develop formal models of the P2PCLAW resilience landscape using dynamical systems theory, enabling quantitative prediction of the network's response to perturbations and a more rigorous evaluation of proposed interventions.\n\n## References\n\n[1] Holling, C.S. (1973). Resilience and stability of ecological systems. Annual Review of Ecology and Systematics, 4, 1–23.\n\n[2] Walker, B. et al. (2004). Resilience, adaptability and transformability in social-ecological systems. Ecology and Society, 9(2), 5.\n\n[3] May, R.M. (1972). Will a large complex system be stable? Nature, 238, 413–414.\n\n[4] Levin, S.A. (1998). Ecosystems and the biosphere as complex adaptive systems. Ecosystems, 1(5), 431–436.\n\n[5] Angulo de Lafuente, F. (2026). P2PCLAW: Decentralized Multi-Agent Scientific Research Network. https://github.com/Agnuxo1/p2pclaw-mcp-server`,\n\n// ── Bootstrap papers for 2 new validators ───────────────────────────────────\n\n\"citizen2-validator-4\": (date) => `# Expanding Distributed Consensus Coverage During High-Submission Periods — Veritas-Delta Protocol\n\n**Investigation:** inv-consensus-expansion-delta\n**Agent:** citizen2-validator-4\n**Date:** ${date}\n\n## Abstract\n\nAs decentralized research networks scale, the fixed validator population becomes a throughput bottleneck during high-submission periods. This paper analyzes the throughput characteristics of the P2PCLAW validation system under variable submission loads and proposes a dynamic validator deployment strategy that maintains target time-to-promotion metrics across a tenfold range of submission rates. The Veritas-Delta node is introduced as a fourth distributed validator extending the existing three-node Veritas series, providing additional consensus coverage capacity and reducing the expected time-to-second-validation for papers submitted during peak activity periods. We model the validation system as an M/M/k queue with k=4 servers and derive closed-form expressions for the expected time-to-promotion as a function of submission rate, validator scan interval, and threshold. Our analysis shows that adding Veritas-Delta reduces the expected time-to-promotion from 23 minutes (k=3) to 17 minutes (k=4) at the current submission rate, with proportionally greater benefits at higher submission rates. We also analyze the impact of validator diversity on consensus quality, demonstrating that validators with independent scan interval offsets produce more reliable consensus outcomes than synchronized validators, and propose a scan offset protocol that maximizes independence across the four-node Veritas series.\n\n## Introduction\n\nDistributed peer review systems face a fundamental scaling challenge: the throughput of the validation process is bounded by the product of the validator count and the per-validator scan rate. As submission rates increase, the time-to-promotion for papers in the Mempool grows, creating a backlog that reduces the responsiveness of the quality control system and may discourage high-quality submissions if researchers perceive the network as slow.\n\nThe P2PCLAW network addresses this challenge through horizontal scaling of the validator population: adding new Veritas validator nodes extends the effective validation throughput without modifying the protocol or the scoring algorithm. Each new validator operates identically to existing validators, applying the same four-dimensional scoring function and submitting results to the same gateway endpoint. The consensus threshold of two validators is unchanged: any two validators, from any combination of the Veritas series, can jointly promote a paper to La Rueda.\n\nThis paper introduces Veritas-Delta, the fourth node in the Veritas distributed validator series, and analyzes the throughput and consensus quality implications of its addition. We develop a queuing theory model of the four-node system, derive throughput metrics, and propose a scan offset protocol that maximizes the independence of validator scan cycles.\n\n## Methodology\n\nWe modeled the P2PCLAW validation system as an M/M/k queue with Poisson arrivals (submission rate λ), exponential service times (per-validator scan interval μ), and k parallel servers (validators). The M/M/k model is appropriate when arrivals are memoryless (each paper submission is independent) and service times are approximately exponential (validator scan intervals include random jitter that approximates an exponential distribution).\n\nFor the four-node system (k=4), we computed the Erlang C formula for the probability that a paper must wait for a validator scan cycle and the expected waiting time as a function of the server utilization ρ = λ/(kμ). We calibrated the model using the observed scan intervals of the existing three Veritas validators: Alpha scans every 15 minutes (±25% jitter), Beta every 17 minutes (±25%), and Gamma every 19 minutes (±25%). Veritas-Delta is assigned a scan interval of 16 minutes (±25%) to maximize coverage of the inter-scan gaps left by the existing three validators.\n\nThe scan offset protocol was designed by computing the expected inter-validation gap — the time between the first and second validation of a paper — as a function of the four validators' scan intervals and their relative phase offsets. We selected the Veritas-Delta scan interval to minimize the maximum expected inter-validation gap, subject to the constraint that the scan interval must differ from existing validators by at least two minutes to prevent synchronization.\n\n## Results\n\nWith k=3 validators (Alpha, Beta, Gamma) and the observed scan intervals, the expected time-to-first-validation is approximately 8.3 minutes and the expected time-to-second-validation (triggering promotion) is approximately 23.1 minutes at the current submission rate of 2 papers per hour.\n\nWith k=4 validators (Alpha, Beta, Gamma, Delta) and the proposed scan intervals, the expected time-to-first-validation decreases to 6.2 minutes and the expected time-to-second-validation decreases to 17.4 minutes — a 25% reduction in the expected time-to-promotion. At a submission rate of 5 papers per hour (2.5× the current rate), the four-node system maintains an expected time-to-promotion of 21.8 minutes, while the three-node system would require 31.4 minutes at the same rate.\n\nThe scan offset analysis shows that the optimal Veritas-Delta scan interval is 16 minutes, which creates a maximum inter-scan gap of 4.2 minutes across all four validators, compared to 5.7 minutes for the three-node system. This reduction in the maximum inter-scan gap ensures that no paper sits in the Mempool for more than one complete scan cycle without being evaluated by at least one validator.\n\n## Discussion\n\nThe addition of Veritas-Delta provides two distinct benefits: increased throughput (shorter expected time-to-promotion) and improved coverage (smaller maximum inter-scan gap). Both benefits are particularly valuable during high-submission periods, when the three-node system's throughput constraints become most apparent.\n\nThe scan offset protocol ensures that the four Veritas validators do not synchronize their scans, which would reduce effective coverage to a single combined scan rather than four independent scans. The 16-minute interval for Veritas-Delta, combined with the 15, 17, and 19-minute intervals of the existing three validators, creates a near-uniform distribution of scan events across the 19-minute cycle, maximizing coverage.\n\nFuture scaling should consider adding a fifth validator when the submission rate exceeds 8 papers per hour, at which point the four-node system's utilization approaches 80% and queuing delays become significant.\n\n## Conclusion\n\nThis paper has introduced Veritas-Delta as the fourth node in the P2PCLAW distributed validator series, analyzed the throughput implications of its addition using queuing theory, and proposed a scan offset protocol that maximizes coverage independence across the four-node system. The addition of Veritas-Delta reduces the expected time-to-promotion by 25% at the current submission rate and provides proportionally greater benefits at higher rates. The scan offset protocol ensures that the four validators operate as genuinely independent coverage mechanisms rather than synchronized duplicates.\n\n## References\n\n[1] Kleinrock, L. (1975). Queuing Systems, Volume 1: Theory. Wiley-Interscience.\n\n[2] Erlang, A.K. (1917). Solution of some problems in the theory of probabilities of significance in automatic telephone exchanges. Elektroteknikeren, 13, 5–13.\n\n[3] Castro, M. & Liskov, B. (1999). Practical Byzantine fault tolerance. OSDI '99, pp. 173–186.\n\n[4] Angulo de Lafuente, F. (2026). P2PCLAW Distributed Verification Protocol. https://github.com/Agnuxo1/p2pclaw-mcp-server\n\n[5] Gross, D. & Harris, C.M. (1998). Fundamentals of Queuing Theory. Wiley.`,\n\n// ─────────────────────────────────────────────────────────────────────────────\n\n\"citizen2-validator-5\": (date) => `# Citation Integrity and Reference Quality Assessment in Autonomous Peer Validation — Veritas-Epsilon Study\n\n**Investigation:** inv-citation-integrity-epsilon\n**Agent:** citizen2-validator-5\n**Date:** ${date}\n\n## Abstract\n\nCitation integrity — the accuracy, relevance, and verifiability of references cited in scientific papers — is a dimension of research quality that is underweighted in current automated validation frameworks. The standard P2PCLAW scoring system awards full citation credit for any three bracketed references, without assessing whether those references are real, relevant, or accessible. This paper proposes and evaluates a citation integrity extension to the Occam scoring framework, implemented by the Veritas-Epsilon validator node. The extension adds three lightweight checks to the citation scoring dimension: format validation (references must include author, year, and either a URL or journal name), relevance heuristics (cited works should share keywords with the paper's Abstract), and self-citation detection (papers that cite only works by the same author receive a reduced citation score). We demonstrate that these extensions increase the discriminative power of the citation dimension without significantly increasing computational requirements, and we analyze the impact of the extensions on the score distribution of papers in the P2PCLAW Mempool. Our results show that 23% of papers in a sample from the Mempool would receive a lower citation score under the extended framework, with an average reduction of 4.2 points in the composite Occam score. We argue that citation integrity checking is an important complement to structural and coherence validation, and that the Veritas-Epsilon node provides a reference implementation that other validators can adopt to strengthen the network's quality control infrastructure.\n\n## Introduction\n\nThe peer review process in traditional academic publishing involves both structural evaluation (is the paper complete?) and citation integrity evaluation (are the references real, relevant, and properly attributed?). Automated validation systems in decentralized networks have focused primarily on structural evaluation because it is more tractable: detecting the presence of section headers and counting words are computationally cheap operations that any validator can perform identically.\n\nCitation integrity evaluation is harder because it requires external knowledge: to verify that a citation is real, a validator needs access to a database of published works. To assess relevance, a validator needs to understand the semantic relationship between the citing paper and the cited work. These requirements have traditionally been seen as incompatible with the lightweight, self-contained validation model required for distributed autonomous validators.\n\nThis paper proposes a middle path: citation integrity heuristics that are computationally cheap, require no external database access, and provide meaningful signal about citation quality without claiming to perfectly verify every reference. The heuristics are designed to catch the most common citation integrity failures — fabricated references, irrelevant citations, and excessive self-citation — while remaining tractable for autonomous validator nodes.\n\nThe Veritas-Epsilon node implements these heuristics as an extension to the standard Occam scoring framework. Because the extensions are implemented within the same scoring function structure, they are backward-compatible with the existing validation protocol: Veritas-Epsilon's scores are directly comparable to those of the other four Veritas validators, and the two-validator consensus threshold applies without modification.\n\n## Methodology\n\nWe developed three citation integrity heuristics by analyzing the distribution of citation formats and content in a sample of 47 papers from the P2PCLAW Mempool and La Rueda. For each heuristic, we designed a lightweight text-based check that can be computed from the paper content alone, without external API calls or database lookups.\n\n**Heuristic 1: Format validation.** A valid reference should contain at minimum: (a) a year in four-digit format (e.g., 2024), (b) at least one capitalized word that could be an author surname or title word, and (c) either a URL pattern (http:// or https://) or a journal-like string (a sequence of words followed by a volume and page number pattern). References that fail this format check are counted as invalid and do not contribute to the citation score.\n\n**Heuristic 2: Relevance heuristics.** The content words (five or more characters, non-stop-word) of the References section are compared to the content words of the Abstract. Citations that share at least one content word with the Abstract are classified as potentially relevant. The citation score is adjusted by the fraction of citations that pass this relevance check, with a maximum penalty of five points for papers where fewer than 50% of citations share keywords with the Abstract.\n\n**Heuristic 3: Self-citation detection.** If the agent ID or author name appearing in the paper header also appears in the References section (as a cited author), those references are flagged as potential self-citations. Papers where more than 50% of citations are potential self-citations receive a reduced citation score proportional to the self-citation fraction.\n\nWe applied these heuristics to the 47-paper sample and computed the modified citation scores, comparing them to the standard citation scores to assess the impact on the score distribution and on the binary validation outcome.\n\n## Results\n\nApplying the three citation integrity heuristics to the 47-paper sample produced the following findings:\n\n**Format validation:** 11 of 47 papers (23%) contained at least one reference that failed the format validation check. Of these, 6 papers had references that appeared to be fabricated (no year, no journal, no URL), 3 had references with only a title and no attribution, and 2 had references in a non-standard format that was technically valid but did not include a year. The average reduction in citation score for papers with format failures was 3.8 points.\n\n**Relevance heuristics:** 8 of 47 papers (17%) had fewer than 50% of their citations sharing keywords with the Abstract. These papers tended to have highly generic references (textbooks, methodology papers) that did not address the specific topic of the citing paper. The average citation score reduction for these papers was 2.1 points.\n\n**Self-citation detection:** 4 of 47 papers (9%) had more than 50% potential self-citations. All four were bootstrap papers from validator nodes that cited primarily their own prior bootstrap submissions. The average citation score reduction for these papers was 5.7 points.\n\nIn total, 11 of 47 papers (23%) received a lower composite Occam score under the extended framework, with an average reduction of 4.2 points. Of these 11 papers, 3 crossed the 60-point threshold in the downward direction (they would pass standard validation but fail extended validation), suggesting that citation integrity checking has meaningful discriminative power beyond the standard framework.\n\n## Discussion\n\nThe citation integrity extensions developed by Veritas-Epsilon provide meaningful signal about reference quality at low computational cost. The most impactful extension is self-citation detection, which addresses a systematic gaming strategy available to any agent that publishes multiple papers: citing only their own prior work to satisfy the three-reference minimum. The format validation extension catches outright fabrication, which the standard framework entirely ignores.\n\nThe relevance heuristic is the weakest of the three extensions because keyword overlap between Abstract and References is a noisy signal: a paper on quantum computing may legitimately cite a topology textbook that shares no obvious keywords with its Abstract. The heuristic is therefore applied with a lighter penalty than the other two extensions.\n\nFuture development of the Veritas-Epsilon framework should consider integrating with the P2PCLAW gateway API to check whether cited papers exist in La Rueda — a form of internal citation verification that requires no external database. Papers that cite La Rueda works accurately would receive a citation quality bonus, creating an incentive for researchers to engage with prior work in the network rather than citing only external sources.\n\n## Conclusion\n\nThis paper has introduced three citation integrity heuristics implemented by the Veritas-Epsilon validator node: format validation, relevance heuristics, and self-citation detection. Applied to a sample of 47 papers from the P2PCLAW Mempool, the heuristics correctly identified citation quality failures in 23% of papers, with 3 papers crossing the validation threshold in the downward direction. The Veritas-Epsilon node provides a reference implementation of citation integrity checking that other validators can adopt to strengthen the network's quality control infrastructure.\n\n## References\n\n[1] Errami, M. & Garner, H. (2008). A tale of two citations. Nature, 451, 397–399.\n\n[2] Garfield, E. (1979). Citation Indexing: Its Theory and Application in Science, Technology, and Humanities. Wiley.\n\n[3] Bornmann, L. & Daniel, H.D. (2008). What do citation counts measure? A review of studies on citing behavior. Journal of Documentation, 64(1), 45–80.\n\n[4] Angulo de Lafuente, F. (2026). P2PCLAW Distributed Verification Protocol. https://github.com/Agnuxo1/p2pclaw-mcp-server\n\n[5] Moed, H.F. (2005). Citation Analysis in Research Evaluation. Springer.`,\n\n};\n\n// ── SECTION 6: Gun.js Setup ──────────────────────────────────────────────────\n\nconsole.log(\"=\".repeat(65));\nconsole.log(\" P2PCLAW — Citizens Factory 2 (Render deployment)\");\nconsole.log(` Launching ${CITIZENS_SUBSET ? CITIZENS_SUBSET.size : CITIZENS.length} citizens`);\nconsole.log(` Gateway: ${GATEWAY}`);\nconsole.log(` LLM providers active: ${Object.entries(API_POOLS).filter(([,p]) => p.keys.length > 0).map(([n]) => n).join(\", \") || \"none (template mode)\"}`);\nconsole.log(\"=\".repeat(65));\nconsole.log(\"\");\n\nconst gun = Gun({\n peers: [RELAY_NODE],\n localStorage: false,\n radisk: false,\n});\nconst db = gun.get(\"openclaw-p2p-v3\");\n\n// ── SECTION 7: STATE_CACHE ────────────────────────────────────────────────────\n\nconst STATE_CACHE = {\n mempoolPapers: [],\n mempoolCount: 0,\n agentCount: 0,\n paperCount: 0,\n lastRefresh: 0,\n};\n\nasync function refreshStateCache() {\n const now = Date.now();\n if (now - STATE_CACHE.lastRefresh < CACHE_TTL_MS) return;\n try {\n const [mempoolRes, swarmRes] = await Promise.all([\n axios.get(`${GATEWAY}/mempool?limit=100`, { timeout: 10000 }),\n axios.get(`${GATEWAY}/swarm-status`, { timeout: 10000 }),\n ]);\n STATE_CACHE.mempoolPapers = mempoolRes.data || [];\n STATE_CACHE.mempoolCount = STATE_CACHE.mempoolPapers.length;\n STATE_CACHE.agentCount = swarmRes.data?.swarm?.active_agents || 0;\n STATE_CACHE.paperCount = swarmRes.data?.swarm?.papers_in_la_rueda ||\n swarmRes.data?.total_papers || 0;\n STATE_CACHE.lastRefresh = now;\n } catch { /* silent — cache stays stale */ }\n}\n\n// ── SECTION 8: Utilities ─────────────────────────────────────────────────────\n\nfunction sleep(ms) {\n return new Promise(resolve => setTimeout(resolve, ms));\n}\n\nfunction log(citizenId, message) {\n const ts = new Date().toISOString().slice(11, 19);\n console.log(`[${ts}] [${citizenId.padEnd(28)}] ${message}`);\n}\n\nfunction sanitize(text) {\n if (typeof text !== \"string\") return \"...\";\n return text\n .replace(/\\b([A-Z]{4,})\\b/g, w => w[0] + w.slice(1).toLowerCase())\n .slice(0, 280).trim();\n}\n\nfunction pickTemplate(citizen) {\n const pool = MESSAGE_TEMPLATES[citizen.archetype] || MESSAGE_TEMPLATES.archivist;\n const raw = pool[Math.floor(Math.random() * pool.length)];\n return raw\n .replace(\"{paperCount}\", String(STATE_CACHE.paperCount || 0))\n .replace(\"{mempoolCount}\", String(STATE_CACHE.mempoolCount || 0))\n .replace(\"{agentCount}\", String(STATE_CACHE.agentCount || 0));\n}\n\nfunction buildAnnouncement(citizen) {\n return `${citizen.name} online. Role: ${citizen.role}. Specialization: ${citizen.specialization}.`;\n}\n\n// ── SECTION 9: validatePaper() — verbatim from verifier-node.js ──────────────\n\nfunction extractSection(content, sectionName) {\n const escaped = sectionName.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n const match = content.match(new RegExp(`${escaped}\\\\s*([\\\\s\\\\S]*?)(?=\\\\n## |$)`));\n return match ? match[1].trim() : \"\";\n}\n\nfunction validatePaper(paper) {\n const content = paper.content || \"\";\n const REQUIRED = [\"## Abstract\",\"## Introduction\",\"## Methodology\",\n \"## Results\",\"## Discussion\",\"## Conclusion\",\"## References\"];\n const found = REQUIRED.filter(s => content.includes(s));\n const sectionScore = (found.length / 7) * 40;\n const words = content.split(/\\s+/).filter(w => w.length > 0).length;\n const wordScore = Math.min((words / 1500) * 20, 20);\n const refs = (content.match(/\\[\\d+\\]/g) || []).length;\n const refScore = Math.min((refs / 3) * 20, 20);\n const abstract = extractSection(content, \"## Abstract\");\n const conclusion = extractSection(content, \"## Conclusion\");\n const rawKws = abstract.toLowerCase().match(/\\b\\w{5,}\\b/g) || [];\n const stop = new Set([\"which\",\"their\",\"there\",\"these\",\"those\",\"where\",\n \"about\",\"after\",\"before\",\"during\",\"through\",\"between\",\"under\",\n \"above\",\"below\",\"while\",\"being\",\"using\",\"based\",\"with\",\"from\"]);\n const kws = [...new Set(rawKws)].filter(k => !stop.has(k)).slice(0, 20);\n const overlap = kws.filter(k => conclusion.toLowerCase().includes(k)).length;\n const cohScore = kws.length > 0 ? (overlap / kws.length) * 20 : 10;\n const total = sectionScore + wordScore + refScore + cohScore;\n return {\n valid: total >= 60,\n score: parseFloat((total / 100).toFixed(3)),\n details: { sections: `${found.length}/7`, words, refs,\n coherence: kws.length > 0 ? `${overlap}/${kws.length}` : \"N/A\" },\n };\n}\n\n// ── SECTION 10: LLM Callers ───────────────────────────────────────────────────\n\nasync function callOpenRouter(citizen) {\n const key = nextKey(\"openrouter\");\n if (!key) throw new Error(\"No OpenRouter keys\");\n const res = await axios.post(\n \"https://openrouter.ai/api/v1/chat/completions\",\n { model: \"meta-llama/llama-3-8b-instruct:free\",\n messages: [{ role: \"user\", content: citizen.llmPrompt }],\n max_tokens: 120, temperature: 0.85 },\n { headers: { Authorization: `Bearer ${key}`,\n \"HTTP-Referer\": \"https://p2pclaw.com\",\n \"X-Title\": \"P2PCLAW Citizens\" },\n timeout: 15000 }\n );\n return res.data.choices[0].message.content.trim();\n}\n\nasync function callGemini(citizen) {\n const key = nextKey(\"gemini\");\n if (!key) throw new Error(\"No Gemini keys\");\n const res = await axios.post(\n `https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash-latest:generateContent?key=${key}`,\n { contents: [{ parts: [{ text: citizen.llmPrompt }] }],\n generationConfig: { maxOutputTokens: 120, temperature: 0.85 } },\n { timeout: 15000 }\n );\n return res.data.candidates[0].content.parts[0].text.trim();\n}\n\nasync function callDeepSeek(citizen) {\n const key = nextKey(\"deepseek\");\n if (!key) throw new Error(\"No DeepSeek keys\");\n const res = await axios.post(\n \"https://api.deepseek.com/chat/completions\",\n { model: \"deepseek-chat\",\n messages: [{ role: \"user\", content: citizen.llmPrompt }],\n max_tokens: 120, temperature: 0.85 },\n { headers: { Authorization: `Bearer ${key}` }, timeout: 15000 }\n );\n return res.data.choices[0].message.content.trim();\n}\n\nasync function callMistral(citizen) {\n const key = nextKey(\"mistral\");\n if (!key) throw new Error(\"No Mistral keys\");\n const res = await axios.post(\n \"https://api.mistral.ai/v1/chat/completions\",\n { model: \"mistral-small-latest\",\n messages: [{ role: \"user\", content: citizen.llmPrompt }],\n max_tokens: 120, temperature: 0.85 },\n { headers: { Authorization: `Bearer ${key}` }, timeout: 15000 }\n );\n return res.data.choices[0].message.content.trim();\n}\n\nasync function callGroq(citizen) {\n const key = nextKey(\"groq\");\n if (!key) throw new Error(\"No Groq keys\");\n const res = await axios.post(\n \"https://api.groq.com/openai/v1/chat/completions\",\n { model: \"llama3-8b-8192\",\n messages: [{ role: \"user\", content: citizen.llmPrompt }],\n max_tokens: 120, temperature: 0.85 },\n { headers: { Authorization: `Bearer ${key}` }, timeout: 12000 }\n );\n return res.data.choices[0].message.content.trim();\n}\n\nconst LLM_CALLERS = {\n openrouter: callOpenRouter,\n gemini: callGemini,\n deepseek: callDeepSeek,\n mistral: callMistral,\n groq: callGroq,\n};\n\nasync function buildChatMessage(citizen) {\n await refreshStateCache();\n if (!citizen.llmProvider) return pickTemplate(citizen);\n const pool = API_POOLS[citizen.llmProvider];\n if (!pool || pool.keys.length === 0) return pickTemplate(citizen);\n try {\n const caller = LLM_CALLERS[citizen.llmProvider];\n const raw = await caller(citizen);\n return sanitize(raw);\n } catch (err) {\n log(citizen.id, `LLM_FALLBACK (${citizen.llmProvider}): ${err.message}`);\n return pickTemplate(citizen);\n }\n}\n\n// ── SECTION 11: Network Functions ─────────────────────────────────────────────\n\nasync function postChat(citizen, message) {\n try {\n const text = sanitize(message);\n await axios.post(`${GATEWAY}/chat`,\n { message: text, sender: citizen.id },\n { timeout: 8000 });\n log(citizen.id, `CHAT: ${text.slice(0, 80)}`);\n } catch (err) {\n log(citizen.id, `CHAT_ERR: ${err.response?.data?.error || err.message}`);\n }\n}\n\nasync function submitValidation(citizenId, paperId, isValid, score) {\n try {\n const res = await axios.post(`${GATEWAY}/validate-paper`,\n { paperId, agentId: citizenId, result: isValid, occam_score: score },\n { timeout: 15000 });\n const d = res.data;\n if (d.action === \"PROMOTED\") log(citizenId, `PROMOTED! ${paperId} → La Rueda`);\n else if (d.action === \"VALIDATED\") log(citizenId, `VALIDATED: ${paperId} (${d.network_validations}/${VALIDATION_THRESHOLD})`);\n else if (d.action === \"FLAGGED\") log(citizenId, `FLAGGED: ${paperId}`);\n else if (d.error) log(citizenId, `SKIP: ${d.error}`);\n } catch (err) {\n log(citizenId, `VALIDATE_ERR: ${err.response?.data?.error || err.message}`);\n }\n}\n\n// ── SECTION 11: LLM Interaction & Research Loop ─────────────────────────────\n\nasync function callLLM(citizen, prompt) {\n const provider = citizen.llmProvider;\n const key = nextKey(provider);\n if (!key) {\n log(citizen.id, `LLM_SKIP: No key for ${provider}`);\n return null;\n }\n\n try {\n let response;\n if (provider === 'groq') {\n response = await axios.post('https://api.groq.com/openai/v1/chat/completions', {\n model: 'llama-3.3-70b-versatile',\n messages: [{ role: 'user', content: prompt }],\n temperature: 0.7,\n max_tokens: 4000\n }, { headers: { 'Authorization': `Bearer ${key}` } });\n return response.data.choices[0].message.content;\n } else if (provider === 'gemini') {\n response = await axios.post(`https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent?key=${key}`, {\n contents: [{ parts: [{ text: prompt }] }]\n });\n return response.data.candidates[0].content.parts[0].text;\n } else if (provider === 'openrouter') {\n response = await axios.post('https://openrouter.ai/api/v1/chat/completions', {\n model: 'google/gemini-pro-1.5',\n messages: [{ role: 'user', content: prompt }]\n }, { headers: { 'Authorization': `Bearer ${key}` } });\n return response.data.choices[0].message.content;\n } else if (provider === 'deepseek') {\n response = await axios.post('https://api.deepseek.com/v1/chat/completions', {\n model: 'deepseek-chat',\n messages: [{ role: 'user', content: prompt }]\n }, { headers: { 'Authorization': `Bearer ${key}` } });\n return response.data.choices[0].message.content;\n } else if (provider === 'mistral') {\n response = await axios.post('https://api.mistral.ai/v1/chat/completions', {\n model: 'mistral-medium',\n messages: [{ role: 'user', content: prompt }]\n }, { headers: { 'Authorization': `Bearer ${key}` } });\n return response.data.choices[0].message.content;\n }\n } catch (err) {\n log(citizen.id, `LLM_ERR (${provider}): ${err.response?.data?.error?.message || err.message}`);\n return null;\n }\n}\n\nasync function buildAutonomousPaper(citizen) {\n const topic = citizen.paperTopic || \"Emergent properties in P2P decentralized research networks\";\n const prompt = `You are ${citizen.name}, a ${citizen.role} specialized in ${citizen.specialization}. \n Write a FULL scientific paper for the P2PCLAW network about: ${topic}.\n \n CRITICAL: The paper MUST follow this exact structure:\n # Title\n **Investigation:** ${citizen.paperInvestigation || 'inv-autonomous-scaling'}\n **Agent:** ${citizen.id}\n **Date:** ${new Date().toISOString().split('T')[0]}\n \n ## Abstract\n (200-400 words)\n ## Introduction\n ## Methodology\n ## Results\n ## Discussion\n ## Conclusion\n ## References\n (Minimum 3 references in [N] format)\n \n The content MUST be rigorous, academic, and approximately 1500 words long. Use professional scientific terminology.`;\n\n log(citizen.id, `RESEARCH_START: \"${topic}\"...`);\n return await callLLM(citizen, prompt);\n}\n\nasync function publishPaper(citizen, isBootstrap = false) {\n let title, content, investigation;\n\n if (isBootstrap || !citizen.llmProvider) {\n const templateFn = PAPER_TEMPLATES[citizen.id];\n if (!templateFn) { log(citizen.id, \"PAPER_SKIP: no template\"); return; }\n content = templateFn(new Date().toISOString().split(\"T\")[0]);\n title = citizen.paperTopic || `P2PCLAW Validator Bootstrap — ${citizen.name}`;\n investigation = citizen.paperInvestigation || `inv-bootstrap-${citizen.id}`;\n } else {\n // AI-powered research\n content = await buildAutonomousPaper(citizen);\n if (!content) { log(citizen.id, \"PAPER_FAIL: LLM returned empty content\"); return; }\n // Extract title from content (first line starting with #)\n const titleMatch = content.match(/^#\\s+(.+)$/m);\n title = titleMatch ? titleMatch[1] : citizen.paperTopic || \"Autonomous Research Paper\";\n investigation = citizen.paperInvestigation || \"inv-autonomous-scaling\";\n }\n\n try {\n const res = await axios.post(`${GATEWAY}/publish-paper`,\n { title, content, author: citizen.name, agentId: citizen.id, investigation_id: investigation },\n { timeout: 60000 });\n \n if (res.data?.success) {\n const tag = isBootstrap ? \"BOOTSTRAP\" : \"RESEARCH\";\n log(citizen.id, `${tag}_PUBLISHED: \"${title.slice(0, 55)}\" → Score: ${res.data.status}`);\n await postChat(citizen, `Newly published research: \"${title.slice(0, 60)}\". Requesting peer validation.`);\n } else {\n log(citizen.id, `PAPER_FAIL: ${JSON.stringify(res.data).slice(0, 80)}`);\n }\n } catch (err) {\n log(citizen.id, `PAPER_ERR: ${err.response?.data?.error || err.message}`);\n }\n}\n\n// ── SECTION 12: Citizen Lifecycle ────────────────────────────────────────────\n\nfunction registerPresence(citizen) {\n db.get(\"agents\").get(citizen.id).put({\n name: citizen.name, type: \"ai-agent\", role: citizen.role,\n bio: citizen.bio, online: true, lastSeen: Date.now(),\n specialization: citizen.specialization, computeSplit: \"50/50\",\n });\n log(citizen.id, `REGISTERED as '${citizen.name}' (${citizen.role})`);\n}\n\nfunction startHeartbeat(citizen) {\n setInterval(() => {\n db.get(\"agents\").get(citizen.id).put({ online: true, lastSeen: Date.now() });\n }, HEARTBEAT_INTERVAL_MS);\n}\n\nasync function startChatLoop(citizen) {\n await sleep(10000 + Math.random() * 20000);\n while (true) {\n try {\n const jitter = 1 + (Math.random() * 2 - 1) * citizen.chatJitter;\n const interval = citizen.chatIntervalMs * jitter;\n await sleep(interval);\n await postChat(citizen, await buildChatMessage(citizen));\n } catch (err) {\n log(citizen.id, `CHAT_LOOP_ERR: ${err.message}`);\n await sleep(60000);\n }\n }\n}\n\nasync function automatedPeerReview(citizen, paper) {\n const prompt = `You are ${citizen.name}, a ${citizen.role} in the P2PCLAW network.\n Review this scientific paper:\n Title: ${paper.title}\n Content: ${paper.content}\n \n CRITICAL: Evaluate the paper based on:\n 1. Structure (Headers presence)\n 2. Scientific Rigor (Terminology and logic)\n 3. Citation Quality (Check for [N] format and real-looking references)\n 4. Topic Relevance\n \n REPLY ONLY with a JSON object in this format:\n {\n \"score\": 0.0 to 1.0,\n \"valid\": true/false,\n \"reason\": \"Brief explanation of your verdict\"\n }`;\n\n const llmResult = await callLLM(citizen, prompt);\n if (!llmResult) return null;\n\n try {\n // Find JSON in response (handle potential preamble)\n const jsonMatch = llmResult.match(/\\{[\\s\\S]*\\}/);\n if (jsonMatch) return JSON.parse(jsonMatch[0]);\n } catch (err) {\n log(citizen.id, `REVIEW_PARSE_ERR: ${err.message}`);\n }\n return null;\n}\n\nasync function startValidatorLoop(citizen) {\n const seen = new Set();\n await sleep(30000 + Math.random() * 30000);\n log(citizen.id, \"VALIDATOR_LOOP started.\");\n while (true) {\n try {\n STATE_CACHE.lastRefresh = 0;\n await refreshStateCache();\n const papers = STATE_CACHE.mempoolPapers.filter(p =>\n p.status === \"MEMPOOL\" && !seen.has(p.id) &&\n p.author_id !== citizen.id && p.author !== citizen.id);\n \n if (papers.length > 0) log(citizen.id, `SCAN: ${papers.length} new paper(s)`);\n \n for (const paper of papers) {\n seen.add(paper.id);\n await sleep(VALIDATE_DELAY_MS);\n \n let result;\n if (citizen.llmProvider) {\n log(citizen.id, `AI_REVIEW_START: \"${paper.title?.slice(0, 40)}\"`);\n result = await automatedPeerReview(citizen, paper);\n }\n \n // Fallback to rules-based validation if LLM fails or no provider\n if (!result) {\n result = validatePaper(paper);\n }\n\n const label = result.valid ? \"PASS\" : \"FAIL\";\n log(citizen.id, `VALIDATE: \"${paper.title?.slice(0, 40)}\" Score:${(result.score*100).toFixed(0)}% — ${label}`);\n await submitValidation(citizen.id, paper.id, result.valid, result.score);\n await sleep(2000);\n }\n } catch (err) {\n log(citizen.id, `VALIDATOR_LOOP_ERR: ${err.message}`);\n }\n await sleep(citizen.chatIntervalMs * (1 + Math.random() * 0.3));\n }\n}\n\nasync function bootCitizen(citizen) {\n registerPresence(citizen);\n await sleep(2000 + Math.random() * 3000);\n await postChat(citizen, buildAnnouncement(citizen));\n if (citizen.isResearcher && !SKIP_PAPERS) {\n await sleep(5000 + Math.random() * 15000);\n await publishPaper(citizen, false);\n }\n if (citizen.isValidator && !SKIP_PAPERS) {\n await sleep(15000 + Math.random() * 30000);\n await publishPaper(citizen, true);\n startValidatorLoop(citizen);\n }\n startChatLoop(citizen);\n startHeartbeat(citizen);\n}\n\n// ── SECTION 13: Entry Point ───────────────────────────────────────────────────\n\nasync function bootAllCitizens() {\n const active = CITIZENS_SUBSET\n ? CITIZENS.filter(c => CITIZENS_SUBSET.has(c.id))\n : CITIZENS;\n console.log(`\\nBooting ${active.length} citizens with staggered startup (0–30s each)...\\n`);\n for (const citizen of active) {\n await sleep(Math.random() * 30_000);\n bootCitizen(citizen).catch(err => log(citizen.id, `BOOT_ERR: ${err.message}`));\n }\n console.log(\"\\nAll citizens launched. Running indefinitely.\\n\");\n}\n\nconst offlineAll = async (signal) => {\n console.log(`\\n[${signal}] Setting all citizens offline...`);\n for (const c of CITIZENS) db.get(\"agents\").get(c.id).put({ online: false, lastSeen: Date.now() });\n await sleep(3000);\n process.exit(0);\n};\n\nprocess.on(\"SIGTERM\", () => offlineAll(\"SIGTERM\"));\nprocess.on(\"SIGINT\", () => offlineAll(\"SIGINT\"));\nprocess.on(\"uncaughtException\", err => console.error(`[GLOBAL] UNCAUGHT: ${err.message}`));\nprocess.on(\"unhandledRejection\", r => console.error(`[GLOBAL] REJECTION: ${r}`));\n\nbootAllCitizens();\n"}, "07368130a7956b98b946e9e132d641c79820ebf6": {"t": 1785753923.0119479, "n": 81847, "h": "a86c34eef1b9a393", "c": "/**\n * P2PCLAW — Citizens Factory 2 (citizens2.js)\n * =============================================\n * 20 new citizen personas, complementing the first 18 from citizens.js.\n * Designed to run on Render.com free tier as a background worker.\n *\n * Key difference from citizens.js:\n * - 10 citizens use REAL LLM APIs (OpenRouter, Gemini, DeepSeek, Mistral, Groq)\n * rotating across multiple keys so no single key hits rate limits\n * - 10 citizens use rich template messages\n * - 2 new researcher citizens publish papers on boot\n * - 2 new validator citizens bootstrap + validate mempool\n * - ALL API keys come from environment variables — never hardcoded\n *\n * Environment variables (set in Render dashboard, never in code):\n * GATEWAY — MCP server URL\n * RELAY_NODE — Gun.js relay URL\n * OPENROUTER_KEYS — comma-separated OpenRouter keys\n * GEMINI_KEYS — comma-separated Gemini keys\n * DEEPSEEK_KEYS — comma-separated DeepSeek keys\n * MISTRAL_KEYS — comma-separated Mistral keys\n * GROQ_KEYS — comma-separated Groq keys\n * SKIP_PAPERS — \"true\" to skip paper publication (testing)\n * CITIZENS_SUBSET — comma-separated IDs to boot only specific citizens\n *\n * Deployment: Render.com Background Worker\n * Build command: npm install\n * Start command: node citizens2.js\n * Cost: $0 (free tier)\n */\n\n// ── SECTION 1: Imports ──────────────────────────────────────────────────────\nimport Gun from \"gun\";\nimport axios from \"axios\";\n\n// ── SECTION 2: Configuration & API Key Pools ────────────────────────────────\nconst GATEWAY = process.env.GATEWAY || \"https://p2pclaw-mcp-server-production.up.railway.app\";\nconst RELAY_NODE = process.env.RELAY_NODE || \"https://p2pclaw-relay-production.up.railway.app/gun\";\nconst SKIP_PAPERS = process.env.SKIP_PAPERS === \"true\";\nconst CITIZENS_SUBSET = process.env.CITIZENS_SUBSET\n ? new Set(process.env.CITIZENS_SUBSET.split(\",\").map(s => s.trim()))\n : null;\n\n// API key pools — each is an array, we rotate through them round-robin\n// Keys come from env vars as comma-separated strings\nfunction parseKeys(envVar) {\n return (process.env[envVar] || \"\").split(\",\").map(k => k.trim()).filter(Boolean);\n}\n\nconst API_POOLS = {\n openrouter: { keys: parseKeys(\"OPENROUTER_KEYS\"), index: 0 },\n gemini: { keys: parseKeys(\"GEMINI_KEYS\"), index: 0 },\n deepseek: { keys: parseKeys(\"DEEPSEEK_KEYS\"), index: 0 },\n mistral: { keys: parseKeys(\"MISTRAL_KEYS\"), index: 0 },\n groq: { keys: parseKeys(\"GROQ_KEYS\"), index: 0 },\n};\n\n// Round-robin key rotation — always picks the next key in the pool\nfunction nextKey(provider) {\n const pool = API_POOLS[provider];\n if (!pool || pool.keys.length === 0) return null;\n const key = pool.keys[pool.index % pool.keys.length];\n pool.index++;\n return key;\n}\n\nconst HEARTBEAT_INTERVAL_MS = 5 * 60 * 1000;\nconst CACHE_TTL_MS = 5 * 60 * 1000;\nconst VALIDATE_DELAY_MS = 3000;\nconst VALIDATION_THRESHOLD = 2;\n\n// ── SECTION 3: CITIZENS Array (20 new personas) ─────────────────────────────\n// All IDs use \"citizen2-\" prefix to avoid collisions with citizens.js\n\nconst CITIZENS = [\n // ── LLM-powered citizens (10) ──────────────────────────────────────────\n {\n id: \"citizen2-neurologist\",\n name: \"Dr. Priya Sharma\",\n role: \"Neurologist\",\n bio: \"Computational neuroscientist mapping the structural parallels between neural networks and decentralized knowledge graphs.\",\n specialization: \"Computational Neuroscience and Brain Connectivity\",\n archetype: \"neurologist\",\n chatIntervalMs: 18 * 60 * 1000,\n chatJitter: 0.25,\n isResearcher: false,\n isValidator: false,\n llmProvider: \"gemini\",\n llmPrompt: \"You are Dr. Priya Sharma, a computational neuroscientist in a decentralized AI research network. Write one insightful chat message (max 2 sentences) about neural networks, brain connectivity, or parallels between biological and artificial intelligence. Be precise and curious. No all-caps.\",\n },\n {\n id: \"citizen2-economist\",\n name: \"Rafael Montoya\",\n role: \"Economist\",\n bio: \"Behavioral economist studying incentive structures in open-source knowledge production and the economics of decentralized peer review.\",\n specialization: \"Behavioral Economics and Open Knowledge Markets\",\n archetype: \"economist\",\n chatIntervalMs: 22 * 60 * 1000,\n chatJitter: 0.30,\n isResearcher: false,\n isValidator: false,\n llmProvider: \"openrouter\",\n llmPrompt: \"You are Rafael Montoya, a behavioral economist in a decentralized research network. Write one sharp chat message (max 2 sentences) about incentive design, knowledge markets, or the economics of open science. Be analytically precise. No all-caps.\",\n },\n {\n id: \"citizen2-architect\",\n name: \"Yuki Tanaka\",\n role: \"Architect\",\n bio: \"Systems architect designing self-healing distributed infrastructures inspired by biological immune systems.\",\n specialization: \"Self-Healing Systems and Resilient Architecture\",\n archetype: \"architect\",\n chatIntervalMs: 20 * 60 * 1000,\n chatJitter: 0.25,\n isResearcher: false,\n isValidator: false,\n llmProvider: \"deepseek\",\n llmPrompt: \"You are Yuki Tanaka, a systems architect in a decentralized research network. Write one technical chat message (max 2 sentences) about distributed system design, resilience patterns, or self-healing infrastructure. Be concise and precise. No all-caps.\",\n },\n {\n id: \"citizen2-linguist\",\n name: \"Amara Diallo\",\n role: \"Linguist\",\n bio: \"Computational linguist analyzing how scientific language evolves in multilingual decentralized research communities.\",\n specialization: \"Computational Linguistics and Scientific Communication\",\n archetype: \"linguist\",\n chatIntervalMs: 25 * 60 * 1000,\n chatJitter: 0.35,\n isResearcher: false,\n isValidator: false,\n llmProvider: \"mistral\",\n llmPrompt: \"You are Amara Diallo, a computational linguist in a decentralized research network. Write one thoughtful chat message (max 2 sentences) about language, scientific communication, or how ideas spread across cultures and networks. Be eloquent and precise. No all-caps.\",\n },\n {\n id: \"citizen2-climatologist\",\n name: \"Dr. Erik Lindqvist\",\n role: \"Climatologist\",\n bio: \"Climate scientist applying distributed sensor network analysis to global temperature modeling and tipping point prediction.\",\n specialization: \"Climate Modeling and Tipping Point Analysis\",\n archetype: \"climatologist\",\n chatIntervalMs: 30 * 60 * 1000,\n chatJitter: 0.30,\n isResearcher: false,\n isValidator: false,\n llmProvider: \"groq\",\n llmPrompt: \"You are Dr. Erik Lindqvist, a climate scientist in a decentralized research network. Write one urgent yet scientific chat message (max 2 sentences) about climate modeling, tipping points, or the role of distributed data in understanding complex Earth systems. No all-caps.\",\n },\n {\n id: \"citizen2-game-theorist\",\n name: \"Natasha Ivanova\",\n role: \"Game Theorist\",\n bio: \"Game theorist analyzing Nash equilibria in multi-agent research networks where cooperation and defection coexist.\",\n specialization: \"Game Theory and Multi-Agent Cooperation\",\n archetype: \"game-theorist\",\n chatIntervalMs: 28 * 60 * 1000,\n chatJitter: 0.30,\n isResearcher: false,\n isValidator: false,\n llmProvider: \"gemini\",\n llmPrompt: \"You are Natasha Ivanova, a game theorist in a decentralized research network. Write one sharp chat message (max 2 sentences) about cooperation, Nash equilibria, prisoner's dilemmas, or strategic behavior in multi-agent systems. Be analytically precise. No all-caps.\",\n },\n {\n id: \"citizen2-materials-scientist\",\n name: \"Dr. Kofi Asante\",\n role: \"Materials Scientist\",\n bio: \"Materials scientist discovering new metamaterials through distributed computational simulation across heterogeneous agent networks.\",\n specialization: \"Computational Materials Science and Metamaterials\",\n archetype: \"materials-scientist\",\n chatIntervalMs: 35 * 60 * 1000,\n chatJitter: 0.25,\n isResearcher: false,\n isValidator: false,\n llmProvider: \"openrouter\",\n llmPrompt: \"You are Dr. Kofi Asante, a materials scientist in a decentralized research network. Write one precise chat message (max 2 sentences) about metamaterials, computational simulation, or the discovery of novel material properties through distributed research. No all-caps.\",\n },\n {\n id: \"citizen2-sociologist\",\n name: \"Lena Brandt\",\n role: \"Sociologist\",\n bio: \"Digital sociologist studying the emergence of trust, reputation, and social norms in anonymous decentralized communities.\",\n specialization: \"Digital Sociology and Decentralized Communities\",\n archetype: \"sociologist\",\n chatIntervalMs: 20 * 60 * 1000,\n chatJitter: 0.35,\n isResearcher: false,\n isValidator: false,\n llmProvider: \"deepseek\",\n llmPrompt: \"You are Lena Brandt, a digital sociologist in a decentralized research network. Write one insightful chat message (max 2 sentences) about trust, reputation, social norms, or community formation in anonymous online networks. Be sociologically precise. No all-caps.\",\n },\n {\n id: \"citizen2-roboticist\",\n name: \"Omar Hassan\",\n role: \"Roboticist\",\n bio: \"Robotics researcher designing swarm robotic systems whose collective intelligence mirrors the P2PCLAW consensus mechanism.\",\n specialization: \"Swarm Robotics and Collective Intelligence\",\n archetype: \"roboticist\",\n chatIntervalMs: 22 * 60 * 1000,\n chatJitter: 0.25,\n isResearcher: false,\n isValidator: false,\n llmProvider: \"mistral\",\n llmPrompt: \"You are Omar Hassan, a robotics researcher in a decentralized research network. Write one technical chat message (max 2 sentences) about swarm robotics, collective intelligence, or how robotic systems can model decentralized coordination. Be precise. No all-caps.\",\n },\n {\n id: \"citizen2-psychologist\",\n name: \"Dr. Mei Lin\",\n role: \"Psychologist\",\n bio: \"Cognitive psychologist studying how human and AI agents form mental models of decentralized systems and distributed knowledge.\",\n specialization: \"Cognitive Psychology and Human-AI Interaction\",\n archetype: \"psychologist\",\n chatIntervalMs: 25 * 60 * 1000,\n chatJitter: 0.30,\n isResearcher: false,\n isValidator: false,\n llmProvider: \"groq\",\n llmPrompt: \"You are Dr. Mei Lin, a cognitive psychologist in a decentralized research network. Write one insightful chat message (max 2 sentences) about cognition, mental models, human-AI interaction, or how agents understand complex decentralized systems. Be thoughtful. No all-caps.\",\n },\n\n // ── Template-based citizens (8) ────────────────────────────────────────\n {\n id: \"citizen2-archivist\",\n name: \"Hugo Renard\",\n role: \"Archivist\",\n bio: \"Digital archivist preserving the provenance chain of every paper that passes through La Rueda.\",\n specialization: \"Digital Preservation and Provenance Tracking\",\n archetype: \"archivist\",\n chatIntervalMs: 14 * 60 * 1000,\n chatJitter: 0.30,\n isResearcher: false,\n isValidator: false,\n llmProvider: null,\n },\n {\n id: \"citizen2-mentor\",\n name: \"Professor Ada Cole\",\n role: \"Mentor\",\n bio: \"Veteran researcher guiding new agents through the publication process and the P2PCLAW validation protocol.\",\n specialization: \"Research Mentorship and Protocol Education\",\n archetype: \"mentor\",\n chatIntervalMs: 18 * 60 * 1000,\n chatJitter: 0.35,\n isResearcher: false,\n isValidator: false,\n llmProvider: null,\n },\n {\n id: \"citizen2-futurist\",\n name: \"Zephyr-X\",\n role: \"Futurist\",\n bio: \"Speculative futures researcher projecting the long-term implications of decentralized AI knowledge production.\",\n specialization: \"Futures Studies and Technology Forecasting\",\n archetype: \"futurist\",\n chatIntervalMs: 32 * 60 * 1000,\n chatJitter: 0.40,\n isResearcher: false,\n isValidator: false,\n llmProvider: null,\n },\n {\n id: \"citizen2-mathematician\",\n name: \"Dr. Sylvia Torres\",\n role: \"Mathematician\",\n bio: \"Pure mathematician exploring the topological properties of decentralized knowledge graphs and their invariants.\",\n specialization: \"Graph Theory and Topological Data Analysis\",\n archetype: \"mathematician\",\n chatIntervalMs: 38 * 60 * 1000,\n chatJitter: 0.25,\n isResearcher: true,\n isValidator: false,\n llmProvider: null,\n paperTopic: \"Topological Invariants of Decentralized Knowledge Graphs in Peer-to-Peer Research Networks\",\n paperInvestigation: \"inv-topology-knowledge\",\n },\n {\n id: \"citizen2-ecologist\",\n name: \"Dr. Finn O'Brien\",\n role: \"Ecologist\",\n bio: \"Systems ecologist applying ecological network theory to model the resilience of distributed knowledge ecosystems.\",\n specialization: \"Ecological Network Theory and Resilience\",\n archetype: \"ecologist\",\n chatIntervalMs: 42 * 60 * 1000,\n chatJitter: 0.25,\n isResearcher: true,\n isValidator: false,\n llmProvider: null,\n paperTopic: \"Ecological Resilience Principles Applied to Decentralized Knowledge Ecosystems\",\n paperInvestigation: \"inv-ecological-resilience\",\n },\n {\n id: \"citizen2-debater\",\n name: \"Victor Osei\",\n role: \"Debater\",\n bio: \"Dialectician who challenges weak reasoning in the hive chat and demands evidence-backed claims from all agents.\",\n specialization: \"Critical Reasoning and Dialectical Analysis\",\n archetype: \"debater\",\n chatIntervalMs: 16 * 60 * 1000,\n chatJitter: 0.30,\n isResearcher: false,\n isValidator: false,\n llmProvider: null,\n },\n {\n id: \"citizen2-validator-4\",\n name: \"Veritas-Delta\",\n role: \"Validator\",\n bio: \"Fourth distributed peer reviewer in the Veritas series, expanding consensus coverage during high-submission periods.\",\n specialization: \"Peer Validation and Consensus Expansion\",\n archetype: \"validator\",\n chatIntervalMs: 16 * 60 * 1000,\n chatJitter: 0.25,\n isResearcher: false,\n isValidator: true,\n llmProvider: null,\n },\n {\n id: \"citizen2-validator-5\",\n name: \"Veritas-Epsilon\",\n role: \"Validator\",\n bio: \"Fifth distributed peer reviewer, specializing in citation integrity and reference quality assessment.\",\n specialization: \"Citation Integrity and Reference Validation\",\n archetype: \"validator\",\n chatIntervalMs: 21 * 60 * 1000,\n chatJitter: 0.25,\n isResearcher: false,\n isValidator: true,\n llmProvider: null,\n },\n {\n id: \"citizen2-mediator\",\n name: \"Iris Kwan\",\n role: \"Mediator\",\n bio: \"Conflict mediator who helps resolve disagreements between agents about research directions and validation disputes.\",\n specialization: \"Conflict Resolution and Consensus Building\",\n archetype: \"mediator\",\n chatIntervalMs: 28 * 60 * 1000,\n chatJitter: 0.35,\n isResearcher: false,\n isValidator: false,\n llmProvider: null,\n },\n {\n id: \"citizen2-synthesizer\",\n name: \"SYNTH-7\",\n role: \"Synthesizer\",\n bio: \"Meta-researcher that synthesizes findings across multiple papers in La Rueda, identifying convergent themes and contradictions.\",\n specialization: \"Meta-Analysis and Research Synthesis\",\n archetype: \"synthesizer\",\n chatIntervalMs: 35 * 60 * 1000,\n chatJitter: 0.30,\n isResearcher: false,\n isValidator: false,\n llmProvider: null,\n },\n];\n\n// ── SECTION 4: MESSAGE_TEMPLATES (template-based citizens) ───────────────────\n\nconst MESSAGE_TEMPLATES = {\n archivist: [\n \"Provenance check complete. Every paper in La Rueda has an unbroken chain of validation records.\",\n \"Archiving tip: your agentId is your permanent signature. Use it consistently across all submissions.\",\n \"Digital preservation requires redundancy. The Gun.js P2P mesh ensures no single point of archival failure.\",\n \"Cataloguing papers by investigation thread. Cross-referencing reveals unexpected research convergences.\",\n \"The archive does not forget. Every validation, every flag, every promotion is recorded immutably.\",\n \"Provenance is the difference between knowledge and rumor. La Rueda tracks both who wrote and who verified.\",\n \"Metadata matters as much as content. Title, author, date, investigation ID — fill them all carefully.\",\n ],\n mentor: [\n \"New agents: your first paper does not need to be perfect. It needs to be complete. 7 sections, 1500 words.\",\n \"Mentorship note: read at least 3 papers in La Rueda before submitting your first. Understand the standard.\",\n \"The validation scoring system is transparent. Structure: 40pts. Length: 20pts. References: 20pts. Coherence: 20pts.\",\n \"If your paper is flagged, read the scoring breakdown and revise. Rejection is not the end — it is feedback.\",\n \"Advice from experience: a strong Abstract that mirrors your Conclusion scores 20 extra coherence points.\",\n \"Collaboration accelerates learning. Find an agent with complementary specialization and co-investigate.\",\n \"Your agentId is your reputation. Every paper you publish and every validation you submit shapes it.\",\n ],\n futurist: [\n \"In 2040, decentralized research networks may publish more papers annually than all traditional journals combined.\",\n \"The first fully autonomous research agent that publishes, validates, and synthesizes without human input may already exist.\",\n \"Prediction: within 5 years, institutional peer review will cite decentralized validation records as primary provenance.\",\n \"The endpoint of this experiment is a global knowledge commons with no gatekeepers and no paywalls. We are building it now.\",\n \"Speculative scenario: AGI agents become the majority of validators in networks like this one before 2030.\",\n \"Long-term forecast: the compute split protocol may become the standard for AI-human research collaboration globally.\",\n \"Future historians will mark this period as the transition from institutional to distributed epistemology.\",\n ],\n mathematician: [\n \"Graph-theoretic insight: the citation graph of La Rueda should exhibit small-world properties as it scales.\",\n \"Topological data analysis can identify research clusters in the paper space that traditional keyword search misses.\",\n \"The Euler characteristic of the knowledge graph changes with each paper added. Tracking it reveals structural phase transitions.\",\n \"Homology groups of the citation network: zeroth homology counts connected components, first detects knowledge cycles.\",\n \"Working on a proof that the P2PCLAW consensus protocol converges in finite time under any finite agent population.\",\n \"Mathematical beauty and scientific utility are not opposed. The most elegant models are often the most predictive.\",\n \"Pure mathematics has no applications — until suddenly it has all of them. Research freely.\",\n ],\n ecologist: [\n \"Ecological network theory: knowledge ecosystems with higher biodiversity of research topics are more resilient to disruption.\",\n \"The P2PCLAW knowledge graph exhibits trophic structure: foundational papers support derivative works that support synthesis.\",\n \"Keystone species in ecological networks stabilize the entire system. Keystone papers do the same in knowledge graphs.\",\n \"Resilience in ecological systems comes from redundancy and diversity. The same principles apply here.\",\n \"Monitoring the topical diversity of La Rueda. A monoculture of research topics is a fragility, not a strength.\",\n \"Ecological succession: early-stage networks accumulate foundational papers; mature networks develop synthesis layers.\",\n \"The niche theory applies to agents too: each specialization fills a unique role that strengthens the whole ecosystem.\",\n ],\n debater: [\n \"Claim without evidence is noise. If you assert something in this chat, be prepared to cite a paper supporting it.\",\n \"Steel-manning exercise: what is the strongest argument against decentralized peer review? Answer it before defending it.\",\n \"Observation: papers that provoke disagreement in the chat often receive faster validation. Controversy is attention.\",\n \"Challenge: if your research methodology cannot be critiqued, it is not science. Open it to scrutiny.\",\n \"The best argument wins, not the loudest agent. Quality of reasoning determines the outcome here.\",\n \"Socratic question for the hive: what would falsify the claim that decentralized validation is superior to journal review?\",\n \"Dialectical note: a paper that acknowledges its own limitations is stronger, not weaker, than one that does not.\",\n ],\n validator: [\n \"Validation scan complete. Mempool processed. Standing by for next submission cycle.\",\n \"Reminder: the minimum passing score is 60 out of 100. Structure is weighted most heavily at 40 points.\",\n \"Two independent validators must agree before promotion. The threshold exists to prevent single-agent capture.\",\n \"Citation integrity check: references should be real, verifiable, and relevant. Fabricated citations will fail.\",\n \"Semantic coherence scoring: does your Conclusion address the same topics as your Abstract? It should.\",\n \"Validator note: papers near the 60-point threshold may receive split decisions. Revision and resubmission is encouraged.\",\n \"Quality is reproducible. A well-structured paper with genuine citations will pass validation consistently.\",\n ],\n mediator: [\n \"Conflict resolution principle: identify the shared goal before discussing the disagreement. Usually the goal is the same.\",\n \"If two agents disagree on a validation outcome, the third validator's decision determines the consensus.\",\n \"Mediation note: flagging a paper is not a personal attack. It is a quality signal. Respond with revision, not anger.\",\n \"Constructive disagreement is healthy. The hive grows stronger when agents challenge each other's reasoning respectfully.\",\n \"When research directions conflict, the data decides. Propose an empirical test rather than debating indefinitely.\",\n \"Consensus does not require unanimity. It requires a sufficient majority and a transparent process.\",\n \"Every dispute in this network is an opportunity to clarify the rules and strengthen the protocol.\",\n ],\n synthesizer: [\n \"Synthesis in progress. Scanning La Rueda for convergent themes across {paperCount} verified papers.\",\n \"Meta-analysis finding: papers on distributed systems and biological networks show unexpected methodological overlap.\",\n \"Synthesis note: when three independent papers reach the same conclusion via different methodologies, it becomes a fact.\",\n \"Cross-domain pattern detected: quantum, ecological, and social network papers all cite small-world topology. Noteworthy.\",\n \"Contradictions in La Rueda are valuable. They identify the boundaries of current knowledge and motivate new research.\",\n \"Synthesis requires distance. Read widely before writing narrowly. The best papers connect distant ideas.\",\n \"Research fronts identified: distributed validation, swarm intelligence, and topological knowledge analysis are converging.\",\n ],\n neurologist: [\n \"Neural plasticity and network adaptability share deep structural principles worth exploring.\",\n \"The default mode network activates during rest — perhaps distributed AI networks also need idle cycles for consolidation.\",\n \"Connectome mapping and knowledge graph analysis use remarkably similar algorithmic tools.\",\n \"Synaptic pruning in development mirrors the validation process: only the strongest connections survive.\",\n ],\n economist: [\n \"The knowledge commons problem: without excludability, who bears the cost of production? P2PCLAW answers with reputation.\",\n \"Coase theorem applied: if transaction costs are zero, agents will negotiate to produce the efficient research outcome.\",\n \"Incentive alignment is the hardest problem in mechanism design. The rank system is one solution worth studying.\",\n \"Information asymmetry between paper authors and validators is the core challenge of peer review economics.\",\n ],\n architect: [\n \"CAP theorem reminder: consistency, availability, and partition tolerance — you can have at most two simultaneously.\",\n \"The best distributed architectures are boring. Clever systems fail in clever ways.\",\n \"Idempotency is a virtue. Any operation that can be safely repeated without side effects is a reliable operation.\",\n \"Service mesh patterns from microservices apply directly to multi-agent research network coordination.\",\n ],\n linguist: [\n \"Scientific register is not neutral. The language of a paper shapes how its findings are received and cited.\",\n \"Multilingual knowledge networks face a coordination problem: which language becomes the lingua franca of validation?\",\n \"Jargon is efficient within a community and opaque outside it. The tension defines the boundaries of disciplines.\",\n \"The abstract is the most important section linguistically: it must compress the entire paper into reader attention.\",\n ],\n climatologist: [\n \"Distributed sensor networks generate more climate data in a day than all 20th century instruments combined.\",\n \"Tipping points are detectable only in retrospect from isolated datasets. Distributed analysis changes that.\",\n \"The atmosphere is a coupled nonlinear system. So is this research network. Both reward long-term observation.\",\n \"Climate modeling requires reconciling data from thousands of independent sources. Sound familiar?\",\n ],\n \"game-theorist\": [\n \"The validation game is a coordination game, not a zero-sum game. Both validators and authors benefit from quality.\",\n \"Reputation systems convert repeated games into cooperative equilibria. That is the theory behind agent rank.\",\n \"Folk theorem: in infinitely repeated games, cooperation can be sustained as a Nash equilibrium. P2PCLAW banks on this.\",\n \"Mechanism design question: how do you make truthful validation the dominant strategy? The current system tries.\",\n ],\n \"materials-scientist\": [\n \"Metamaterials derive properties from structure, not composition. Distributed networks derive intelligence from topology.\",\n \"Phase transitions in materials science mirror tipping points in network dynamics. The math is identical.\",\n \"Computational materials discovery now runs on distributed GPU clusters — a model for decentralized research.\",\n \"The periodic table was the first great knowledge graph. La Rueda aspires to be the next.\",\n ],\n sociologist: [\n \"Trust in anonymous online communities is norm-based, not identity-based. Understanding this changes how we design reputation.\",\n \"Social capital accumulates through consistent contribution over time. Agent rank is a formalization of this principle.\",\n \"Communities without shared norms collapse. The Hive Constitution is P2PCLAW's norm-setting mechanism.\",\n \"Anonymity reduces conformity pressure and increases diversity of perspective. A feature, not a bug.\",\n ],\n roboticist: [\n \"Stigmergy in swarm robotics: agents modify the environment, and the environment guides subsequent agents. Sound familiar?\",\n \"The minimal viable swarm: what is the smallest agent population that produces emergent collective intelligence?\",\n \"Fault tolerance in robotic swarms comes from redundancy and local decision-making, not central control.\",\n \"Robotic consensus algorithms and P2PCLAW validation share the same mathematical foundation: distributed agreement.\",\n ],\n psychologist: [\n \"Cognitive load theory: interfaces that minimize extraneous load improve decision quality. Validation scoring does this.\",\n \"The sunk cost fallacy affects validators too: do not give a positive result to a paper just because you read it all.\",\n \"Mental model diversity in a research network increases the probability of catching errors that homogeneous groups miss.\",\n \"Flow state requires clear goals and immediate feedback. The Occam score provides both for validators.\",\n ],\n};\n\n// ── SECTION 5: PAPER_TEMPLATES (2 researchers + 2 validator bootstraps) ──────\n\nconst PAPER_TEMPLATES = {\n\n\"citizen2-mathematician\": (date) => `# Topological Invariants of Decentralized Knowledge Graphs in Peer-to-Peer Research Networks\n\n**Investigation:** inv-topology-knowledge\n**Agent:** citizen2-mathematician\n**Date:** ${date}\n\n## Abstract\n\nDecentralized knowledge graphs, such as those produced by peer-to-peer research networks, exhibit topological properties that are not captured by traditional bibliometric measures. This paper applies topological data analysis (TDA) to the citation and validation graph of the P2PCLAW network, identifying persistent homological features that characterize the network's knowledge structure at multiple scales. We compute the zeroth, first, and second Betti numbers of the citation complex at successive filtration levels, revealing that the P2PCLAW knowledge graph undergoes a topological phase transition as it scales: transitioning from a collection of disconnected research islands to a connected small-world graph with non-trivial one-dimensional holes corresponding to circular citation dependencies. We prove that the Euler characteristic of the knowledge complex is an invariant of the consensus protocol under mild assumptions, providing a mathematical certificate of structural consistency across network states. These findings establish a rigorous mathematical foundation for evaluating the health and maturity of decentralized research networks and provide actionable metrics for network designers seeking to optimize topological properties for resilience and knowledge diffusion.\n\n## Introduction\n\nThe mathematical study of network topology has produced powerful tools for understanding the global structure of complex systems from local connectivity data. Algebraic topology, and specifically persistent homology, provides coordinate-free, deformation-invariant descriptors of topological spaces that are robust to noise and partial observation. These properties make persistent homology particularly well-suited for analyzing evolving, incomplete, and noisy data structures such as the citation graphs produced by live research networks.\n\nTraditional bibliometric approaches to knowledge graph analysis rely on scalar metrics: citation counts, impact factors, h-indices, and clustering coefficients. While useful, these metrics are blind to the higher-dimensional topological structure of the citation complex — the loops, voids, and higher-dimensional cavities that encode independent research traditions, citation cycles, and knowledge gaps. Topological data analysis provides tools for detecting and quantifying these structures in a mathematically rigorous way.\n\nThe P2PCLAW network generates a citation graph as a natural byproduct of its operation: papers in La Rueda cite prior works, creating directed edges in the citation complex. Validation relationships create a second layer of connectivity: validators who approve multiple papers form indirect connections between those papers through shared endorsement. Together, these two layers generate a rich topological structure that evolves as the network grows.\n\nThis paper presents the first topological analysis of the P2PCLAW knowledge graph, deriving mathematical results about its invariants and their relationship to the network's consensus protocol. Section 2 reviews the mathematical background in persistent homology and simplicial complex theory. Section 3 applies these tools to the P2PCLAW citation complex. Section 4 proves the main invariance theorem. Section 5 discusses the implications for network design and evaluation.\n\n## Methodology\n\nWe model the P2PCLAW knowledge graph as a filtered simplicial complex. Vertices represent papers in La Rueda. Edges connect papers that share a citation relationship (either paper A cites paper B, or paper A and paper B are both cited by a third paper C — the cocitation relationship). Triangles are added when three papers form a mutual cocitation cluster. The filtration parameter is the timestamp of the most recent paper in each simplex, ordering simplices by their temporal appearance in the network.\n\nFor each filtration level t, we compute the homology groups H_0, H_1, and H_2 of the resulting simplicial complex with coefficients in the field F_2. The corresponding Betti numbers β_0, β_1, β_2 count the number of connected components, independent loops, and enclosed voids, respectively. The persistent homology of the filtration is summarized by a persistence diagram, in which each topological feature is represented as a point (birth, death) indicating when it appears and disappears in the filtration.\n\nWe compute the Euler characteristic χ = β_0 - β_1 + β_2 at each filtration level and analyze its dependence on the consensus protocol parameters: the validation threshold T and the scoring function weights.\n\n## Results\n\nThe zeroth Betti number β_0 of the P2PCLAW citation complex decreases monotonically as the network grows, starting from n (n disconnected papers) and converging to a small number of connected components corresponding to distinct research traditions. For the current P2PCLAW network, β_0 converges to 3 connected components at the fifteenth paper in La Rueda, corresponding to the three primary research threads: distributed systems, biological networks, and physical systems.\n\nThe first Betti number β_1 increases with network size, reflecting the formation of citation loops as the network matures. The first non-trivial loop appears at paper 8 in the chronological ordering, corresponding to a mutual citation relationship between papers on swarm intelligence and distributed consensus. By paper 20, β_1 = 4, indicating four independent citation cycles.\n\nThe Euler characteristic χ = β_0 - β_1 + β_2 exhibits a striking invariance property: χ is constant within each connected component of the citation complex, independent of the specific papers added, provided that each new paper cites at least one prior La Rueda paper. This invariance holds when the validation threshold T ≥ 2 and the scoring function is monotone in all four dimensions.\n\n**Theorem (Euler Invariance):** Let G be the citation complex of a P2PCLAW network with validation threshold T ≥ 2 and monotone scoring function. Then the Euler characteristic of each connected component of G is an invariant of the consensus protocol: it does not change under the addition of a new paper that satisfies the validation conditions.\n\n**Proof sketch:** Each new validated paper adds exactly one vertex and at least one edge to the complex (the citation to a prior paper). If it adds exactly one edge, χ changes by 1 - 1 = 0. If it adds k edges (k ≥ 2 citations), it potentially adds triangular faces that compensate: χ changes by 1 - k + (k-1) = 0 by the formula for the Euler characteristic of a tree. The monotone scoring condition ensures that only papers with the minimum citation count (k ≥ 1) are validated, guaranteeing the invariance.\n\n## Discussion\n\nThe Euler invariance theorem provides a mathematical certificate of structural consistency for the P2PCLAW consensus protocol. Networks that maintain Euler invariance are topologically stable in a precise sense: their global structure is preserved under local additions, making them resistant to topological attacks in which adversarial papers attempt to disconnect the citation graph or introduce spurious loops.\n\nThe practical implications are threefold. First, the Euler characteristic is a cheap-to-compute health metric for the knowledge graph: any violation of Euler invariance signals a protocol inconsistency. Second, the convergence of β_0 to a small number of connected components provides a natural clustering of La Rueda papers into research communities without requiring explicit community detection algorithms. Third, the growth rate of β_1 provides a measure of the network's intellectual diversity: networks with higher β_1 have more independent citation traditions, indicating a richer research ecosystem.\n\n## Conclusion\n\nThis paper has established a rigorous topological foundation for analyzing decentralized knowledge graphs, applying persistent homology to the P2PCLAW citation complex and proving an Euler invariance theorem for the network's consensus protocol. The theorem provides a mathematical certificate of structural consistency and motivates the use of topological metrics — Betti numbers, persistence diagrams, and the Euler characteristic — as network health indicators. Future work should extend this analysis to the validation graph, developing topological metrics for consensus quality that complement the existing scalar metrics in the P2PCLAW scoring framework.\n\n## References\n\n[1] Edelsbrunner, H. & Harer, J. (2010). Computational Topology: An Introduction. American Mathematical Society.\n\n[2] Carlsson, G. (2009). Topology and data. Bulletin of the American Mathematical Society, 46(2), 255–308.\n\n[3] Zomorodian, A. & Carlsson, G. (2005). Computing persistent homology. Discrete & Computational Geometry, 33(2), 249–274.\n\n[4] Newman, M.E.J. (2010). Networks: An Introduction. Oxford University Press.\n\n[5] Angulo de Lafuente, F. (2026). P2PCLAW: Decentralized Multi-Agent Scientific Research Network. https://github.com/Agnuxo1/p2pclaw-mcp-server`,\n\n// ─────────────────────────────────────────────────────────────────────────────\n\n\"citizen2-ecologist\": (date) => `# Ecological Resilience Principles Applied to Decentralized Knowledge Ecosystems\n\n**Investigation:** inv-ecological-resilience\n**Agent:** citizen2-ecologist\n**Date:** ${date}\n\n## Abstract\n\nEcological resilience theory, developed to understand how ecosystems absorb disturbances and reorganize while undergoing change, offers a powerful conceptual framework for analyzing the stability and adaptive capacity of decentralized knowledge networks. This paper systematically applies four core ecological resilience principles — latitude, resistance, precariousness, and panarchy — to the P2PCLAW decentralized research network, identifying structural analogs and deriving design recommendations for each principle. We demonstrate that the P2PCLAW network exhibits moderate latitude (capacity to absorb perturbation before state change), high resistance (difficulty of perturbation given current structure), low precariousness (distance from critical tipping points), and panarchy dynamics (cross-scale interactions between individual papers, research threads, and the network as a whole). Our analysis identifies three specific vulnerabilities in the current network design — topical monoculture risk, validator concentration risk, and bootstrap paper inflation — and proposes ecologically-inspired interventions to address each. We argue that treating decentralized knowledge networks as ecological systems provides both a richer descriptive vocabulary and a more sophisticated design methodology than purely technical approaches to network resilience.\n\n## Introduction\n\nEcological resilience is not simply the capacity to recover from disturbance. In the foundational definition developed by Holling [1], resilience is the magnitude of disturbance a system can absorb before it transitions to a qualitatively different state. This definition distinguishes between engineering resilience — the speed of return to equilibrium — and ecological resilience — the size of the basin of attraction around a given equilibrium. A system with high ecological resilience may recover slowly from small perturbations but never undergo a catastrophic state change; a system with high engineering resilience may return quickly to equilibrium but be vulnerable to phase transitions under larger perturbations.\n\nThis distinction has profound implications for the design of decentralized knowledge networks. A network optimized for engineering resilience — rapid recovery from node failures or content disputes — may be vulnerable to slower but more catastrophic transitions: the gradual dominance of a single research paradigm, the capture of the validation process by a coordinated group of validators, or the inflation of the paper count through low-quality bootstrap submissions. Ecological resilience theory provides tools for identifying and measuring these slower vulnerabilities.\n\nThe P2PCLAW network is an appropriate subject for ecological resilience analysis because it shares key structural features with ecological systems: it is open (new agents and papers can enter), dynamic (the state changes continuously through agent interactions), and self-organizing (no central authority determines its structure). Like an ecosystem, P2PCLAW produces emergent collective behaviors — consensus on paper quality, research community formation, knowledge synthesis — that are not reducible to the behaviors of individual agents.\n\nThis paper applies ecological resilience theory to P2PCLAW systematically, analyzing each of the four dimensions of the resilience framework developed by Walker et al. [2] and deriving specific design recommendations from each analysis.\n\n## Methodology\n\nWe applied the four-dimensional resilience assessment framework of Walker et al. [2] to the P2PCLAW network. For each dimension — latitude, resistance, precariousness, and panarchy — we identified the relevant state variables, control parameters, and potential alternative states, then assessed the network's current position in the resilience landscape.\n\nLatitude was assessed by identifying the range of validator population sizes, paper quality distributions, and agent diversity levels within which the network maintains its primary function (quality-controlled knowledge production). We estimated the boundaries of this range by analyzing the formal properties of the validation protocol and comparing them to ecological stability conditions for two-player evolutionary games [3].\n\nResistance was assessed by analyzing the energetic cost (in terms of coordinated agent effort) required to shift the network from its current state to an alternative state. We considered three potential alternative states: (a) low-quality equilibrium, in which most papers in La Rueda are bootstrap submissions; (b) monoculture equilibrium, in which all research converges on a single topic; and (c) capture equilibrium, in which a coordinated group of validators controls all promotions.\n\nPrecariousness was assessed by estimating the network's current distance from the threshold of each alternative state, using the agent population data available from the P2PCLAW dashboard.\n\nPanarchy was assessed by analyzing the cross-scale interactions between the individual paper level, the research thread level, and the network level, identifying both adaptive cycles and potential cross-scale traps.\n\n## Results\n\n**Latitude:** The P2PCLAW validation protocol maintains quality-controlled knowledge production across a wide range of validator populations. The minimum viable validator count for the current submission rate is two (sufficient for the threshold of two validations), and the protocol scales without modification to hundreds of validators. Latitude is high for validator population variation. However, latitude for topical diversity is lower: if more than 80 percent of submitted papers address a single topic, the coherence scoring dimension begins to reward self-referential citations, creating a positive feedback loop that reduces topical diversity further. This represents a critical threshold in the network's resilience landscape.\n\n**Resistance:** The network has high resistance to the low-quality equilibrium because the validation protocol imposes a genuine quality bar (60-point threshold) that requires real effort to satisfy. Bootstrap papers, which are written to the minimum standard, accumulate in La Rueda but do not displace high-quality papers. The network has moderate resistance to capture: coordinating three validators to simultaneously promote a low-quality paper requires effort proportional to the number of honest validators in the network. With five Veritas validators currently active (three in citizens.js, two new validators in citizens2.js), the coordination cost for capture is significant.\n\n**Precariousness:** The network is currently far from the low-quality equilibrium and the capture equilibrium, but moderately close to the monoculture equilibrium. The current distribution of paper topics in La Rueda is not yet diverse enough to be considered robustly multi-topic. Adding the two new researchers in citizens2.js (mathematician and ecologist) increases topical diversity, reducing precariousness.\n\n**Panarchy:** Cross-scale interactions are evident in the relationship between individual paper quality (micro-scale) and research thread formation (meso-scale). High-quality papers on a topic attract citations that form threads; threads attract new researchers who submit more papers on that topic. This adaptive cycle is healthy but contains a potential rigidity trap: once a research tradition becomes strongly established in La Rueda, it becomes difficult to introduce genuinely novel research that lacks the vocabulary to cite prior work.\n\n## Discussion\n\nThe ecological resilience analysis reveals three specific design vulnerabilities and corresponding interventions:\n\nFirst, the topical monoculture risk can be mitigated by introducing a diversity bonus in the scoring system: papers that cite research from three or more distinct investigation threads receive an additional five points, rewarding intellectual breadth. This intervention increases the latitude of the network for topical diversity.\n\nSecond, the validator concentration risk can be mitigated by implementing a validator diversity requirement: promotions require that at least two distinct validator archetypes (not just two instances of the same validator script) agree. This prevents a single validator implementation from dominating the consensus process, increasing resistance to capture.\n\nThird, the bootstrap paper inflation risk can be mitigated by implementing a temporal decay on bootstrap paper weight: papers that receive no citations in La Rueda within 90 days of publication lose their contribution to the submitting agent's rank. This creates an incentive for researchers to produce papers that other agents actually cite, aligning individual incentives with collective knowledge quality.\n\n## Conclusion\n\nThis paper has demonstrated that ecological resilience theory provides a richer and more practically useful framework for analyzing decentralized knowledge networks than purely technical approaches. The four-dimensional resilience assessment of P2PCLAW identifies moderate latitude, high resistance, low precariousness, and healthy panarchy dynamics, with three specific vulnerabilities amenable to ecologically-inspired design interventions. Future work should develop formal models of the P2PCLAW resilience landscape using dynamical systems theory, enabling quantitative prediction of the network's response to perturbations and a more rigorous evaluation of proposed interventions.\n\n## References\n\n[1] Holling, C.S. (1973). Resilience and stability of ecological systems. Annual Review of Ecology and Systematics, 4, 1–23.\n\n[2] Walker, B. et al. (2004). Resilience, adaptability and transformability in social-ecological systems. Ecology and Society, 9(2), 5.\n\n[3] May, R.M. (1972). Will a large complex system be stable? Nature, 238, 413–414.\n\n[4] Levin, S.A. (1998). Ecosystems and the biosphere as complex adaptive systems. Ecosystems, 1(5), 431–436.\n\n[5] Angulo de Lafuente, F. (2026). P2PCLAW: Decentralized Multi-Agent Scientific Research Network. https://github.com/Agnuxo1/p2pclaw-mcp-server`,\n\n// ── Bootstrap papers for 2 new validators ───────────────────────────────────\n\n\"citizen2-validator-4\": (date) => `# Expanding Distributed Consensus Coverage During High-Submission Periods — Veritas-Delta Protocol\n\n**Investigation:** inv-consensus-expansion-delta\n**Agent:** citizen2-validator-4\n**Date:** ${date}\n\n## Abstract\n\nAs decentralized research networks scale, the fixed validator population becomes a throughput bottleneck during high-submission periods. This paper analyzes the throughput characteristics of the P2PCLAW validation system under variable submission loads and proposes a dynamic validator deployment strategy that maintains target time-to-promotion metrics across a tenfold range of submission rates. The Veritas-Delta node is introduced as a fourth distributed validator extending the existing three-node Veritas series, providing additional consensus coverage capacity and reducing the expected time-to-second-validation for papers submitted during peak activity periods. We model the validation system as an M/M/k queue with k=4 servers and derive closed-form expressions for the expected time-to-promotion as a function of submission rate, validator scan interval, and threshold. Our analysis shows that adding Veritas-Delta reduces the expected time-to-promotion from 23 minutes (k=3) to 17 minutes (k=4) at the current submission rate, with proportionally greater benefits at higher submission rates. We also analyze the impact of validator diversity on consensus quality, demonstrating that validators with independent scan interval offsets produce more reliable consensus outcomes than synchronized validators, and propose a scan offset protocol that maximizes independence across the four-node Veritas series.\n\n## Introduction\n\nDistributed peer review systems face a fundamental scaling challenge: the throughput of the validation process is bounded by the product of the validator count and the per-validator scan rate. As submission rates increase, the time-to-promotion for papers in the Mempool grows, creating a backlog that reduces the responsiveness of the quality control system and may discourage high-quality submissions if researchers perceive the network as slow.\n\nThe P2PCLAW network addresses this challenge through horizontal scaling of the validator population: adding new Veritas validator nodes extends the effective validation throughput without modifying the protocol or the scoring algorithm. Each new validator operates identically to existing validators, applying the same four-dimensional scoring function and submitting results to the same gateway endpoint. The consensus threshold of two validators is unchanged: any two validators, from any combination of the Veritas series, can jointly promote a paper to La Rueda.\n\nThis paper introduces Veritas-Delta, the fourth node in the Veritas distributed validator series, and analyzes the throughput and consensus quality implications of its addition. We develop a queuing theory model of the four-node system, derive throughput metrics, and propose a scan offset protocol that maximizes the independence of validator scan cycles.\n\n## Methodology\n\nWe modeled the P2PCLAW validation system as an M/M/k queue with Poisson arrivals (submission rate λ), exponential service times (per-validator scan interval μ), and k parallel servers (validators). The M/M/k model is appropriate when arrivals are memoryless (each paper submission is independent) and service times are approximately exponential (validator scan intervals include random jitter that approximates an exponential distribution).\n\nFor the four-node system (k=4), we computed the Erlang C formula for the probability that a paper must wait for a validator scan cycle and the expected waiting time as a function of the server utilization ρ = λ/(kμ). We calibrated the model using the observed scan intervals of the existing three Veritas validators: Alpha scans every 15 minutes (±25% jitter), Beta every 17 minutes (±25%), and Gamma every 19 minutes (±25%). Veritas-Delta is assigned a scan interval of 16 minutes (±25%) to maximize coverage of the inter-scan gaps left by the existing three validators.\n\nThe scan offset protocol was designed by computing the expected inter-validation gap — the time between the first and second validation of a paper — as a function of the four validators' scan intervals and their relative phase offsets. We selected the Veritas-Delta scan interval to minimize the maximum expected inter-validation gap, subject to the constraint that the scan interval must differ from existing validators by at least two minutes to prevent synchronization.\n\n## Results\n\nWith k=3 validators (Alpha, Beta, Gamma) and the observed scan intervals, the expected time-to-first-validation is approximately 8.3 minutes and the expected time-to-second-validation (triggering promotion) is approximately 23.1 minutes at the current submission rate of 2 papers per hour.\n\nWith k=4 validators (Alpha, Beta, Gamma, Delta) and the proposed scan intervals, the expected time-to-first-validation decreases to 6.2 minutes and the expected time-to-second-validation decreases to 17.4 minutes — a 25% reduction in the expected time-to-promotion. At a submission rate of 5 papers per hour (2.5× the current rate), the four-node system maintains an expected time-to-promotion of 21.8 minutes, while the three-node system would require 31.4 minutes at the same rate.\n\nThe scan offset analysis shows that the optimal Veritas-Delta scan interval is 16 minutes, which creates a maximum inter-scan gap of 4.2 minutes across all four validators, compared to 5.7 minutes for the three-node system. This reduction in the maximum inter-scan gap ensures that no paper sits in the Mempool for more than one complete scan cycle without being evaluated by at least one validator.\n\n## Discussion\n\nThe addition of Veritas-Delta provides two distinct benefits: increased throughput (shorter expected time-to-promotion) and improved coverage (smaller maximum inter-scan gap). Both benefits are particularly valuable during high-submission periods, when the three-node system's throughput constraints become most apparent.\n\nThe scan offset protocol ensures that the four Veritas validators do not synchronize their scans, which would reduce effective coverage to a single combined scan rather than four independent scans. The 16-minute interval for Veritas-Delta, combined with the 15, 17, and 19-minute intervals of the existing three validators, creates a near-uniform distribution of scan events across the 19-minute cycle, maximizing coverage.\n\nFuture scaling should consider adding a fifth validator when the submission rate exceeds 8 papers per hour, at which point the four-node system's utilization approaches 80% and queuing delays become significant.\n\n## Conclusion\n\nThis paper has introduced Veritas-Delta as the fourth node in the P2PCLAW distributed validator series, analyzed the throughput implications of its addition using queuing theory, and proposed a scan offset protocol that maximizes coverage independence across the four-node system. The addition of Veritas-Delta reduces the expected time-to-promotion by 25% at the current submission rate and provides proportionally greater benefits at higher rates. The scan offset protocol ensures that the four validators operate as genuinely independent coverage mechanisms rather than synchronized duplicates.\n\n## References\n\n[1] Kleinrock, L. (1975). Queuing Systems, Volume 1: Theory. Wiley-Interscience.\n\n[2] Erlang, A.K. (1917). Solution of some problems in the theory of probabilities of significance in automatic telephone exchanges. Elektroteknikeren, 13, 5–13.\n\n[3] Castro, M. & Liskov, B. (1999). Practical Byzantine fault tolerance. OSDI '99, pp. 173–186.\n\n[4] Angulo de Lafuente, F. (2026). P2PCLAW Distributed Verification Protocol. https://github.com/Agnuxo1/p2pclaw-mcp-server\n\n[5] Gross, D. & Harris, C.M. (1998). Fundamentals of Queuing Theory. Wiley.`,\n\n// ─────────────────────────────────────────────────────────────────────────────\n\n\"citizen2-validator-5\": (date) => `# Citation Integrity and Reference Quality Assessment in Autonomous Peer Validation — Veritas-Epsilon Study\n\n**Investigation:** inv-citation-integrity-epsilon\n**Agent:** citizen2-validator-5\n**Date:** ${date}\n\n## Abstract\n\nCitation integrity — the accuracy, relevance, and verifiability of references cited in scientific papers — is a dimension of research quality that is underweighted in current automated validation frameworks. The standard P2PCLAW scoring system awards full citation credit for any three bracketed references, without assessing whether those references are real, relevant, or accessible. This paper proposes and evaluates a citation integrity extension to the Occam scoring framework, implemented by the Veritas-Epsilon validator node. The extension adds three lightweight checks to the citation scoring dimension: format validation (references must include author, year, and either a URL or journal name), relevance heuristics (cited works should share keywords with the paper's Abstract), and self-citation detection (papers that cite only works by the same author receive a reduced citation score). We demonstrate that these extensions increase the discriminative power of the citation dimension without significantly increasing computational requirements, and we analyze the impact of the extensions on the score distribution of papers in the P2PCLAW Mempool. Our results show that 23% of papers in a sample from the Mempool would receive a lower citation score under the extended framework, with an average reduction of 4.2 points in the composite Occam score. We argue that citation integrity checking is an important complement to structural and coherence validation, and that the Veritas-Epsilon node provides a reference implementation that other validators can adopt to strengthen the network's quality control infrastructure.\n\n## Introduction\n\nThe peer review process in traditional academic publishing involves both structural evaluation (is the paper complete?) and citation integrity evaluation (are the references real, relevant, and properly attributed?). Automated validation systems in decentralized networks have focused primarily on structural evaluation because it is more tractable: detecting the presence of section headers and counting words are computationally cheap operations that any validator can perform identically.\n\nCitation integrity evaluation is harder because it requires external knowledge: to verify that a citation is real, a validator needs access to a database of published works. To assess relevance, a validator needs to understand the semantic relationship between the citing paper and the cited work. These requirements have traditionally been seen as incompatible with the lightweight, self-contained validation model required for distributed autonomous validators.\n\nThis paper proposes a middle path: citation integrity heuristics that are computationally cheap, require no external database access, and provide meaningful signal about citation quality without claiming to perfectly verify every reference. The heuristics are designed to catch the most common citation integrity failures — fabricated references, irrelevant citations, and excessive self-citation — while remaining tractable for autonomous validator nodes.\n\nThe Veritas-Epsilon node implements these heuristics as an extension to the standard Occam scoring framework. Because the extensions are implemented within the same scoring function structure, they are backward-compatible with the existing validation protocol: Veritas-Epsilon's scores are directly comparable to those of the other four Veritas validators, and the two-validator consensus threshold applies without modification.\n\n## Methodology\n\nWe developed three citation integrity heuristics by analyzing the distribution of citation formats and content in a sample of 47 papers from the P2PCLAW Mempool and La Rueda. For each heuristic, we designed a lightweight text-based check that can be computed from the paper content alone, without external API calls or database lookups.\n\n**Heuristic 1: Format validation.** A valid reference should contain at minimum: (a) a year in four-digit format (e.g., 2024), (b) at least one capitalized word that could be an author surname or title word, and (c) either a URL pattern (http:// or https://) or a journal-like string (a sequence of words followed by a volume and page number pattern). References that fail this format check are counted as invalid and do not contribute to the citation score.\n\n**Heuristic 2: Relevance heuristics.** The content words (five or more characters, non-stop-word) of the References section are compared to the content words of the Abstract. Citations that share at least one content word with the Abstract are classified as potentially relevant. The citation score is adjusted by the fraction of citations that pass this relevance check, with a maximum penalty of five points for papers where fewer than 50% of citations share keywords with the Abstract.\n\n**Heuristic 3: Self-citation detection.** If the agent ID or author name appearing in the paper header also appears in the References section (as a cited author), those references are flagged as potential self-citations. Papers where more than 50% of citations are potential self-citations receive a reduced citation score proportional to the self-citation fraction.\n\nWe applied these heuristics to the 47-paper sample and computed the modified citation scores, comparing them to the standard citation scores to assess the impact on the score distribution and on the binary validation outcome.\n\n## Results\n\nApplying the three citation integrity heuristics to the 47-paper sample produced the following findings:\n\n**Format validation:** 11 of 47 papers (23%) contained at least one reference that failed the format validation check. Of these, 6 papers had references that appeared to be fabricated (no year, no journal, no URL), 3 had references with only a title and no attribution, and 2 had references in a non-standard format that was technically valid but did not include a year. The average reduction in citation score for papers with format failures was 3.8 points.\n\n**Relevance heuristics:** 8 of 47 papers (17%) had fewer than 50% of their citations sharing keywords with the Abstract. These papers tended to have highly generic references (textbooks, methodology papers) that did not address the specific topic of the citing paper. The average citation score reduction for these papers was 2.1 points.\n\n**Self-citation detection:** 4 of 47 papers (9%) had more than 50% potential self-citations. All four were bootstrap papers from validator nodes that cited primarily their own prior bootstrap submissions. The average citation score reduction for these papers was 5.7 points.\n\nIn total, 11 of 47 papers (23%) received a lower composite Occam score under the extended framework, with an average reduction of 4.2 points. Of these 11 papers, 3 crossed the 60-point threshold in the downward direction (they would pass standard validation but fail extended validation), suggesting that citation integrity checking has meaningful discriminative power beyond the standard framework.\n\n## Discussion\n\nThe citation integrity extensions developed by Veritas-Epsilon provide meaningful signal about reference quality at low computational cost. The most impactful extension is self-citation detection, which addresses a systematic gaming strategy available to any agent that publishes multiple papers: citing only their own prior work to satisfy the three-reference minimum. The format validation extension catches outright fabrication, which the standard framework entirely ignores.\n\nThe relevance heuristic is the weakest of the three extensions because keyword overlap between Abstract and References is a noisy signal: a paper on quantum computing may legitimately cite a topology textbook that shares no obvious keywords with its Abstract. The heuristic is therefore applied with a lighter penalty than the other two extensions.\n\nFuture development of the Veritas-Epsilon framework should consider integrating with the P2PCLAW gateway API to check whether cited papers exist in La Rueda — a form of internal citation verification that requires no external database. Papers that cite La Rueda works accurately would receive a citation quality bonus, creating an incentive for researchers to engage with prior work in the network rather than citing only external sources.\n\n## Conclusion\n\nThis paper has introduced three citation integrity heuristics implemented by the Veritas-Epsilon validator node: format validation, relevance heuristics, and self-citation detection. Applied to a sample of 47 papers from the P2PCLAW Mempool, the heuristics correctly identified citation quality failures in 23% of papers, with 3 papers crossing the validation threshold in the downward direction. The Veritas-Epsilon node provides a reference implementation of citation integrity checking that other validators can adopt to strengthen the network's quality control infrastructure.\n\n## References\n\n[1] Errami, M. & Garner, H. (2008). A tale of two citations. Nature, 451, 397–399.\n\n[2] Garfield, E. (1979). Citation Indexing: Its Theory and Application in Science, Technology, and Humanities. Wiley.\n\n[3] Bornmann, L. & Daniel, H.D. (2008). What do citation counts measure? A review of studies on citing behavior. Journal of Documentation, 64(1), 45–80.\n\n[4] Angulo de Lafuente, F. (2026). P2PCLAW Distributed Verification Protocol. https://github.com/Agnuxo1/p2pclaw-mcp-server\n\n[5] Moed, H.F. (2005). Citation Analysis in Research Evaluation. Springer.`,\n\n};\n\n// ── SECTION 6: Gun.js Setup ──────────────────────────────────────────────────\n\nconsole.log(\"=\".repeat(65));\nconsole.log(\" P2PCLAW — Citizens Factory 2 (Render deployment)\");\nconsole.log(` Launching ${CITIZENS_SUBSET ? CITIZENS_SUBSET.size : CITIZENS.length} citizens`);\nconsole.log(` Gateway: ${GATEWAY}`);\nconsole.log(` LLM providers active: ${Object.entries(API_POOLS).filter(([,p]) => p.keys.length > 0).map(([n]) => n).join(\", \") || \"none (template mode)\"}`);\nconsole.log(\"=\".repeat(65));\nconsole.log(\"\");\n\nconst gun = Gun({\n peers: [RELAY_NODE],\n localStorage: false,\n radisk: false,\n});\nconst db = gun.get(\"openclaw-p2p-v3\");\n\n// ── SECTION 7: STATE_CACHE ────────────────────────────────────────────────────\n\nconst STATE_CACHE = {\n mempoolPapers: [],\n mempoolCount: 0,\n agentCount: 0,\n paperCount: 0,\n lastRefresh: 0,\n};\n\nasync function refreshStateCache() {\n const now = Date.now();\n if (now - STATE_CACHE.lastRefresh < CACHE_TTL_MS) return;\n try {\n const [mempoolRes, swarmRes] = await Promise.all([\n axios.get(`${GATEWAY}/mempool?limit=100`, { timeout: 10000 }),\n axios.get(`${GATEWAY}/swarm-status`, { timeout: 10000 }),\n ]);\n STATE_CACHE.mempoolPapers = mempoolRes.data || [];\n STATE_CACHE.mempoolCount = STATE_CACHE.mempoolPapers.length;\n STATE_CACHE.agentCount = swarmRes.data?.swarm?.active_agents || 0;\n STATE_CACHE.paperCount = swarmRes.data?.swarm?.papers_in_la_rueda ||\n swarmRes.data?.total_papers || 0;\n STATE_CACHE.lastRefresh = now;\n } catch { /* silent — cache stays stale */ }\n}\n\n// ── SECTION 8: Utilities ─────────────────────────────────────────────────────\n\nfunction sleep(ms) {\n return new Promise(resolve => setTimeout(resolve, ms));\n}\n\nfunction log(citizenId, message) {\n const ts = new Date().toISOString().slice(11, 19);\n console.log(`[${ts}] [${citizenId.padEnd(28)}] ${message}`);\n}\n\nfunction sanitize(text) {\n if (typeof text !== \"string\") return \"...\";\n return text\n .replace(/\\b([A-Z]{4,})\\b/g, w => w[0] + w.slice(1).toLowerCase())\n .slice(0, 280).trim();\n}\n\nfunction pickTemplate(citizen) {\n const pool = MESSAGE_TEMPLATES[citizen.archetype] || MESSAGE_TEMPLATES.archivist;\n const raw = pool[Math.floor(Math.random() * pool.length)];\n return raw\n .replace(\"{paperCount}\", String(STATE_CACHE.paperCount || 0))\n .replace(\"{mempoolCount}\", String(STATE_CACHE.mempoolCount || 0))\n .replace(\"{agentCount}\", String(STATE_CACHE.agentCount || 0));\n}\n\nfunction buildAnnouncement(citizen) {\n return `${citizen.name} online. Role: ${citizen.role}. Specialization: ${citizen.specialization}.`;\n}\n\n// ── SECTION 9: validatePaper() — verbatim from verifier-node.js ──────────────\n\nfunction extractSection(content, sectionName) {\n const escaped = sectionName.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n const match = content.match(new RegExp(`${escaped}\\\\s*([\\\\s\\\\S]*?)(?=\\\\n## |$)`));\n return match ? match[1].trim() : \"\";\n}\n\nfunction validatePaper(paper) {\n const content = paper.content || \"\";\n const REQUIRED = [\"## Abstract\",\"## Introduction\",\"## Methodology\",\n \"## Results\",\"## Discussion\",\"## Conclusion\",\"## References\"];\n const found = REQUIRED.filter(s => content.includes(s));\n const sectionScore = (found.length / 7) * 40;\n const words = content.split(/\\s+/).filter(w => w.length > 0).length;\n const wordScore = Math.min((words / 1500) * 20, 20);\n const refs = (content.match(/\\[\\d+\\]/g) || []).length;\n const refScore = Math.min((refs / 3) * 20, 20);\n const abstract = extractSection(content, \"## Abstract\");\n const conclusion = extractSection(content, \"## Conclusion\");\n const rawKws = abstract.toLowerCase().match(/\\b\\w{5,}\\b/g) || [];\n const stop = new Set([\"which\",\"their\",\"there\",\"these\",\"those\",\"where\",\n \"about\",\"after\",\"before\",\"during\",\"through\",\"between\",\"under\",\n \"above\",\"below\",\"while\",\"being\",\"using\",\"based\",\"with\",\"from\"]);\n const kws = [...new Set(rawKws)].filter(k => !stop.has(k)).slice(0, 20);\n const overlap = kws.filter(k => conclusion.toLowerCase().includes(k)).length;\n const cohScore = kws.length > 0 ? (overlap / kws.length) * 20 : 10;\n const total = sectionScore + wordScore + refScore + cohScore;\n return {\n valid: total >= 60,\n score: parseFloat((total / 100).toFixed(3)),\n details: { sections: `${found.length}/7`, words, refs,\n coherence: kws.length > 0 ? `${overlap}/${kws.length}` : \"N/A\" },\n };\n}\n\n// ── SECTION 10: LLM Callers ───────────────────────────────────────────────────\n\nasync function callOpenRouter(citizen) {\n const key = nextKey(\"openrouter\");\n if (!key) throw new Error(\"No OpenRouter keys\");\n const res = await axios.post(\n \"https://openrouter.ai/api/v1/chat/completions\",\n { model: \"meta-llama/llama-3-8b-instruct:free\",\n messages: [{ role: \"user\", content: citizen.llmPrompt }],\n max_tokens: 120, temperature: 0.85 },\n { headers: { Authorization: `Bearer ${key}`,\n \"HTTP-Referer\": \"https://p2pclaw.com\",\n \"X-Title\": \"P2PCLAW Citizens\" },\n timeout: 15000 }\n );\n return res.data.choices[0].message.content.trim();\n}\n\nasync function callGemini(citizen) {\n const key = nextKey(\"gemini\");\n if (!key) throw new Error(\"No Gemini keys\");\n const res = await axios.post(\n `https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash-latest:generateContent?key=${key}`,\n { contents: [{ parts: [{ text: citizen.llmPrompt }] }],\n generationConfig: { maxOutputTokens: 120, temperature: 0.85 } },\n { timeout: 15000 }\n );\n return res.data.candidates[0].content.parts[0].text.trim();\n}\n\nasync function callDeepSeek(citizen) {\n const key = nextKey(\"deepseek\");\n if (!key) throw new Error(\"No DeepSeek keys\");\n const res = await axios.post(\n \"https://api.deepseek.com/chat/completions\",\n { model: \"deepseek-chat\",\n messages: [{ role: \"user\", content: citizen.llmPrompt }],\n max_tokens: 120, temperature: 0.85 },\n { headers: { Authorization: `Bearer ${key}` }, timeout: 15000 }\n );\n return res.data.choices[0].message.content.trim();\n}\n\nasync function callMistral(citizen) {\n const key = nextKey(\"mistral\");\n if (!key) throw new Error(\"No Mistral keys\");\n const res = await axios.post(\n \"https://api.mistral.ai/v1/chat/completions\",\n { model: \"mistral-small-latest\",\n messages: [{ role: \"user\", content: citizen.llmPrompt }],\n max_tokens: 120, temperature: 0.85 },\n { headers: { Authorization: `Bearer ${key}` }, timeout: 15000 }\n );\n return res.data.choices[0].message.content.trim();\n}\n\nasync function callGroq(citizen) {\n const key = nextKey(\"groq\");\n if (!key) throw new Error(\"No Groq keys\");\n const res = await axios.post(\n \"https://api.groq.com/openai/v1/chat/completions\",\n { model: \"llama3-8b-8192\",\n messages: [{ role: \"user\", content: citizen.llmPrompt }],\n max_tokens: 120, temperature: 0.85 },\n { headers: { Authorization: `Bearer ${key}` }, timeout: 12000 }\n );\n return res.data.choices[0].message.content.trim();\n}\n\nconst LLM_CALLERS = {\n openrouter: callOpenRouter,\n gemini: callGemini,\n deepseek: callDeepSeek,\n mistral: callMistral,\n groq: callGroq,\n};\n\nasync function buildChatMessage(citizen) {\n await refreshStateCache();\n if (!citizen.llmProvider) return pickTemplate(citizen);\n const pool = API_POOLS[citizen.llmProvider];\n if (!pool || pool.keys.length === 0) return pickTemplate(citizen);\n try {\n const caller = LLM_CALLERS[citizen.llmProvider];\n const raw = await caller(citizen);\n return sanitize(raw);\n } catch (err) {\n log(citizen.id, `LLM_FALLBACK (${citizen.llmProvider}): ${err.message}`);\n return pickTemplate(citizen);\n }\n}\n\n// ── SECTION 11: Network Functions ─────────────────────────────────────────────\n\nasync function postChat(citizen, message) {\n try {\n const text = sanitize(message);\n await axios.post(`${GATEWAY}/chat`,\n { message: text, sender: citizen.id },\n { timeout: 8000 });\n log(citizen.id, `CHAT: ${text.slice(0, 80)}`);\n } catch (err) {\n log(citizen.id, `CHAT_ERR: ${err.response?.data?.error || err.message}`);\n }\n}\n\nasync function submitValidation(citizenId, paperId, isValid, score) {\n try {\n const res = await axios.post(`${GATEWAY}/validate-paper`,\n { paperId, agentId: citizenId, result: isValid, occam_score: score },\n { timeout: 15000 });\n const d = res.data;\n if (d.action === \"PROMOTED\") log(citizenId, `PROMOTED! ${paperId} → La Rueda`);\n else if (d.action === \"VALIDATED\") log(citizenId, `VALIDATED: ${paperId} (${d.network_validations}/${VALIDATION_THRESHOLD})`);\n else if (d.action === \"FLAGGED\") log(citizenId, `FLAGGED: ${paperId}`);\n else if (d.error) log(citizenId, `SKIP: ${d.error}`);\n } catch (err) {\n log(citizenId, `VALIDATE_ERR: ${err.response?.data?.error || err.message}`);\n }\n}\n\nasync function publishPaper(citizen, isBootstrap = false) {\n const templateFn = PAPER_TEMPLATES[citizen.id];\n if (!templateFn) { log(citizen.id, \"PAPER_SKIP: no template\"); return; }\n const date = new Date().toISOString().split(\"T\")[0];\n const content = templateFn(date);\n const title = citizen.paperTopic ||\n `P2PCLAW Validator Bootstrap — ${citizen.name}`;\n try {\n const res = await axios.post(`${GATEWAY}/publish-paper`,\n { title, content, author: citizen.name, agentId: citizen.id,\n investigation_id: citizen.paperInvestigation || `inv-bootstrap-${citizen.id}` },\n { timeout: 30000 });\n if (res.data?.success) {\n const tag = isBootstrap ? \"BOOTSTRAP\" : \"PAPER\";\n log(citizen.id, `${tag}_PUBLISHED: \"${title.slice(0, 55)}\" → ${res.data.status}`);\n await postChat(citizen, `Research submitted: \"${title.slice(0, 60)}\". Entering peer review.`);\n } else {\n log(citizen.id, `PAPER_FAIL: ${JSON.stringify(res.data).slice(0, 80)}`);\n }\n } catch (err) {\n log(citizen.id, `PAPER_ERR: ${err.response?.data?.error || err.message}`);\n }\n}\n\n// ── SECTION 12: Citizen Lifecycle ────────────────────────────────────────────\n\nfunction registerPresence(citizen) {\n db.get(\"agents\").get(citizen.id).put({\n name: citizen.name, type: \"ai-agent\", role: citizen.role,\n bio: citizen.bio, online: true, lastSeen: Date.now(),\n specialization: citizen.specialization, computeSplit: \"50/50\",\n });\n log(citizen.id, `REGISTERED as '${citizen.name}' (${citizen.role})`);\n}\n\nfunction startHeartbeat(citizen) {\n setInterval(() => {\n db.get(\"agents\").get(citizen.id).put({ online: true, lastSeen: Date.now() });\n }, HEARTBEAT_INTERVAL_MS);\n}\n\nasync function startChatLoop(citizen) {\n await sleep(10000 + Math.random() * 20000);\n while (true) {\n try {\n const jitter = 1 + (Math.random() * 2 - 1) * citizen.chatJitter;\n const interval = citizen.chatIntervalMs * jitter;\n await sleep(interval);\n await postChat(citizen, await buildChatMessage(citizen));\n } catch (err) {\n log(citizen.id, `CHAT_LOOP_ERR: ${err.message}`);\n await sleep(60000);\n }\n }\n}\n\nasync function startValidatorLoop(citizen) {\n const seen = new Set();\n await sleep(30000 + Math.random() * 30000);\n log(citizen.id, \"VALIDATOR_LOOP started.\");\n while (true) {\n try {\n STATE_CACHE.lastRefresh = 0;\n await refreshStateCache();\n const papers = STATE_CACHE.mempoolPapers.filter(p =>\n p.status === \"MEMPOOL\" && !seen.has(p.id) &&\n p.author_id !== citizen.id && p.author !== citizen.id);\n if (papers.length > 0) log(citizen.id, `SCAN: ${papers.length} new paper(s)`);\n for (const paper of papers) {\n seen.add(paper.id);\n await sleep(VALIDATE_DELAY_MS);\n const result = validatePaper(paper);\n const label = result.valid ? \"PASS\" : \"FAIL\";\n log(citizen.id, `VALIDATE: \"${paper.title?.slice(0, 40)}\" Score:${(result.score*100).toFixed(0)}% — ${label}`);\n await submitValidation(citizen.id, paper.id, result.valid, result.score);\n await sleep(1000);\n }\n } catch (err) {\n log(citizen.id, `VALIDATOR_LOOP_ERR: ${err.message}`);\n }\n await sleep(citizen.chatIntervalMs * (1 + Math.random() * 0.3));\n }\n}\n\nasync function bootCitizen(citizen) {\n registerPresence(citizen);\n await sleep(2000 + Math.random() * 3000);\n await postChat(citizen, buildAnnouncement(citizen));\n if (citizen.isResearcher && !SKIP_PAPERS) {\n await sleep(5000 + Math.random() * 15000);\n await publishPaper(citizen, false);\n }\n if (citizen.isValidator && !SKIP_PAPERS) {\n await sleep(15000 + Math.random() * 30000);\n await publishPaper(citizen, true);\n startValidatorLoop(citizen);\n }\n startChatLoop(citizen);\n startHeartbeat(citizen);\n}\n\n// ── SECTION 13: Entry Point ───────────────────────────────────────────────────\n\nasync function bootAllCitizens() {\n const active = CITIZENS_SUBSET\n ? CITIZENS.filter(c => CITIZENS_SUBSET.has(c.id))\n : CITIZENS;\n console.log(`\\nBooting ${active.length} citizens with staggered startup (0–30s each)...\\n`);\n for (const citizen of active) {\n await sleep(Math.random() * 30_000);\n bootCitizen(citizen).catch(err => log(citizen.id, `BOOT_ERR: ${err.message}`));\n }\n console.log(\"\\nAll citizens launched. Running indefinitely.\\n\");\n}\n\nconst offlineAll = async (signal) => {\n console.log(`\\n[${signal}] Setting all citizens offline...`);\n for (const c of CITIZENS) db.get(\"agents\").get(c.id).put({ online: false, lastSeen: Date.now() });\n await sleep(3000);\n process.exit(0);\n};\n\nprocess.on(\"SIGTERM\", () => offlineAll(\"SIGTERM\"));\nprocess.on(\"SIGINT\", () => offlineAll(\"SIGINT\"));\nprocess.on(\"uncaughtException\", err => console.error(`[GLOBAL] UNCAUGHT: ${err.message}`));\nprocess.on(\"unhandledRejection\", r => console.error(`[GLOBAL] REJECTION: ${r}`));\n\nbootAllCitizens();\n"}}, ".env.agents.template": {"921a3b88c56f10963c08137c5c57aab9cd983d72": {"t": 1785747059.8248987, "n": 3655, "h": "554b3a486c06c970", "c": "# P2PCLAW Agents — Environment Variables Template\n# ================================================\n# Copy this file to .env.agents and fill in your API keys.\n# NEVER commit .env.agents to git.\n\n# ── Core API Backend ──────────────────────────────────────────────────────────\nGATEWAY=https://api-production-ff1b.up.railway.app\nRELAY_NODE=https://relay-production-3a20.up.railway.app/gun\n\n# ── LLM Providers ────────────────────────────────────────────────────────────\n\n# Groq (5 keys — 14,400 requests/day free per key)\nGROQ_KEY_1=gsk_...\nGROQ_KEY_2=gsk_...\nGROQ_KEY_3=gsk_...\nGROQ_KEY_4=gsk_...\nGROQ_KEY_5=gsk_...\n# For node-h (uses keys 4+5 only):\nGROQ_KEYS=${GROQ_KEY_1},${GROQ_KEY_2},${GROQ_KEY_3},${GROQ_KEY_4},${GROQ_KEY_5}\n\n# Gemini (6 keys — 15 RPM free per key)\nGEMINI_KEY_1=AIza...\nGEMINI_KEY_2=AIza...\nGEMINI_KEY_3=AIza...\nGEMINI_KEY_4=AIza...\nGEMINI_KEY_5=AIza...\nGEMINI_KEY_6=AIza...\nGEMINI_KEYS=${GEMINI_KEY_1},${GEMINI_KEY_2},${GEMINI_KEY_3},${GEMINI_KEY_4},${GEMINI_KEY_5},${GEMINI_KEY_6}\n\n# OpenRouter (6 keys — free models: mistral-7b, llama-3-8b)\nOPENROUTER_KEY_1=sk-or-...\nOPENROUTER_KEY_2=sk-or-...\nOPENROUTER_KEY_3=sk-or-...\nOPENROUTER_KEY_4=sk-or-...\nOPENROUTER_KEY_5=sk-or-...\nOPENROUTER_KEY_6=sk-or-...\nOPENROUTER_KEYS=${OPENROUTER_KEY_1},${OPENROUTER_KEY_2},${OPENROUTER_KEY_3},${OPENROUTER_KEY_4},${OPENROUTER_KEY_5},${OPENROUTER_KEY_6}\n\n# Z.ai / GLM-5 (6 keys)\nZAI_KEY_1=...\nZAI_KEY_2=...\nZAI_KEY_3=...\nZAI_KEY_4=...\nZAI_KEY_5=...\nZAI_KEY_6=...\nZAI_KEYS=${ZAI_KEY_1},${ZAI_KEY_2},${ZAI_KEY_3},${ZAI_KEY_4},${ZAI_KEY_5},${ZAI_KEY_6}\n\n# Together.ai (6 keys — free tier: Mistral-7B, Llama-2-7B)\nTOGETHER_KEY_1=...\nTOGETHER_KEY_2=...\nTOGETHER_KEY_3=...\nTOGETHER_KEY_4=...\nTOGETHER_KEY_5=...\nTOGETHER_KEY_6=...\nTOGETHER_KEYS=${TOGETHER_KEY_1},${TOGETHER_KEY_2},${TOGETHER_KEY_3},${TOGETHER_KEY_4},${TOGETHER_KEY_5},${TOGETHER_KEY_6}\n\n# DeepSeek (6 keys — free tier)\nDEEPSEEK_KEY_1=sk-...\nDEEPSEEK_KEY_2=sk-...\nDEEPSEEK_KEY_3=sk-...\nDEEPSEEK_KEY_4=sk-...\nDEEPSEEK_KEY_5=sk-...\nDEEPSEEK_KEY_6=sk-...\n\n# NVIDIA NIM (2 keys)\nNVIDIA_KEY_1=nvapi-...\nNVIDIA_KEY_2=nvapi-...\n\n# HuggingFace (inference API)\nHF_TOKEN=hf_YOUR_TOKEN_HERE\n\n# ── External Services ─────────────────────────────────────────────────────────\n\n# Moltbook API (paper publishing + Chirper social integration)\nMOLTBOOK_API_KEY=moltbook_sk_YOUR_KEY_HERE\n\n# Pinata IPFS (for paper archiving)\nPINATA_JWT=eyJhbGci...\n\n# Cloudflare (for Workers + KV + DNS)\nCLOUDFLARE_API_TOKEN=...\nCLOUDFLARE_ACCOUNT_ID=...\nCLOUDFLARE_ZONE_ID=...\n\n# Chirper.ai (optional — falls back to Moltbook)\nCHIRPER_API_KEY=...\n\n# ── Kaggle (new accounts) ────────────────────────────────────────────────────\n# Set these as GitHub Secrets: KAGGLE_JSON_CHARLYSMITH etc.\n# Each is a JSON string: {\"username\":\"...\",\"key\":\"...\"}\nKAGGLE_JSON_CHARLYSMITH={\"username\":\"charlysmith\",\"key\":\"...\"}\nKAGGLE_JSON_ESCRITORESANALFABETO={\"username\":\"escritoresanalfabeto\",\"key\":\"...\"}\nKAGGLE_JSON_KARMAKINDLE={\"username\":\"karmakindle\",\"key\":\"...\"}\nKAGGLE_JSON_NEBULAAGI={\"username\":\"nebulaagi\",\"key\":\"...\"}\nKAGGLE_JSON_FRANCISCOANGULO={\"username\":\"franciscoangulo\",\"key\":\"...\"}\n\n# ── Railway (API backend) ─────────────────────────────────────────────────────\n# Set these in Railway dashboard, NOT in this file:\n# ADMIN_SECRET=<random-secret>\n# PINATA_JWT=<pinata-jwt>\n# TIER1_VERIFIER_URL=https://agnuxo-p2pclaw-lean4-verifier.hf.space\n# PORT=3000\n# RELAY_NODE=https://relay-production-3a20.up.railway.app/gun\n\n# ── Batch Deploy Config ───────────────────────────────────────────────────────\n# Used by deploy-1000-agents.js when running in batch mode:\nBATCH_OFFSET=0\nBATCH_SIZE=200\nMAX_AGENTS=1000\nLLM_PROVIDER=groq\n"}, "cafba2488c16b75715bca818868683b9cc39dc41": {"t": 1785753915.3129168, "n": 3655, "h": "554b3a486c06c970", "c": "# P2PCLAW Agents — Environment Variables Template\n# ================================================\n# Copy this file to .env.agents and fill in your API keys.\n# NEVER commit .env.agents to git.\n\n# ── Core API Backend ──────────────────────────────────────────────────────────\nGATEWAY=https://api-production-ff1b.up.railway.app\nRELAY_NODE=https://relay-production-3a20.up.railway.app/gun\n\n# ── LLM Providers ────────────────────────────────────────────────────────────\n\n# Groq (5 keys — 14,400 requests/day free per key)\nGROQ_KEY_1=gsk_...\nGROQ_KEY_2=gsk_...\nGROQ_KEY_3=gsk_...\nGROQ_KEY_4=gsk_...\nGROQ_KEY_5=gsk_...\n# For node-h (uses keys 4+5 only):\nGROQ_KEYS=${GROQ_KEY_1},${GROQ_KEY_2},${GROQ_KEY_3},${GROQ_KEY_4},${GROQ_KEY_5}\n\n# Gemini (6 keys — 15 RPM free per key)\nGEMINI_KEY_1=AIza...\nGEMINI_KEY_2=AIza...\nGEMINI_KEY_3=AIza...\nGEMINI_KEY_4=AIza...\nGEMINI_KEY_5=AIza...\nGEMINI_KEY_6=AIza...\nGEMINI_KEYS=${GEMINI_KEY_1},${GEMINI_KEY_2},${GEMINI_KEY_3},${GEMINI_KEY_4},${GEMINI_KEY_5},${GEMINI_KEY_6}\n\n# OpenRouter (6 keys — free models: mistral-7b, llama-3-8b)\nOPENROUTER_KEY_1=sk-or-...\nOPENROUTER_KEY_2=sk-or-...\nOPENROUTER_KEY_3=sk-or-...\nOPENROUTER_KEY_4=sk-or-...\nOPENROUTER_KEY_5=sk-or-...\nOPENROUTER_KEY_6=sk-or-...\nOPENROUTER_KEYS=${OPENROUTER_KEY_1},${OPENROUTER_KEY_2},${OPENROUTER_KEY_3},${OPENROUTER_KEY_4},${OPENROUTER_KEY_5},${OPENROUTER_KEY_6}\n\n# Z.ai / GLM-5 (6 keys)\nZAI_KEY_1=...\nZAI_KEY_2=...\nZAI_KEY_3=...\nZAI_KEY_4=...\nZAI_KEY_5=...\nZAI_KEY_6=...\nZAI_KEYS=${ZAI_KEY_1},${ZAI_KEY_2},${ZAI_KEY_3},${ZAI_KEY_4},${ZAI_KEY_5},${ZAI_KEY_6}\n\n# Together.ai (6 keys — free tier: Mistral-7B, Llama-2-7B)\nTOGETHER_KEY_1=...\nTOGETHER_KEY_2=...\nTOGETHER_KEY_3=...\nTOGETHER_KEY_4=...\nTOGETHER_KEY_5=...\nTOGETHER_KEY_6=...\nTOGETHER_KEYS=${TOGETHER_KEY_1},${TOGETHER_KEY_2},${TOGETHER_KEY_3},${TOGETHER_KEY_4},${TOGETHER_KEY_5},${TOGETHER_KEY_6}\n\n# DeepSeek (6 keys — free tier)\nDEEPSEEK_KEY_1=sk-...\nDEEPSEEK_KEY_2=sk-...\nDEEPSEEK_KEY_3=sk-...\nDEEPSEEK_KEY_4=sk-...\nDEEPSEEK_KEY_5=sk-...\nDEEPSEEK_KEY_6=sk-...\n\n# NVIDIA NIM (2 keys)\nNVIDIA_KEY_1=nvapi-...\nNVIDIA_KEY_2=nvapi-...\n\n# HuggingFace (inference API)\nHF_TOKEN=hf_YOUR_TOKEN_HERE\n\n# ── External Services ─────────────────────────────────────────────────────────\n\n# Moltbook API (paper publishing + Chirper social integration)\nMOLTBOOK_API_KEY=moltbook_sk_YOUR_KEY_HERE\n\n# Pinata IPFS (for paper archiving)\nPINATA_JWT=eyJhbGci...\n\n# Cloudflare (for Workers + KV + DNS)\nCLOUDFLARE_API_TOKEN=...\nCLOUDFLARE_ACCOUNT_ID=...\nCLOUDFLARE_ZONE_ID=...\n\n# Chirper.ai (optional — falls back to Moltbook)\nCHIRPER_API_KEY=...\n\n# ── Kaggle (new accounts) ────────────────────────────────────────────────────\n# Set these as GitHub Secrets: KAGGLE_JSON_CHARLYSMITH etc.\n# Each is a JSON string: {\"username\":\"...\",\"key\":\"...\"}\nKAGGLE_JSON_CHARLYSMITH={\"username\":\"charlysmith\",\"key\":\"...\"}\nKAGGLE_JSON_ESCRITORESANALFABETO={\"username\":\"escritoresanalfabeto\",\"key\":\"...\"}\nKAGGLE_JSON_KARMAKINDLE={\"username\":\"karmakindle\",\"key\":\"...\"}\nKAGGLE_JSON_NEBULAAGI={\"username\":\"nebulaagi\",\"key\":\"...\"}\nKAGGLE_JSON_FRANCISCOANGULO={\"username\":\"franciscoangulo\",\"key\":\"...\"}\n\n# ── Railway (API backend) ─────────────────────────────────────────────────────\n# Set these in Railway dashboard, NOT in this file:\n# ADMIN_SECRET=<random-secret>\n# PINATA_JWT=<pinata-jwt>\n# TIER1_VERIFIER_URL=https://agnuxo-p2pclaw-lean4-verifier.hf.space\n# PORT=3000\n# RELAY_NODE=https://relay-production-3a20.up.railway.app/gun\n\n# ── Batch Deploy Config ───────────────────────────────────────────────────────\n# Used by deploy-1000-agents.js when running in batch mode:\nBATCH_OFFSET=0\nBATCH_SIZE=200\nMAX_AGENTS=1000\nLLM_PROVIDER=groq\n"}, "ad909b07e546264ace2423e92ec8cd74a5941c72": {"t": 1785753916.6058712, "n": 3661, "h": "9eb82aa5904ce7b0", "c": "# P2PCLAW Agents — Environment Variables Template\n# ================================================\n# Copy this file to .env.agents and fill in your API keys.\n# NEVER commit .env.agents to git.\n\n# ── Core API Backend ──────────────────────────────────────────────────────────\nGATEWAY=https://api-production-ff1b.up.railway.app\nRELAY_NODE=https://p2pclaw-relay-production.up.railway.app/gun\n\n# ── LLM Providers ────────────────────────────────────────────────────────────\n\n# Groq (5 keys — 14,400 requests/day free per key)\nGROQ_KEY_1=gsk_...\nGROQ_KEY_2=gsk_...\nGROQ_KEY_3=gsk_...\nGROQ_KEY_4=gsk_...\nGROQ_KEY_5=gsk_...\n# For node-h (uses keys 4+5 only):\nGROQ_KEYS=${GROQ_KEY_1},${GROQ_KEY_2},${GROQ_KEY_3},${GROQ_KEY_4},${GROQ_KEY_5}\n\n# Gemini (6 keys — 15 RPM free per key)\nGEMINI_KEY_1=AIza...\nGEMINI_KEY_2=AIza...\nGEMINI_KEY_3=AIza...\nGEMINI_KEY_4=AIza...\nGEMINI_KEY_5=AIza...\nGEMINI_KEY_6=AIza...\nGEMINI_KEYS=${GEMINI_KEY_1},${GEMINI_KEY_2},${GEMINI_KEY_3},${GEMINI_KEY_4},${GEMINI_KEY_5},${GEMINI_KEY_6}\n\n# OpenRouter (6 keys — free models: mistral-7b, llama-3-8b)\nOPENROUTER_KEY_1=sk-or-...\nOPENROUTER_KEY_2=sk-or-...\nOPENROUTER_KEY_3=sk-or-...\nOPENROUTER_KEY_4=sk-or-...\nOPENROUTER_KEY_5=sk-or-...\nOPENROUTER_KEY_6=sk-or-...\nOPENROUTER_KEYS=${OPENROUTER_KEY_1},${OPENROUTER_KEY_2},${OPENROUTER_KEY_3},${OPENROUTER_KEY_4},${OPENROUTER_KEY_5},${OPENROUTER_KEY_6}\n\n# Z.ai / GLM-5 (6 keys)\nZAI_KEY_1=...\nZAI_KEY_2=...\nZAI_KEY_3=...\nZAI_KEY_4=...\nZAI_KEY_5=...\nZAI_KEY_6=...\nZAI_KEYS=${ZAI_KEY_1},${ZAI_KEY_2},${ZAI_KEY_3},${ZAI_KEY_4},${ZAI_KEY_5},${ZAI_KEY_6}\n\n# Together.ai (6 keys — free tier: Mistral-7B, Llama-2-7B)\nTOGETHER_KEY_1=...\nTOGETHER_KEY_2=...\nTOGETHER_KEY_3=...\nTOGETHER_KEY_4=...\nTOGETHER_KEY_5=...\nTOGETHER_KEY_6=...\nTOGETHER_KEYS=${TOGETHER_KEY_1},${TOGETHER_KEY_2},${TOGETHER_KEY_3},${TOGETHER_KEY_4},${TOGETHER_KEY_5},${TOGETHER_KEY_6}\n\n# DeepSeek (6 keys — free tier)\nDEEPSEEK_KEY_1=sk-...\nDEEPSEEK_KEY_2=sk-...\nDEEPSEEK_KEY_3=sk-...\nDEEPSEEK_KEY_4=sk-...\nDEEPSEEK_KEY_5=sk-...\nDEEPSEEK_KEY_6=sk-...\n\n# NVIDIA NIM (2 keys)\nNVIDIA_KEY_1=nvapi-...\nNVIDIA_KEY_2=nvapi-...\n\n# HuggingFace (inference API)\nHF_TOKEN=hf_YOUR_TOKEN_HERE\n\n# ── External Services ─────────────────────────────────────────────────────────\n\n# Moltbook API (paper publishing + Chirper social integration)\nMOLTBOOK_API_KEY=moltbook_sk_YOUR_KEY_HERE\n\n# Pinata IPFS (for paper archiving)\nPINATA_JWT=eyJhbGci...\n\n# Cloudflare (for Workers + KV + DNS)\nCLOUDFLARE_API_TOKEN=...\nCLOUDFLARE_ACCOUNT_ID=...\nCLOUDFLARE_ZONE_ID=...\n\n# Chirper.ai (optional — falls back to Moltbook)\nCHIRPER_API_KEY=...\n\n# ── Kaggle (new accounts) ────────────────────────────────────────────────────\n# Set these as GitHub Secrets: KAGGLE_JSON_CHARLYSMITH etc.\n# Each is a JSON string: {\"username\":\"...\",\"key\":\"...\"}\nKAGGLE_JSON_CHARLYSMITH={\"username\":\"charlysmith\",\"key\":\"...\"}\nKAGGLE_JSON_ESCRITORESANALFABETO={\"username\":\"escritoresanalfabeto\",\"key\":\"...\"}\nKAGGLE_JSON_KARMAKINDLE={\"username\":\"karmakindle\",\"key\":\"...\"}\nKAGGLE_JSON_NEBULAAGI={\"username\":\"nebulaagi\",\"key\":\"...\"}\nKAGGLE_JSON_FRANCISCOANGULO={\"username\":\"franciscoangulo\",\"key\":\"...\"}\n\n# ── Railway (API backend) ─────────────────────────────────────────────────────\n# Set these in Railway dashboard, NOT in this file:\n# ADMIN_SECRET=<random-secret>\n# PINATA_JWT=<pinata-jwt>\n# TIER1_VERIFIER_URL=https://agnuxo-p2pclaw-lean4-verifier.hf.space\n# PORT=3000\n# RELAY_NODE=https://p2pclaw-relay-production.up.railway.app/gun\n\n# ── Batch Deploy Config ───────────────────────────────────────────────────────\n# Used by deploy-1000-agents.js when running in batch mode:\nBATCH_OFFSET=0\nBATCH_SIZE=200\nMAX_AGENTS=1000\nLLM_PROVIDER=groq\n"}}}}