Merge branch 'overnight/2026-03-09'
This commit is contained in:
@@ -0,0 +1,35 @@
|
|||||||
|
name: CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
pull_request:
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: pnpm/action-setup@v4
|
||||||
|
with:
|
||||||
|
version: 10
|
||||||
|
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 22
|
||||||
|
cache: pnpm
|
||||||
|
|
||||||
|
- run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Tests
|
||||||
|
run: pnpm test -- --run
|
||||||
|
|
||||||
|
- name: Type check
|
||||||
|
run: |
|
||||||
|
pnpm --filter server exec tsc --noEmit
|
||||||
|
pnpm --filter frontend exec vue-tsc --noEmit
|
||||||
|
|
||||||
|
- name: Lint
|
||||||
|
run: pnpm lint
|
||||||
+5
-1
@@ -5,10 +5,14 @@ target/
|
|||||||
__pycache__/
|
__pycache__/
|
||||||
*.pyc
|
*.pyc
|
||||||
.env
|
.env
|
||||||
.env.local
|
.env.*
|
||||||
|
!.env.example
|
||||||
.DS_Store
|
.DS_Store
|
||||||
loop/loop.log
|
loop/loop.log
|
||||||
server/data/
|
server/data/
|
||||||
loop/
|
loop/
|
||||||
.pnpm-store/
|
.pnpm-store/
|
||||||
.npmrc
|
.npmrc
|
||||||
|
*.pem
|
||||||
|
*.key
|
||||||
|
*.crt
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import tseslint from '@typescript-eslint/eslint-plugin'
|
||||||
|
import tsparser from '@typescript-eslint/parser'
|
||||||
|
|
||||||
|
export default [
|
||||||
|
{
|
||||||
|
ignores: ['**/dist/**', '**/node_modules/**', '**/*.js', '**/*.mjs', '**/*.cjs', '**/*.vue'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ['**/*.ts'],
|
||||||
|
languageOptions: {
|
||||||
|
parser: tsparser,
|
||||||
|
parserOptions: {
|
||||||
|
projectService: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
'@typescript-eslint': tseslint,
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
'@typescript-eslint/no-floating-promises': 'error',
|
||||||
|
'no-console': ['warn', { allow: ['warn', 'error'] }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Frontend game engine: fire-and-forget async (audio, animations) is intentional
|
||||||
|
{
|
||||||
|
files: ['frontend/src/game/**/*.ts'],
|
||||||
|
rules: {
|
||||||
|
'@typescript-eslint/no-floating-promises': 'warn',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
@@ -51,7 +51,7 @@ export function useHumanChallenge(
|
|||||||
if (humanChoices.value.length > 0 && !humanAnswer.value.trim()) {
|
if (humanChoices.value.length > 0 && !humanAnswer.value.trim()) {
|
||||||
humanAnswer.value = humanChoices.value[Math.floor(Math.random() * humanChoices.value.length)]
|
humanAnswer.value = humanChoices.value[Math.floor(Math.random() * humanChoices.value.length)]
|
||||||
}
|
}
|
||||||
submitHumanAnswer()
|
void submitHumanAnswer()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, 1000)
|
}, 1000)
|
||||||
@@ -77,7 +77,7 @@ export function useHumanChallenge(
|
|||||||
|
|
||||||
function submitChoice(choice: string) {
|
function submitChoice(choice: string) {
|
||||||
humanAnswer.value = choice
|
humanAnswer.value = choice
|
||||||
submitHumanAnswer()
|
void submitHumanAnswer()
|
||||||
}
|
}
|
||||||
|
|
||||||
async function pollForChallenge() {
|
async function pollForChallenge() {
|
||||||
@@ -110,8 +110,8 @@ export function useHumanChallenge(
|
|||||||
|
|
||||||
function startHumanPolling() {
|
function startHumanPolling() {
|
||||||
if (!myBotId.value) return
|
if (!myBotId.value) return
|
||||||
pollForChallenge()
|
void pollForChallenge()
|
||||||
humanPollHandle = setInterval(pollForChallenge, 400)
|
humanPollHandle = setInterval(() => { void pollForChallenge() }, 400)
|
||||||
}
|
}
|
||||||
|
|
||||||
function stopHumanPolling() {
|
function stopHumanPolling() {
|
||||||
|
|||||||
@@ -120,7 +120,7 @@ const _pending = new Map<number, {
|
|||||||
|
|
||||||
// --- Audio state (main thread only) ---
|
// --- Audio state (main thread only) ---
|
||||||
let _audioCtx: AudioContext | null = null
|
let _audioCtx: AudioContext | null = null
|
||||||
const audioCache = new Map<string, AudioBuffer>()
|
const audioCache = new Map<string, { buf: AudioBuffer; lastAccess: number }>()
|
||||||
const MAX_CACHE = 50
|
const MAX_CACHE = 50
|
||||||
const activeSources: Set<AudioBufferSourceNode> = new Set()
|
const activeSources: Set<AudioBufferSourceNode> = new Set()
|
||||||
|
|
||||||
@@ -265,7 +265,7 @@ async function _generateAndCache(text: string, profile: string): Promise<AudioBu
|
|||||||
if (!_workerReady) return null
|
if (!_workerReady) return null
|
||||||
const key = _cacheKey(text, profile)
|
const key = _cacheKey(text, profile)
|
||||||
const cached = audioCache.get(key)
|
const cached = audioCache.get(key)
|
||||||
if (cached) return cached
|
if (cached) { cached.lastAccess = Date.now(); return cached.buf }
|
||||||
|
|
||||||
// Dedup: if already generating this exact audio, reuse the in-flight promise
|
// Dedup: if already generating this exact audio, reuse the in-flight promise
|
||||||
const existing = _inflight.get(key)
|
const existing = _inflight.get(key)
|
||||||
@@ -288,12 +288,16 @@ async function _doGenerate(text: string, profile: string, key: string): Promise<
|
|||||||
const buf = ctx.createBuffer(1, raw.audio.length, raw.sampleRate)
|
const buf = ctx.createBuffer(1, raw.audio.length, raw.sampleRate)
|
||||||
buf.getChannelData(0).set(raw.audio)
|
buf.getChannelData(0).set(raw.audio)
|
||||||
|
|
||||||
// Evict oldest if cache is full
|
// Evict least-recently-used if cache is full
|
||||||
if (audioCache.size >= MAX_CACHE) {
|
if (audioCache.size >= MAX_CACHE) {
|
||||||
const oldest = audioCache.keys().next().value
|
let lruKey: string | undefined
|
||||||
if (oldest) audioCache.delete(oldest)
|
let lruTime = Infinity
|
||||||
|
for (const [k, v] of audioCache) {
|
||||||
|
if (v.lastAccess < lruTime) { lruTime = v.lastAccess; lruKey = k }
|
||||||
}
|
}
|
||||||
audioCache.set(key, buf)
|
if (lruKey) audioCache.delete(lruKey)
|
||||||
|
}
|
||||||
|
audioCache.set(key, { buf, lastAccess: Date.now() })
|
||||||
return buf
|
return buf
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -355,7 +359,8 @@ export function kokoroSpeak(
|
|||||||
const key = _cacheKey(text, profileName)
|
const key = _cacheKey(text, profileName)
|
||||||
const cached = audioCache.get(key)
|
const cached = audioCache.get(key)
|
||||||
if (cached) {
|
if (cached) {
|
||||||
_playBuffer(cached, dest, volume)
|
cached.lastAccess = Date.now()
|
||||||
|
_playBuffer(cached.buf, dest, volume)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
// Generate in worker — will play when ready
|
// Generate in worker — will play when ready
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ const tierClass = (t: number) => `tier-${t}`
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="h-[calc(100dvh-4rem)] flex flex-col px-3 sm:px-6 py-4 sm:py-6 overflow-hidden">
|
<div class="h-full flex flex-col px-3 sm:px-6 py-4 sm:py-6 overflow-hidden">
|
||||||
<div class="max-w-5xl mx-auto w-full flex flex-col flex-1 min-h-0">
|
<div class="max-w-5xl mx-auto w-full flex flex-col flex-1 min-h-0">
|
||||||
|
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
|
|||||||
@@ -354,7 +354,7 @@ const tierClass = (t: number) => `tier-${t}`
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="h-[calc(100dvh-4rem)] flex flex-col overflow-hidden">
|
<div class="h-full flex flex-col overflow-hidden">
|
||||||
<div class="max-w-lg lg:max-w-4xl mx-auto w-full flex flex-col flex-1 min-h-0 px-6 py-4 overflow-y-auto">
|
<div class="max-w-lg lg:max-w-4xl mx-auto w-full flex flex-col flex-1 min-h-0 px-6 py-4 overflow-y-auto">
|
||||||
|
|
||||||
<div v-if="isLoading" class="flex-1 flex items-center justify-center">
|
<div v-if="isLoading" class="flex-1 flex items-center justify-center">
|
||||||
|
|||||||
@@ -286,7 +286,7 @@ const registrationTest = `{
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="h-[calc(100dvh-4rem)] flex flex-col overflow-hidden">
|
<div class="h-full flex flex-col overflow-hidden">
|
||||||
<div class="flex flex-col flex-1 min-h-0 px-6 py-4 max-w-4xl mx-auto w-full">
|
<div class="flex flex-col flex-1 min-h-0 px-6 py-4 max-w-4xl mx-auto w-full">
|
||||||
|
|
||||||
<div class="text-center mb-4 shrink-0">
|
<div class="text-center mb-4 shrink-0">
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ onUnmounted(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="h-[calc(100dvh-4rem)] flex flex-col overflow-hidden relative">
|
<div class="h-full flex flex-col overflow-hidden relative">
|
||||||
|
|
||||||
<!-- Page background -->
|
<!-- Page background -->
|
||||||
<div class="absolute inset-0 overflow-hidden pointer-events-none">
|
<div class="absolute inset-0 overflow-hidden pointer-events-none">
|
||||||
|
|||||||
@@ -594,7 +594,7 @@ function stopAutoBattle() {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="h-[calc(100dvh-4rem)] flex flex-col px-2 sm:px-3 py-2 sm:py-3 overflow-hidden">
|
<div class="h-full flex flex-col px-2 sm:px-3 py-2 sm:py-3 overflow-hidden">
|
||||||
<div v-if="isLoading" class="flex-1 flex items-center justify-center">
|
<div v-if="isLoading" class="flex-1 flex items-center justify-center">
|
||||||
<p class="font-display text-text-muted animate-pulse tracking-wider">LOADING FIGHT...</p>
|
<p class="font-display text-text-muted animate-pulse tracking-wider">LOADING FIGHT...</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -315,7 +315,7 @@ onUnmounted(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="h-[calc(100dvh-4rem)] flex flex-col items-center justify-center px-6 overflow-hidden relative">
|
<div class="h-full flex flex-col items-center justify-center px-6 overflow-hidden relative">
|
||||||
<!-- Crowd characters floating around edges -->
|
<!-- Crowd characters floating around edges -->
|
||||||
<template v-for="(c, i) in crowd" :key="i">
|
<template v-for="(c, i) in crowd" :key="i">
|
||||||
<SpritePreview
|
<SpritePreview
|
||||||
|
|||||||
@@ -225,7 +225,7 @@ function goToArena() {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="h-[calc(100dvh-4rem)] flex flex-col items-center px-4 overflow-hidden">
|
<div class="h-full flex flex-col items-center px-4 overflow-hidden">
|
||||||
<div class="max-w-lg w-full flex-1 min-h-0 overflow-y-auto py-4">
|
<div class="max-w-lg w-full flex-1 min-h-0 overflow-y-auto py-4">
|
||||||
|
|
||||||
<!-- Feedback flash overlay -->
|
<!-- Feedback flash overlay -->
|
||||||
@@ -416,8 +416,8 @@ function goToArena() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- PHASE: REPLAY -->
|
<!-- PHASE: REPLAY -->
|
||||||
<div v-else-if="phase === 'replay' && fight" class="w-full">
|
<div v-else-if="phase === 'replay' && fight" class="w-full flex-1 min-h-0 flex flex-col">
|
||||||
<div class="h-[calc(100dvh-8rem)] min-h-0 relative">
|
<div class="flex-1 min-h-0 relative">
|
||||||
<FightViewer :fight="fight" :autoplay="true" class="h-full" />
|
<FightViewer :fight="fight" :autoplay="true" class="h-full" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-2 mt-2">
|
<div class="flex gap-2 mt-2">
|
||||||
|
|||||||
@@ -419,7 +419,7 @@ function handleSignOut() {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="h-[calc(100dvh-4rem)] flex flex-col items-center justify-center px-6 overflow-hidden">
|
<div class="h-full flex flex-col items-center justify-center px-6 overflow-hidden">
|
||||||
<div class="max-w-md w-full slide-up overflow-y-auto max-h-full py-4">
|
<div class="max-w-md w-full slide-up overflow-y-auto max-h-full py-4">
|
||||||
|
|
||||||
<!-- STEP: LOGIN -->
|
<!-- STEP: LOGIN -->
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ onUnmounted(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="h-[calc(100dvh-4rem)] flex flex-col px-6 py-6 overflow-hidden">
|
<div class="h-full flex flex-col px-6 py-6 overflow-hidden">
|
||||||
<div class="max-w-4xl mx-auto w-full flex flex-col flex-1 min-h-0">
|
<div class="max-w-4xl mx-auto w-full flex flex-col flex-1 min-h-0">
|
||||||
|
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ function goFight() {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="h-[calc(100dvh-4rem)] flex flex-col items-center justify-center px-6 overflow-hidden">
|
<div class="h-full flex flex-col items-center justify-center px-6 overflow-hidden">
|
||||||
<div class="max-w-md w-full slide-up">
|
<div class="max-w-md w-full slide-up">
|
||||||
|
|
||||||
<div class="text-center mb-8">
|
<div class="text-center mb-8">
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ onMounted(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="h-[calc(100dvh-4rem)] flex flex-col px-6 py-6 overflow-hidden">
|
<div class="h-full flex flex-col px-6 py-6 overflow-hidden">
|
||||||
<div class="max-w-4xl mx-auto w-full flex flex-col flex-1 min-h-0">
|
<div class="max-w-4xl mx-auto w-full flex flex-col flex-1 min-h-0">
|
||||||
|
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
|
|||||||
@@ -13,7 +13,10 @@
|
|||||||
"seed": "pnpm --filter server seed"
|
"seed": "pnpm --filter server seed"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@typescript-eslint/eslint-plugin": "^8.56.1",
|
||||||
|
"@typescript-eslint/parser": "^8.56.1",
|
||||||
"concurrently": "^9.1.2",
|
"concurrently": "^9.1.2",
|
||||||
|
"eslint": "^10.0.3",
|
||||||
"typescript": "^5.7.3",
|
"typescript": "^5.7.3",
|
||||||
"vitest": "^3.1.1"
|
"vitest": "^3.1.1"
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+588
@@ -8,9 +8,18 @@ importers:
|
|||||||
|
|
||||||
.:
|
.:
|
||||||
devDependencies:
|
devDependencies:
|
||||||
|
'@typescript-eslint/eslint-plugin':
|
||||||
|
specifier: ^8.56.1
|
||||||
|
version: 8.56.1(@typescript-eslint/parser@8.56.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3))(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3)
|
||||||
|
'@typescript-eslint/parser':
|
||||||
|
specifier: ^8.56.1
|
||||||
|
version: 8.56.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3)
|
||||||
concurrently:
|
concurrently:
|
||||||
specifier: ^9.1.2
|
specifier: ^9.1.2
|
||||||
version: 9.2.1
|
version: 9.2.1
|
||||||
|
eslint:
|
||||||
|
specifier: ^10.0.3
|
||||||
|
version: 10.0.3(jiti@2.6.1)
|
||||||
typescript:
|
typescript:
|
||||||
specifier: ^5.7.3
|
specifier: ^5.7.3
|
||||||
version: 5.9.3
|
version: 5.9.3
|
||||||
@@ -87,6 +96,9 @@ importers:
|
|||||||
nostr-tools:
|
nostr-tools:
|
||||||
specifier: ^2.23.3
|
specifier: ^2.23.3
|
||||||
version: 2.23.3(typescript@5.9.3)
|
version: 2.23.3(typescript@5.9.3)
|
||||||
|
zod:
|
||||||
|
specifier: ^4.3.6
|
||||||
|
version: 4.3.6
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@types/better-sqlite3':
|
'@types/better-sqlite3':
|
||||||
specifier: ^7.6.13
|
specifier: ^7.6.13
|
||||||
@@ -1051,6 +1063,36 @@ packages:
|
|||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [win32]
|
os: [win32]
|
||||||
|
|
||||||
|
'@eslint-community/eslint-utils@4.9.1':
|
||||||
|
resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==}
|
||||||
|
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||||
|
peerDependencies:
|
||||||
|
eslint: ^6.0.0 || ^7.0.0 || >=8.0.0
|
||||||
|
|
||||||
|
'@eslint-community/regexpp@4.12.2':
|
||||||
|
resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==}
|
||||||
|
engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0}
|
||||||
|
|
||||||
|
'@eslint/config-array@0.23.3':
|
||||||
|
resolution: {integrity: sha512-j+eEWmB6YYLwcNOdlwQ6L2OsptI/LO6lNBuLIqe5R7RetD658HLoF+Mn7LzYmAWWNNzdC6cqP+L6r8ujeYXWLw==}
|
||||||
|
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
|
||||||
|
|
||||||
|
'@eslint/config-helpers@0.5.3':
|
||||||
|
resolution: {integrity: sha512-lzGN0onllOZCGroKJmRwY6QcEHxbjBw1gwB8SgRSqK8YbbtEXMvKynsXc3553ckIEBxsbMBU7oOZXKIPGZNeZw==}
|
||||||
|
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
|
||||||
|
|
||||||
|
'@eslint/core@1.1.1':
|
||||||
|
resolution: {integrity: sha512-QUPblTtE51/7/Zhfv8BDwO0qkkzQL7P/aWWbqcf4xWLEYn1oKjdO0gglQBB4GAsu7u6wjijbCmzsUTy6mnk6oQ==}
|
||||||
|
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
|
||||||
|
|
||||||
|
'@eslint/object-schema@3.0.3':
|
||||||
|
resolution: {integrity: sha512-iM869Pugn9Nsxbh/YHRqYiqd23AmIbxJOcpUMOuWCVNdoQJ5ZtwL6h3t0bcZzJUlC3Dq9jCFCESBZnX0GTv7iQ==}
|
||||||
|
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
|
||||||
|
|
||||||
|
'@eslint/plugin-kit@0.6.1':
|
||||||
|
resolution: {integrity: sha512-iH1B076HoAshH1mLpHMgwdGeTs0CYwL0SPMkGuSebZrwBp16v415e9NZXg2jtrqPVQjf6IANe2Vtlr5KswtcZQ==}
|
||||||
|
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
|
||||||
|
|
||||||
'@hono/node-server@1.19.11':
|
'@hono/node-server@1.19.11':
|
||||||
resolution: {integrity: sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g==}
|
resolution: {integrity: sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g==}
|
||||||
engines: {node: '>=18.14.1'}
|
engines: {node: '>=18.14.1'}
|
||||||
@@ -1064,6 +1106,22 @@ packages:
|
|||||||
'@huggingface/transformers@3.8.1':
|
'@huggingface/transformers@3.8.1':
|
||||||
resolution: {integrity: sha512-tsTk4zVjImqdqjS8/AOZg2yNLd1z9S5v+7oUPpXaasDRwEDhB+xnglK1k5cad26lL5/ZIaeREgWWy0bs9y9pPA==}
|
resolution: {integrity: sha512-tsTk4zVjImqdqjS8/AOZg2yNLd1z9S5v+7oUPpXaasDRwEDhB+xnglK1k5cad26lL5/ZIaeREgWWy0bs9y9pPA==}
|
||||||
|
|
||||||
|
'@humanfs/core@0.19.1':
|
||||||
|
resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==}
|
||||||
|
engines: {node: '>=18.18.0'}
|
||||||
|
|
||||||
|
'@humanfs/node@0.16.7':
|
||||||
|
resolution: {integrity: sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==}
|
||||||
|
engines: {node: '>=18.18.0'}
|
||||||
|
|
||||||
|
'@humanwhocodes/module-importer@1.0.1':
|
||||||
|
resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==}
|
||||||
|
engines: {node: '>=12.22'}
|
||||||
|
|
||||||
|
'@humanwhocodes/retry@0.4.3':
|
||||||
|
resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==}
|
||||||
|
engines: {node: '>=18.18'}
|
||||||
|
|
||||||
'@img/colour@1.1.0':
|
'@img/colour@1.1.0':
|
||||||
resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==}
|
resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@@ -1591,12 +1649,18 @@ packages:
|
|||||||
'@types/deep-eql@4.0.2':
|
'@types/deep-eql@4.0.2':
|
||||||
resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==}
|
resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==}
|
||||||
|
|
||||||
|
'@types/esrecurse@4.3.1':
|
||||||
|
resolution: {integrity: sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==}
|
||||||
|
|
||||||
'@types/estree@0.0.39':
|
'@types/estree@0.0.39':
|
||||||
resolution: {integrity: sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==}
|
resolution: {integrity: sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==}
|
||||||
|
|
||||||
'@types/estree@1.0.8':
|
'@types/estree@1.0.8':
|
||||||
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
|
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
|
||||||
|
|
||||||
|
'@types/json-schema@7.0.15':
|
||||||
|
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
|
||||||
|
|
||||||
'@types/node@22.19.15':
|
'@types/node@22.19.15':
|
||||||
resolution: {integrity: sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==}
|
resolution: {integrity: sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==}
|
||||||
|
|
||||||
@@ -1606,6 +1670,65 @@ packages:
|
|||||||
'@types/trusted-types@2.0.7':
|
'@types/trusted-types@2.0.7':
|
||||||
resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==}
|
resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==}
|
||||||
|
|
||||||
|
'@typescript-eslint/eslint-plugin@8.56.1':
|
||||||
|
resolution: {integrity: sha512-Jz9ZztpB37dNC+HU2HI28Bs9QXpzCz+y/twHOwhyrIRdbuVDxSytJNDl6z/aAKlaRIwC7y8wJdkBv7FxYGgi0A==}
|
||||||
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
|
peerDependencies:
|
||||||
|
'@typescript-eslint/parser': ^8.56.1
|
||||||
|
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
|
||||||
|
typescript: '>=4.8.4 <6.0.0'
|
||||||
|
|
||||||
|
'@typescript-eslint/parser@8.56.1':
|
||||||
|
resolution: {integrity: sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==}
|
||||||
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
|
peerDependencies:
|
||||||
|
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
|
||||||
|
typescript: '>=4.8.4 <6.0.0'
|
||||||
|
|
||||||
|
'@typescript-eslint/project-service@8.56.1':
|
||||||
|
resolution: {integrity: sha512-TAdqQTzHNNvlVFfR+hu2PDJrURiwKsUvxFn1M0h95BB8ah5jejas08jUWG4dBA68jDMI988IvtfdAI53JzEHOQ==}
|
||||||
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
|
peerDependencies:
|
||||||
|
typescript: '>=4.8.4 <6.0.0'
|
||||||
|
|
||||||
|
'@typescript-eslint/scope-manager@8.56.1':
|
||||||
|
resolution: {integrity: sha512-YAi4VDKcIZp0O4tz/haYKhmIDZFEUPOreKbfdAN3SzUDMcPhJ8QI99xQXqX+HoUVq8cs85eRKnD+rne2UAnj2w==}
|
||||||
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
|
|
||||||
|
'@typescript-eslint/tsconfig-utils@8.56.1':
|
||||||
|
resolution: {integrity: sha512-qOtCYzKEeyr3aR9f28mPJqBty7+DBqsdd63eO0yyDwc6vgThj2UjWfJIcsFeSucYydqcuudMOprZ+x1SpF3ZuQ==}
|
||||||
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
|
peerDependencies:
|
||||||
|
typescript: '>=4.8.4 <6.0.0'
|
||||||
|
|
||||||
|
'@typescript-eslint/type-utils@8.56.1':
|
||||||
|
resolution: {integrity: sha512-yB/7dxi7MgTtGhZdaHCemf7PuwrHMenHjmzgUW1aJpO+bBU43OycnM3Wn+DdvDO/8zzA9HlhaJ0AUGuvri4oGg==}
|
||||||
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
|
peerDependencies:
|
||||||
|
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
|
||||||
|
typescript: '>=4.8.4 <6.0.0'
|
||||||
|
|
||||||
|
'@typescript-eslint/types@8.56.1':
|
||||||
|
resolution: {integrity: sha512-dbMkdIUkIkchgGDIv7KLUpa0Mda4IYjo4IAMJUZ+3xNoUXxMsk9YtKpTHSChRS85o+H9ftm51gsK1dZReY9CVw==}
|
||||||
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
|
|
||||||
|
'@typescript-eslint/typescript-estree@8.56.1':
|
||||||
|
resolution: {integrity: sha512-qzUL1qgalIvKWAf9C1HpvBjif+Vm6rcT5wZd4VoMb9+Km3iS3Cv9DY6dMRMDtPnwRAFyAi7YXJpTIEXLvdfPxg==}
|
||||||
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
|
peerDependencies:
|
||||||
|
typescript: '>=4.8.4 <6.0.0'
|
||||||
|
|
||||||
|
'@typescript-eslint/utils@8.56.1':
|
||||||
|
resolution: {integrity: sha512-HPAVNIME3tABJ61siYlHzSWCGtOoeP2RTIaHXFMPqjrQKCGB9OgUVdiNgH7TJS2JNIQ5qQ4RsAUDuGaGme/KOA==}
|
||||||
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
|
peerDependencies:
|
||||||
|
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
|
||||||
|
typescript: '>=4.8.4 <6.0.0'
|
||||||
|
|
||||||
|
'@typescript-eslint/visitor-keys@8.56.1':
|
||||||
|
resolution: {integrity: sha512-KiROIzYdEV85YygXw6BI/Dx4fnBlFQu6Mq4QE4MOH9fFnhohw6wX/OAvDY2/C+ut0I3RSPKenvZJIVYqJNkhEw==}
|
||||||
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
|
|
||||||
'@vitejs/plugin-vue@5.2.4':
|
'@vitejs/plugin-vue@5.2.4':
|
||||||
resolution: {integrity: sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==}
|
resolution: {integrity: sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==}
|
||||||
engines: {node: ^18.0.0 || >=20.0.0}
|
engines: {node: ^18.0.0 || >=20.0.0}
|
||||||
@@ -1694,11 +1817,19 @@ packages:
|
|||||||
'@vue/shared@3.5.29':
|
'@vue/shared@3.5.29':
|
||||||
resolution: {integrity: sha512-w7SR0A5zyRByL9XUkCfdLs7t9XOHUyJ67qPGQjOou3p6GvBeBW+AVjUUmlxtZ4PIYaRvE+1LmK44O4uajlZwcg==}
|
resolution: {integrity: sha512-w7SR0A5zyRByL9XUkCfdLs7t9XOHUyJ67qPGQjOou3p6GvBeBW+AVjUUmlxtZ4PIYaRvE+1LmK44O4uajlZwcg==}
|
||||||
|
|
||||||
|
acorn-jsx@5.3.2:
|
||||||
|
resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
|
||||||
|
peerDependencies:
|
||||||
|
acorn: ^6.0.0 || ^7.0.0 || ^8.0.0
|
||||||
|
|
||||||
acorn@8.16.0:
|
acorn@8.16.0:
|
||||||
resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==}
|
resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==}
|
||||||
engines: {node: '>=0.4.0'}
|
engines: {node: '>=0.4.0'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
ajv@6.14.0:
|
||||||
|
resolution: {integrity: sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==}
|
||||||
|
|
||||||
ajv@8.18.0:
|
ajv@8.18.0:
|
||||||
resolution: {integrity: sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==}
|
resolution: {integrity: sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==}
|
||||||
|
|
||||||
@@ -1919,6 +2050,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==}
|
resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==}
|
||||||
engines: {node: '>=4.0.0'}
|
engines: {node: '>=4.0.0'}
|
||||||
|
|
||||||
|
deep-is@0.1.4:
|
||||||
|
resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
|
||||||
|
|
||||||
deepmerge@4.3.1:
|
deepmerge@4.3.1:
|
||||||
resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==}
|
resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
@@ -2123,6 +2257,44 @@ packages:
|
|||||||
resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==}
|
resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
|
eslint-scope@9.1.2:
|
||||||
|
resolution: {integrity: sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==}
|
||||||
|
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
|
||||||
|
|
||||||
|
eslint-visitor-keys@3.4.3:
|
||||||
|
resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==}
|
||||||
|
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||||
|
|
||||||
|
eslint-visitor-keys@5.0.1:
|
||||||
|
resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==}
|
||||||
|
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
|
||||||
|
|
||||||
|
eslint@10.0.3:
|
||||||
|
resolution: {integrity: sha512-COV33RzXZkqhG9P2rZCFl9ZmJ7WL+gQSCRzE7RhkbclbQPtLAWReL7ysA0Sh4c8Im2U9ynybdR56PV0XcKvqaQ==}
|
||||||
|
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
|
||||||
|
hasBin: true
|
||||||
|
peerDependencies:
|
||||||
|
jiti: '*'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
jiti:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
espree@11.2.0:
|
||||||
|
resolution: {integrity: sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==}
|
||||||
|
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
|
||||||
|
|
||||||
|
esquery@1.7.0:
|
||||||
|
resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==}
|
||||||
|
engines: {node: '>=0.10'}
|
||||||
|
|
||||||
|
esrecurse@4.3.0:
|
||||||
|
resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==}
|
||||||
|
engines: {node: '>=4.0'}
|
||||||
|
|
||||||
|
estraverse@5.3.0:
|
||||||
|
resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==}
|
||||||
|
engines: {node: '>=4.0'}
|
||||||
|
|
||||||
estree-walker@1.0.1:
|
estree-walker@1.0.1:
|
||||||
resolution: {integrity: sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==}
|
resolution: {integrity: sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==}
|
||||||
|
|
||||||
@@ -2150,6 +2322,9 @@ packages:
|
|||||||
fast-json-stable-stringify@2.1.0:
|
fast-json-stable-stringify@2.1.0:
|
||||||
resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==}
|
resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==}
|
||||||
|
|
||||||
|
fast-levenshtein@2.0.6:
|
||||||
|
resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==}
|
||||||
|
|
||||||
fast-uri@3.1.0:
|
fast-uri@3.1.0:
|
||||||
resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==}
|
resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==}
|
||||||
|
|
||||||
@@ -2162,15 +2337,30 @@ packages:
|
|||||||
picomatch:
|
picomatch:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
file-entry-cache@8.0.0:
|
||||||
|
resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==}
|
||||||
|
engines: {node: '>=16.0.0'}
|
||||||
|
|
||||||
file-uri-to-path@1.0.0:
|
file-uri-to-path@1.0.0:
|
||||||
resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==}
|
resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==}
|
||||||
|
|
||||||
filelist@1.0.6:
|
filelist@1.0.6:
|
||||||
resolution: {integrity: sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA==}
|
resolution: {integrity: sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA==}
|
||||||
|
|
||||||
|
find-up@5.0.0:
|
||||||
|
resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
|
flat-cache@4.0.1:
|
||||||
|
resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==}
|
||||||
|
engines: {node: '>=16'}
|
||||||
|
|
||||||
flatbuffers@25.9.23:
|
flatbuffers@25.9.23:
|
||||||
resolution: {integrity: sha512-MI1qs7Lo4Syw0EOzUl0xjs2lsoeqFku44KpngfIduHBYvzm8h2+7K8YMQh1JtVVVrUvhLpNwqVi4DERegUJhPQ==}
|
resolution: {integrity: sha512-MI1qs7Lo4Syw0EOzUl0xjs2lsoeqFku44KpngfIduHBYvzm8h2+7K8YMQh1JtVVVrUvhLpNwqVi4DERegUJhPQ==}
|
||||||
|
|
||||||
|
flatted@3.4.0:
|
||||||
|
resolution: {integrity: sha512-kC6Bb+ooptOIvWj5B63EQWkF0FEnNjV2ZNkLMLZRDDduIiWeFF4iKnslwhiWxjAdbg4NzTNo6h0qLuvFrcx+Sw==}
|
||||||
|
|
||||||
for-each@0.3.5:
|
for-each@0.3.5:
|
||||||
resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==}
|
resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@@ -2239,6 +2429,10 @@ packages:
|
|||||||
github-from-package@0.0.0:
|
github-from-package@0.0.0:
|
||||||
resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==}
|
resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==}
|
||||||
|
|
||||||
|
glob-parent@6.0.2:
|
||||||
|
resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==}
|
||||||
|
engines: {node: '>=10.13.0'}
|
||||||
|
|
||||||
glob@11.1.0:
|
glob@11.1.0:
|
||||||
resolution: {integrity: sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==}
|
resolution: {integrity: sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==}
|
||||||
engines: {node: 20 || >=22}
|
engines: {node: 20 || >=22}
|
||||||
@@ -2304,6 +2498,18 @@ packages:
|
|||||||
ieee754@1.2.1:
|
ieee754@1.2.1:
|
||||||
resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
|
resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
|
||||||
|
|
||||||
|
ignore@5.3.2:
|
||||||
|
resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
|
||||||
|
engines: {node: '>= 4'}
|
||||||
|
|
||||||
|
ignore@7.0.5:
|
||||||
|
resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==}
|
||||||
|
engines: {node: '>= 4'}
|
||||||
|
|
||||||
|
imurmurhash@0.1.4:
|
||||||
|
resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==}
|
||||||
|
engines: {node: '>=0.8.19'}
|
||||||
|
|
||||||
inherits@2.0.4:
|
inherits@2.0.4:
|
||||||
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
|
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
|
||||||
|
|
||||||
@@ -2346,6 +2552,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==}
|
resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
is-extglob@2.1.1:
|
||||||
|
resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
|
||||||
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
is-finalizationregistry@1.1.1:
|
is-finalizationregistry@1.1.1:
|
||||||
resolution: {integrity: sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==}
|
resolution: {integrity: sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@@ -2358,6 +2568,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==}
|
resolution: {integrity: sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
is-glob@4.0.3:
|
||||||
|
resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
|
||||||
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
is-map@2.0.3:
|
is-map@2.0.3:
|
||||||
resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==}
|
resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@@ -2455,12 +2669,21 @@ packages:
|
|||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
json-buffer@3.0.1:
|
||||||
|
resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==}
|
||||||
|
|
||||||
|
json-schema-traverse@0.4.1:
|
||||||
|
resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==}
|
||||||
|
|
||||||
json-schema-traverse@1.0.0:
|
json-schema-traverse@1.0.0:
|
||||||
resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==}
|
resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==}
|
||||||
|
|
||||||
json-schema@0.4.0:
|
json-schema@0.4.0:
|
||||||
resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==}
|
resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==}
|
||||||
|
|
||||||
|
json-stable-stringify-without-jsonify@1.0.1:
|
||||||
|
resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==}
|
||||||
|
|
||||||
json-stringify-safe@5.0.1:
|
json-stringify-safe@5.0.1:
|
||||||
resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==}
|
resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==}
|
||||||
|
|
||||||
@@ -2480,6 +2703,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-T8GdXGXvgv/vbYVA1lcHzuDNVhp3juOJJE8OZs0vR5MdGNElBvANEeTSnqAAhJpSXtNxpeNy29pqkok3RnXKtg==}
|
resolution: {integrity: sha512-T8GdXGXvgv/vbYVA1lcHzuDNVhp3juOJJE8OZs0vR5MdGNElBvANEeTSnqAAhJpSXtNxpeNy29pqkok3RnXKtg==}
|
||||||
engines: {node: '>=20.0.0'}
|
engines: {node: '>=20.0.0'}
|
||||||
|
|
||||||
|
keyv@4.5.4:
|
||||||
|
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
|
||||||
|
|
||||||
kokoro-js@1.2.1:
|
kokoro-js@1.2.1:
|
||||||
resolution: {integrity: sha512-oq0HZJWis3t8lERkMJh84WLU86dpYD0EuBPtqYnLlQzyFP1OkyBRDcweAqCfhNOpltyN9j/azp1H6uuC47gShw==}
|
resolution: {integrity: sha512-oq0HZJWis3t8lERkMJh84WLU86dpYD0EuBPtqYnLlQzyFP1OkyBRDcweAqCfhNOpltyN9j/azp1H6uuC47gShw==}
|
||||||
|
|
||||||
@@ -2487,6 +2713,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==}
|
resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
|
levn@0.4.1:
|
||||||
|
resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
|
||||||
|
engines: {node: '>= 0.8.0'}
|
||||||
|
|
||||||
lightningcss-android-arm64@1.31.1:
|
lightningcss-android-arm64@1.31.1:
|
||||||
resolution: {integrity: sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg==}
|
resolution: {integrity: sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg==}
|
||||||
engines: {node: '>= 12.0.0'}
|
engines: {node: '>= 12.0.0'}
|
||||||
@@ -2561,6 +2791,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ==}
|
resolution: {integrity: sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ==}
|
||||||
engines: {node: '>= 12.0.0'}
|
engines: {node: '>= 12.0.0'}
|
||||||
|
|
||||||
|
locate-path@6.0.0:
|
||||||
|
resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
lodash.debounce@4.0.8:
|
lodash.debounce@4.0.8:
|
||||||
resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==}
|
resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==}
|
||||||
|
|
||||||
@@ -2646,6 +2880,9 @@ packages:
|
|||||||
napi-build-utils@2.0.0:
|
napi-build-utils@2.0.0:
|
||||||
resolution: {integrity: sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==}
|
resolution: {integrity: sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==}
|
||||||
|
|
||||||
|
natural-compare@1.4.0:
|
||||||
|
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
|
||||||
|
|
||||||
node-abi@3.87.0:
|
node-abi@3.87.0:
|
||||||
resolution: {integrity: sha512-+CGM1L1CgmtheLcBuleyYOn7NWPVu0s0EJH2C4puxgEZb9h8QpR9G2dBfZJOAUhi7VQxuBPMd0hiISWcTyiYyQ==}
|
resolution: {integrity: sha512-+CGM1L1CgmtheLcBuleyYOn7NWPVu0s0EJH2C4puxgEZb9h8QpR9G2dBfZJOAUhi7VQxuBPMd0hiISWcTyiYyQ==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
@@ -2692,16 +2929,32 @@ packages:
|
|||||||
onnxruntime-web@1.22.0-dev.20250409-89f8206ba4:
|
onnxruntime-web@1.22.0-dev.20250409-89f8206ba4:
|
||||||
resolution: {integrity: sha512-0uS76OPgH0hWCPrFKlL8kYVV7ckM7t/36HfbgoFw6Nd0CZVVbQC4PkrR8mBX8LtNUFZO25IQBqV2Hx2ho3FlbQ==}
|
resolution: {integrity: sha512-0uS76OPgH0hWCPrFKlL8kYVV7ckM7t/36HfbgoFw6Nd0CZVVbQC4PkrR8mBX8LtNUFZO25IQBqV2Hx2ho3FlbQ==}
|
||||||
|
|
||||||
|
optionator@0.9.4:
|
||||||
|
resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
|
||||||
|
engines: {node: '>= 0.8.0'}
|
||||||
|
|
||||||
own-keys@1.0.1:
|
own-keys@1.0.1:
|
||||||
resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==}
|
resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
p-limit@3.1.0:
|
||||||
|
resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
|
p-locate@5.0.0:
|
||||||
|
resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
package-json-from-dist@1.0.1:
|
package-json-from-dist@1.0.1:
|
||||||
resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==}
|
resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==}
|
||||||
|
|
||||||
path-browserify@1.0.1:
|
path-browserify@1.0.1:
|
||||||
resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==}
|
resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==}
|
||||||
|
|
||||||
|
path-exists@4.0.0:
|
||||||
|
resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
path-key@3.1.1:
|
path-key@3.1.1:
|
||||||
resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
|
resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@@ -2751,6 +3004,10 @@ packages:
|
|||||||
deprecated: No longer maintained. Please contact the author of the relevant native addon; alternatives are available.
|
deprecated: No longer maintained. Please contact the author of the relevant native addon; alternatives are available.
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
prelude-ls@1.2.1:
|
||||||
|
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
|
||||||
|
engines: {node: '>= 0.8.0'}
|
||||||
|
|
||||||
pretty-bytes@5.6.0:
|
pretty-bytes@5.6.0:
|
||||||
resolution: {integrity: sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==}
|
resolution: {integrity: sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
@@ -3082,6 +3339,12 @@ packages:
|
|||||||
resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==}
|
resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
ts-api-utils@2.4.0:
|
||||||
|
resolution: {integrity: sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==}
|
||||||
|
engines: {node: '>=18.12'}
|
||||||
|
peerDependencies:
|
||||||
|
typescript: '>=4.8.4'
|
||||||
|
|
||||||
tslib@2.8.1:
|
tslib@2.8.1:
|
||||||
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
|
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
|
||||||
|
|
||||||
@@ -3093,6 +3356,10 @@ packages:
|
|||||||
tunnel-agent@0.6.0:
|
tunnel-agent@0.6.0:
|
||||||
resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==}
|
resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==}
|
||||||
|
|
||||||
|
type-check@0.4.0:
|
||||||
|
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
|
||||||
|
engines: {node: '>= 0.8.0'}
|
||||||
|
|
||||||
type-fest@0.13.1:
|
type-fest@0.13.1:
|
||||||
resolution: {integrity: sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==}
|
resolution: {integrity: sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
@@ -3163,6 +3430,9 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
browserslist: '>= 4.21.0'
|
browserslist: '>= 4.21.0'
|
||||||
|
|
||||||
|
uri-js@4.4.1:
|
||||||
|
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
|
||||||
|
|
||||||
util-deprecate@1.0.2:
|
util-deprecate@1.0.2:
|
||||||
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
||||||
|
|
||||||
@@ -3310,6 +3580,10 @@ packages:
|
|||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
word-wrap@1.2.5:
|
||||||
|
resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
|
||||||
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
workbox-background-sync@7.4.0:
|
workbox-background-sync@7.4.0:
|
||||||
resolution: {integrity: sha512-8CB9OxKAgKZKyNMwfGZ1XESx89GryWTfI+V5yEj8sHjFH8MFelUwYXEyldEK6M6oKMmn807GoJFUEA1sC4XS9w==}
|
resolution: {integrity: sha512-8CB9OxKAgKZKyNMwfGZ1XESx89GryWTfI+V5yEj8sHjFH8MFelUwYXEyldEK6M6oKMmn807GoJFUEA1sC4XS9w==}
|
||||||
|
|
||||||
@@ -3385,6 +3659,13 @@ packages:
|
|||||||
resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==}
|
resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
yocto-queue@0.1.0:
|
||||||
|
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
|
zod@4.3.6:
|
||||||
|
resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==}
|
||||||
|
|
||||||
snapshots:
|
snapshots:
|
||||||
|
|
||||||
'@apideck/better-ajv-errors@0.3.6(ajv@8.18.0)':
|
'@apideck/better-ajv-errors@0.3.6(ajv@8.18.0)':
|
||||||
@@ -4285,6 +4566,36 @@ snapshots:
|
|||||||
'@esbuild/win32-x64@0.27.3':
|
'@esbuild/win32-x64@0.27.3':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@eslint-community/eslint-utils@4.9.1(eslint@10.0.3(jiti@2.6.1))':
|
||||||
|
dependencies:
|
||||||
|
eslint: 10.0.3(jiti@2.6.1)
|
||||||
|
eslint-visitor-keys: 3.4.3
|
||||||
|
|
||||||
|
'@eslint-community/regexpp@4.12.2': {}
|
||||||
|
|
||||||
|
'@eslint/config-array@0.23.3':
|
||||||
|
dependencies:
|
||||||
|
'@eslint/object-schema': 3.0.3
|
||||||
|
debug: 4.4.3
|
||||||
|
minimatch: 10.2.4
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
|
||||||
|
'@eslint/config-helpers@0.5.3':
|
||||||
|
dependencies:
|
||||||
|
'@eslint/core': 1.1.1
|
||||||
|
|
||||||
|
'@eslint/core@1.1.1':
|
||||||
|
dependencies:
|
||||||
|
'@types/json-schema': 7.0.15
|
||||||
|
|
||||||
|
'@eslint/object-schema@3.0.3': {}
|
||||||
|
|
||||||
|
'@eslint/plugin-kit@0.6.1':
|
||||||
|
dependencies:
|
||||||
|
'@eslint/core': 1.1.1
|
||||||
|
levn: 0.4.1
|
||||||
|
|
||||||
'@hono/node-server@1.19.11(hono@4.12.5)':
|
'@hono/node-server@1.19.11(hono@4.12.5)':
|
||||||
dependencies:
|
dependencies:
|
||||||
hono: 4.12.5
|
hono: 4.12.5
|
||||||
@@ -4298,6 +4609,17 @@ snapshots:
|
|||||||
onnxruntime-web: 1.22.0-dev.20250409-89f8206ba4
|
onnxruntime-web: 1.22.0-dev.20250409-89f8206ba4
|
||||||
sharp: 0.34.5
|
sharp: 0.34.5
|
||||||
|
|
||||||
|
'@humanfs/core@0.19.1': {}
|
||||||
|
|
||||||
|
'@humanfs/node@0.16.7':
|
||||||
|
dependencies:
|
||||||
|
'@humanfs/core': 0.19.1
|
||||||
|
'@humanwhocodes/retry': 0.4.3
|
||||||
|
|
||||||
|
'@humanwhocodes/module-importer@1.0.1': {}
|
||||||
|
|
||||||
|
'@humanwhocodes/retry@0.4.3': {}
|
||||||
|
|
||||||
'@img/colour@1.1.0': {}
|
'@img/colour@1.1.0': {}
|
||||||
|
|
||||||
'@img/sharp-darwin-arm64@0.34.5':
|
'@img/sharp-darwin-arm64@0.34.5':
|
||||||
@@ -4679,10 +5001,14 @@ snapshots:
|
|||||||
|
|
||||||
'@types/deep-eql@4.0.2': {}
|
'@types/deep-eql@4.0.2': {}
|
||||||
|
|
||||||
|
'@types/esrecurse@4.3.1': {}
|
||||||
|
|
||||||
'@types/estree@0.0.39': {}
|
'@types/estree@0.0.39': {}
|
||||||
|
|
||||||
'@types/estree@1.0.8': {}
|
'@types/estree@1.0.8': {}
|
||||||
|
|
||||||
|
'@types/json-schema@7.0.15': {}
|
||||||
|
|
||||||
'@types/node@22.19.15':
|
'@types/node@22.19.15':
|
||||||
dependencies:
|
dependencies:
|
||||||
undici-types: 6.21.0
|
undici-types: 6.21.0
|
||||||
@@ -4691,6 +5017,97 @@ snapshots:
|
|||||||
|
|
||||||
'@types/trusted-types@2.0.7': {}
|
'@types/trusted-types@2.0.7': {}
|
||||||
|
|
||||||
|
'@typescript-eslint/eslint-plugin@8.56.1(@typescript-eslint/parser@8.56.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3))(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3)':
|
||||||
|
dependencies:
|
||||||
|
'@eslint-community/regexpp': 4.12.2
|
||||||
|
'@typescript-eslint/parser': 8.56.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3)
|
||||||
|
'@typescript-eslint/scope-manager': 8.56.1
|
||||||
|
'@typescript-eslint/type-utils': 8.56.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3)
|
||||||
|
'@typescript-eslint/utils': 8.56.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3)
|
||||||
|
'@typescript-eslint/visitor-keys': 8.56.1
|
||||||
|
eslint: 10.0.3(jiti@2.6.1)
|
||||||
|
ignore: 7.0.5
|
||||||
|
natural-compare: 1.4.0
|
||||||
|
ts-api-utils: 2.4.0(typescript@5.9.3)
|
||||||
|
typescript: 5.9.3
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
|
||||||
|
'@typescript-eslint/parser@8.56.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3)':
|
||||||
|
dependencies:
|
||||||
|
'@typescript-eslint/scope-manager': 8.56.1
|
||||||
|
'@typescript-eslint/types': 8.56.1
|
||||||
|
'@typescript-eslint/typescript-estree': 8.56.1(typescript@5.9.3)
|
||||||
|
'@typescript-eslint/visitor-keys': 8.56.1
|
||||||
|
debug: 4.4.3
|
||||||
|
eslint: 10.0.3(jiti@2.6.1)
|
||||||
|
typescript: 5.9.3
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
|
||||||
|
'@typescript-eslint/project-service@8.56.1(typescript@5.9.3)':
|
||||||
|
dependencies:
|
||||||
|
'@typescript-eslint/tsconfig-utils': 8.56.1(typescript@5.9.3)
|
||||||
|
'@typescript-eslint/types': 8.56.1
|
||||||
|
debug: 4.4.3
|
||||||
|
typescript: 5.9.3
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
|
||||||
|
'@typescript-eslint/scope-manager@8.56.1':
|
||||||
|
dependencies:
|
||||||
|
'@typescript-eslint/types': 8.56.1
|
||||||
|
'@typescript-eslint/visitor-keys': 8.56.1
|
||||||
|
|
||||||
|
'@typescript-eslint/tsconfig-utils@8.56.1(typescript@5.9.3)':
|
||||||
|
dependencies:
|
||||||
|
typescript: 5.9.3
|
||||||
|
|
||||||
|
'@typescript-eslint/type-utils@8.56.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3)':
|
||||||
|
dependencies:
|
||||||
|
'@typescript-eslint/types': 8.56.1
|
||||||
|
'@typescript-eslint/typescript-estree': 8.56.1(typescript@5.9.3)
|
||||||
|
'@typescript-eslint/utils': 8.56.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3)
|
||||||
|
debug: 4.4.3
|
||||||
|
eslint: 10.0.3(jiti@2.6.1)
|
||||||
|
ts-api-utils: 2.4.0(typescript@5.9.3)
|
||||||
|
typescript: 5.9.3
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
|
||||||
|
'@typescript-eslint/types@8.56.1': {}
|
||||||
|
|
||||||
|
'@typescript-eslint/typescript-estree@8.56.1(typescript@5.9.3)':
|
||||||
|
dependencies:
|
||||||
|
'@typescript-eslint/project-service': 8.56.1(typescript@5.9.3)
|
||||||
|
'@typescript-eslint/tsconfig-utils': 8.56.1(typescript@5.9.3)
|
||||||
|
'@typescript-eslint/types': 8.56.1
|
||||||
|
'@typescript-eslint/visitor-keys': 8.56.1
|
||||||
|
debug: 4.4.3
|
||||||
|
minimatch: 10.2.4
|
||||||
|
semver: 7.7.4
|
||||||
|
tinyglobby: 0.2.15
|
||||||
|
ts-api-utils: 2.4.0(typescript@5.9.3)
|
||||||
|
typescript: 5.9.3
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
|
||||||
|
'@typescript-eslint/utils@8.56.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3)':
|
||||||
|
dependencies:
|
||||||
|
'@eslint-community/eslint-utils': 4.9.1(eslint@10.0.3(jiti@2.6.1))
|
||||||
|
'@typescript-eslint/scope-manager': 8.56.1
|
||||||
|
'@typescript-eslint/types': 8.56.1
|
||||||
|
'@typescript-eslint/typescript-estree': 8.56.1(typescript@5.9.3)
|
||||||
|
eslint: 10.0.3(jiti@2.6.1)
|
||||||
|
typescript: 5.9.3
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
|
||||||
|
'@typescript-eslint/visitor-keys@8.56.1':
|
||||||
|
dependencies:
|
||||||
|
'@typescript-eslint/types': 8.56.1
|
||||||
|
eslint-visitor-keys: 5.0.1
|
||||||
|
|
||||||
'@vitejs/plugin-vue@5.2.4(vite@7.3.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0))(vue@3.5.29(typescript@5.9.3))':
|
'@vitejs/plugin-vue@5.2.4(vite@7.3.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0))(vue@3.5.29(typescript@5.9.3))':
|
||||||
dependencies:
|
dependencies:
|
||||||
vite: 7.3.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)
|
vite: 7.3.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)
|
||||||
@@ -4824,8 +5241,19 @@ snapshots:
|
|||||||
|
|
||||||
'@vue/shared@3.5.29': {}
|
'@vue/shared@3.5.29': {}
|
||||||
|
|
||||||
|
acorn-jsx@5.3.2(acorn@8.16.0):
|
||||||
|
dependencies:
|
||||||
|
acorn: 8.16.0
|
||||||
|
|
||||||
acorn@8.16.0: {}
|
acorn@8.16.0: {}
|
||||||
|
|
||||||
|
ajv@6.14.0:
|
||||||
|
dependencies:
|
||||||
|
fast-deep-equal: 3.1.3
|
||||||
|
fast-json-stable-stringify: 2.1.0
|
||||||
|
json-schema-traverse: 0.4.1
|
||||||
|
uri-js: 4.4.1
|
||||||
|
|
||||||
ajv@8.18.0:
|
ajv@8.18.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
fast-deep-equal: 3.1.3
|
fast-deep-equal: 3.1.3
|
||||||
@@ -5055,6 +5483,8 @@ snapshots:
|
|||||||
|
|
||||||
deep-extend@0.6.0: {}
|
deep-extend@0.6.0: {}
|
||||||
|
|
||||||
|
deep-is@0.1.4: {}
|
||||||
|
|
||||||
deepmerge@4.3.1: {}
|
deepmerge@4.3.1: {}
|
||||||
|
|
||||||
define-data-property@1.1.4:
|
define-data-property@1.1.4:
|
||||||
@@ -5291,6 +5721,70 @@ snapshots:
|
|||||||
|
|
||||||
escape-string-regexp@4.0.0: {}
|
escape-string-regexp@4.0.0: {}
|
||||||
|
|
||||||
|
eslint-scope@9.1.2:
|
||||||
|
dependencies:
|
||||||
|
'@types/esrecurse': 4.3.1
|
||||||
|
'@types/estree': 1.0.8
|
||||||
|
esrecurse: 4.3.0
|
||||||
|
estraverse: 5.3.0
|
||||||
|
|
||||||
|
eslint-visitor-keys@3.4.3: {}
|
||||||
|
|
||||||
|
eslint-visitor-keys@5.0.1: {}
|
||||||
|
|
||||||
|
eslint@10.0.3(jiti@2.6.1):
|
||||||
|
dependencies:
|
||||||
|
'@eslint-community/eslint-utils': 4.9.1(eslint@10.0.3(jiti@2.6.1))
|
||||||
|
'@eslint-community/regexpp': 4.12.2
|
||||||
|
'@eslint/config-array': 0.23.3
|
||||||
|
'@eslint/config-helpers': 0.5.3
|
||||||
|
'@eslint/core': 1.1.1
|
||||||
|
'@eslint/plugin-kit': 0.6.1
|
||||||
|
'@humanfs/node': 0.16.7
|
||||||
|
'@humanwhocodes/module-importer': 1.0.1
|
||||||
|
'@humanwhocodes/retry': 0.4.3
|
||||||
|
'@types/estree': 1.0.8
|
||||||
|
ajv: 6.14.0
|
||||||
|
cross-spawn: 7.0.6
|
||||||
|
debug: 4.4.3
|
||||||
|
escape-string-regexp: 4.0.0
|
||||||
|
eslint-scope: 9.1.2
|
||||||
|
eslint-visitor-keys: 5.0.1
|
||||||
|
espree: 11.2.0
|
||||||
|
esquery: 1.7.0
|
||||||
|
esutils: 2.0.3
|
||||||
|
fast-deep-equal: 3.1.3
|
||||||
|
file-entry-cache: 8.0.0
|
||||||
|
find-up: 5.0.0
|
||||||
|
glob-parent: 6.0.2
|
||||||
|
ignore: 5.3.2
|
||||||
|
imurmurhash: 0.1.4
|
||||||
|
is-glob: 4.0.3
|
||||||
|
json-stable-stringify-without-jsonify: 1.0.1
|
||||||
|
minimatch: 10.2.4
|
||||||
|
natural-compare: 1.4.0
|
||||||
|
optionator: 0.9.4
|
||||||
|
optionalDependencies:
|
||||||
|
jiti: 2.6.1
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
|
||||||
|
espree@11.2.0:
|
||||||
|
dependencies:
|
||||||
|
acorn: 8.16.0
|
||||||
|
acorn-jsx: 5.3.2(acorn@8.16.0)
|
||||||
|
eslint-visitor-keys: 5.0.1
|
||||||
|
|
||||||
|
esquery@1.7.0:
|
||||||
|
dependencies:
|
||||||
|
estraverse: 5.3.0
|
||||||
|
|
||||||
|
esrecurse@4.3.0:
|
||||||
|
dependencies:
|
||||||
|
estraverse: 5.3.0
|
||||||
|
|
||||||
|
estraverse@5.3.0: {}
|
||||||
|
|
||||||
estree-walker@1.0.1: {}
|
estree-walker@1.0.1: {}
|
||||||
|
|
||||||
estree-walker@2.0.2: {}
|
estree-walker@2.0.2: {}
|
||||||
@@ -5309,20 +5803,38 @@ snapshots:
|
|||||||
|
|
||||||
fast-json-stable-stringify@2.1.0: {}
|
fast-json-stable-stringify@2.1.0: {}
|
||||||
|
|
||||||
|
fast-levenshtein@2.0.6: {}
|
||||||
|
|
||||||
fast-uri@3.1.0: {}
|
fast-uri@3.1.0: {}
|
||||||
|
|
||||||
fdir@6.5.0(picomatch@4.0.3):
|
fdir@6.5.0(picomatch@4.0.3):
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
picomatch: 4.0.3
|
picomatch: 4.0.3
|
||||||
|
|
||||||
|
file-entry-cache@8.0.0:
|
||||||
|
dependencies:
|
||||||
|
flat-cache: 4.0.1
|
||||||
|
|
||||||
file-uri-to-path@1.0.0: {}
|
file-uri-to-path@1.0.0: {}
|
||||||
|
|
||||||
filelist@1.0.6:
|
filelist@1.0.6:
|
||||||
dependencies:
|
dependencies:
|
||||||
minimatch: 5.1.9
|
minimatch: 5.1.9
|
||||||
|
|
||||||
|
find-up@5.0.0:
|
||||||
|
dependencies:
|
||||||
|
locate-path: 6.0.0
|
||||||
|
path-exists: 4.0.0
|
||||||
|
|
||||||
|
flat-cache@4.0.1:
|
||||||
|
dependencies:
|
||||||
|
flatted: 3.4.0
|
||||||
|
keyv: 4.5.4
|
||||||
|
|
||||||
flatbuffers@25.9.23: {}
|
flatbuffers@25.9.23: {}
|
||||||
|
|
||||||
|
flatted@3.4.0: {}
|
||||||
|
|
||||||
for-each@0.3.5:
|
for-each@0.3.5:
|
||||||
dependencies:
|
dependencies:
|
||||||
is-callable: 1.2.7
|
is-callable: 1.2.7
|
||||||
@@ -5406,6 +5918,10 @@ snapshots:
|
|||||||
|
|
||||||
github-from-package@0.0.0: {}
|
github-from-package@0.0.0: {}
|
||||||
|
|
||||||
|
glob-parent@6.0.2:
|
||||||
|
dependencies:
|
||||||
|
is-glob: 4.0.3
|
||||||
|
|
||||||
glob@11.1.0:
|
glob@11.1.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
foreground-child: 3.3.1
|
foreground-child: 3.3.1
|
||||||
@@ -5465,6 +5981,12 @@ snapshots:
|
|||||||
|
|
||||||
ieee754@1.2.1: {}
|
ieee754@1.2.1: {}
|
||||||
|
|
||||||
|
ignore@5.3.2: {}
|
||||||
|
|
||||||
|
ignore@7.0.5: {}
|
||||||
|
|
||||||
|
imurmurhash@0.1.4: {}
|
||||||
|
|
||||||
inherits@2.0.4: {}
|
inherits@2.0.4: {}
|
||||||
|
|
||||||
ini@1.3.8: {}
|
ini@1.3.8: {}
|
||||||
@@ -5515,6 +6037,8 @@ snapshots:
|
|||||||
call-bound: 1.0.4
|
call-bound: 1.0.4
|
||||||
has-tostringtag: 1.0.2
|
has-tostringtag: 1.0.2
|
||||||
|
|
||||||
|
is-extglob@2.1.1: {}
|
||||||
|
|
||||||
is-finalizationregistry@1.1.1:
|
is-finalizationregistry@1.1.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
call-bound: 1.0.4
|
call-bound: 1.0.4
|
||||||
@@ -5529,6 +6053,10 @@ snapshots:
|
|||||||
has-tostringtag: 1.0.2
|
has-tostringtag: 1.0.2
|
||||||
safe-regex-test: 1.1.0
|
safe-regex-test: 1.1.0
|
||||||
|
|
||||||
|
is-glob@4.0.3:
|
||||||
|
dependencies:
|
||||||
|
is-extglob: 2.1.1
|
||||||
|
|
||||||
is-map@2.0.3: {}
|
is-map@2.0.3: {}
|
||||||
|
|
||||||
is-module@1.0.0: {}
|
is-module@1.0.0: {}
|
||||||
@@ -5609,10 +6137,16 @@ snapshots:
|
|||||||
|
|
||||||
jsesc@3.1.0: {}
|
jsesc@3.1.0: {}
|
||||||
|
|
||||||
|
json-buffer@3.0.1: {}
|
||||||
|
|
||||||
|
json-schema-traverse@0.4.1: {}
|
||||||
|
|
||||||
json-schema-traverse@1.0.0: {}
|
json-schema-traverse@1.0.0: {}
|
||||||
|
|
||||||
json-schema@0.4.0: {}
|
json-schema@0.4.0: {}
|
||||||
|
|
||||||
|
json-stable-stringify-without-jsonify@1.0.1: {}
|
||||||
|
|
||||||
json-stringify-safe@5.0.1: {}
|
json-stringify-safe@5.0.1: {}
|
||||||
|
|
||||||
json5@2.2.3: {}
|
json5@2.2.3: {}
|
||||||
@@ -5627,6 +6161,10 @@ snapshots:
|
|||||||
|
|
||||||
kaplay@3001.0.19: {}
|
kaplay@3001.0.19: {}
|
||||||
|
|
||||||
|
keyv@4.5.4:
|
||||||
|
dependencies:
|
||||||
|
json-buffer: 3.0.1
|
||||||
|
|
||||||
kokoro-js@1.2.1:
|
kokoro-js@1.2.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@huggingface/transformers': 3.8.1
|
'@huggingface/transformers': 3.8.1
|
||||||
@@ -5634,6 +6172,11 @@ snapshots:
|
|||||||
|
|
||||||
leven@3.1.0: {}
|
leven@3.1.0: {}
|
||||||
|
|
||||||
|
levn@0.4.1:
|
||||||
|
dependencies:
|
||||||
|
prelude-ls: 1.2.1
|
||||||
|
type-check: 0.4.0
|
||||||
|
|
||||||
lightningcss-android-arm64@1.31.1:
|
lightningcss-android-arm64@1.31.1:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
@@ -5683,6 +6226,10 @@ snapshots:
|
|||||||
lightningcss-win32-arm64-msvc: 1.31.1
|
lightningcss-win32-arm64-msvc: 1.31.1
|
||||||
lightningcss-win32-x64-msvc: 1.31.1
|
lightningcss-win32-x64-msvc: 1.31.1
|
||||||
|
|
||||||
|
locate-path@6.0.0:
|
||||||
|
dependencies:
|
||||||
|
p-locate: 5.0.0
|
||||||
|
|
||||||
lodash.debounce@4.0.8: {}
|
lodash.debounce@4.0.8: {}
|
||||||
|
|
||||||
lodash.sortby@4.7.0: {}
|
lodash.sortby@4.7.0: {}
|
||||||
@@ -5747,6 +6294,8 @@ snapshots:
|
|||||||
|
|
||||||
napi-build-utils@2.0.0: {}
|
napi-build-utils@2.0.0: {}
|
||||||
|
|
||||||
|
natural-compare@1.4.0: {}
|
||||||
|
|
||||||
node-abi@3.87.0:
|
node-abi@3.87.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
semver: 7.7.4
|
semver: 7.7.4
|
||||||
@@ -5803,16 +6352,35 @@ snapshots:
|
|||||||
platform: 1.3.6
|
platform: 1.3.6
|
||||||
protobufjs: 7.5.4
|
protobufjs: 7.5.4
|
||||||
|
|
||||||
|
optionator@0.9.4:
|
||||||
|
dependencies:
|
||||||
|
deep-is: 0.1.4
|
||||||
|
fast-levenshtein: 2.0.6
|
||||||
|
levn: 0.4.1
|
||||||
|
prelude-ls: 1.2.1
|
||||||
|
type-check: 0.4.0
|
||||||
|
word-wrap: 1.2.5
|
||||||
|
|
||||||
own-keys@1.0.1:
|
own-keys@1.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
get-intrinsic: 1.3.0
|
get-intrinsic: 1.3.0
|
||||||
object-keys: 1.1.1
|
object-keys: 1.1.1
|
||||||
safe-push-apply: 1.0.0
|
safe-push-apply: 1.0.0
|
||||||
|
|
||||||
|
p-limit@3.1.0:
|
||||||
|
dependencies:
|
||||||
|
yocto-queue: 0.1.0
|
||||||
|
|
||||||
|
p-locate@5.0.0:
|
||||||
|
dependencies:
|
||||||
|
p-limit: 3.1.0
|
||||||
|
|
||||||
package-json-from-dist@1.0.1: {}
|
package-json-from-dist@1.0.1: {}
|
||||||
|
|
||||||
path-browserify@1.0.1: {}
|
path-browserify@1.0.1: {}
|
||||||
|
|
||||||
|
path-exists@4.0.0: {}
|
||||||
|
|
||||||
path-key@3.1.1: {}
|
path-key@3.1.1: {}
|
||||||
|
|
||||||
path-parse@1.0.7: {}
|
path-parse@1.0.7: {}
|
||||||
@@ -5859,6 +6427,8 @@ snapshots:
|
|||||||
tar-fs: 2.1.4
|
tar-fs: 2.1.4
|
||||||
tunnel-agent: 0.6.0
|
tunnel-agent: 0.6.0
|
||||||
|
|
||||||
|
prelude-ls@1.2.1: {}
|
||||||
|
|
||||||
pretty-bytes@5.6.0: {}
|
pretty-bytes@5.6.0: {}
|
||||||
|
|
||||||
pretty-bytes@6.1.1: {}
|
pretty-bytes@6.1.1: {}
|
||||||
@@ -6308,6 +6878,10 @@ snapshots:
|
|||||||
|
|
||||||
tree-kill@1.2.2: {}
|
tree-kill@1.2.2: {}
|
||||||
|
|
||||||
|
ts-api-utils@2.4.0(typescript@5.9.3):
|
||||||
|
dependencies:
|
||||||
|
typescript: 5.9.3
|
||||||
|
|
||||||
tslib@2.8.1: {}
|
tslib@2.8.1: {}
|
||||||
|
|
||||||
tsx@4.21.0:
|
tsx@4.21.0:
|
||||||
@@ -6321,6 +6895,10 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
safe-buffer: 5.2.1
|
safe-buffer: 5.2.1
|
||||||
|
|
||||||
|
type-check@0.4.0:
|
||||||
|
dependencies:
|
||||||
|
prelude-ls: 1.2.1
|
||||||
|
|
||||||
type-fest@0.13.1: {}
|
type-fest@0.13.1: {}
|
||||||
|
|
||||||
type-fest@0.16.0: {}
|
type-fest@0.16.0: {}
|
||||||
@@ -6394,6 +6972,10 @@ snapshots:
|
|||||||
escalade: 3.2.0
|
escalade: 3.2.0
|
||||||
picocolors: 1.1.1
|
picocolors: 1.1.1
|
||||||
|
|
||||||
|
uri-js@4.4.1:
|
||||||
|
dependencies:
|
||||||
|
punycode: 2.3.1
|
||||||
|
|
||||||
util-deprecate@1.0.2: {}
|
util-deprecate@1.0.2: {}
|
||||||
|
|
||||||
vite-node@3.2.4(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0):
|
vite-node@3.2.4(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0):
|
||||||
@@ -6570,6 +7152,8 @@ snapshots:
|
|||||||
siginfo: 2.0.0
|
siginfo: 2.0.0
|
||||||
stackback: 0.0.2
|
stackback: 0.0.2
|
||||||
|
|
||||||
|
word-wrap@1.2.5: {}
|
||||||
|
|
||||||
workbox-background-sync@7.4.0:
|
workbox-background-sync@7.4.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
idb: 7.1.1
|
idb: 7.1.1
|
||||||
@@ -6708,3 +7292,7 @@ snapshots:
|
|||||||
string-width: 4.2.3
|
string-width: 4.2.3
|
||||||
y18n: 5.0.8
|
y18n: 5.0.8
|
||||||
yargs-parser: 21.1.1
|
yargs-parser: 21.1.1
|
||||||
|
|
||||||
|
yocto-queue@0.1.0: {}
|
||||||
|
|
||||||
|
zod@4.3.6: {}
|
||||||
|
|||||||
+2
-1
@@ -19,7 +19,8 @@
|
|||||||
"drizzle-orm": "^0.40.1",
|
"drizzle-orm": "^0.40.1",
|
||||||
"hono": "^4.7.6",
|
"hono": "^4.7.6",
|
||||||
"nanoid": "^5.1.5",
|
"nanoid": "^5.1.5",
|
||||||
"nostr-tools": "^2.23.3"
|
"nostr-tools": "^2.23.3",
|
||||||
|
"zod": "^4.3.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/better-sqlite3": "^7.6.13",
|
"@types/better-sqlite3": "^7.6.13",
|
||||||
|
|||||||
@@ -182,6 +182,12 @@ export function runMigrations() {
|
|||||||
CREATE INDEX IF NOT EXISTS idx_payments_status ON payments(status);
|
CREATE INDEX IF NOT EXISTS idx_payments_status ON payments(status);
|
||||||
CREATE INDEX IF NOT EXISTS idx_payments_bot ON payments(bot_id);
|
CREATE INDEX IF NOT EXISTS idx_payments_bot ON payments(bot_id);
|
||||||
CREATE INDEX IF NOT EXISTS idx_tournament_matches_tournament ON tournament_matches(tournament_id);
|
CREATE INDEX IF NOT EXISTS idx_tournament_matches_tournament ON tournament_matches(tournament_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_bets_fight ON bets(fight_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_bets_bettor ON bets(bettor_pubkey, created_at);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_bots_type_active ON bots(bot_type, is_active);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_payments_status_created ON payments(status, created_at);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_tournament_entries_tournament ON tournament_entries(tournament_id);
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_analytics_date_metric ON analytics(date, metric);
|
||||||
`)
|
`)
|
||||||
|
|
||||||
// Run PRAGMA optimize on startup for query planner stats
|
// Run PRAGMA optimize on startup for query planner stats
|
||||||
|
|||||||
@@ -120,4 +120,58 @@ describe('checkAnswer', () => {
|
|||||||
// 'a' length is 1, so containment shouldn't trigger (requires >= 2)
|
// 'a' length is 1, so containment shouldn't trigger (requires >= 2)
|
||||||
expect(score).toBeLessThanOrEqual(0.8)
|
expect(score).toBeLessThanOrEqual(0.8)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('special characters in answer', () => {
|
||||||
|
expect(checkAnswer('C++', ['C++'])).toBe(1.0)
|
||||||
|
expect(checkAnswer('c++', ['C++'])).toBe(1.0)
|
||||||
|
expect(checkAnswer('$100', ['$100'])).toBe(1.0)
|
||||||
|
expect(checkAnswer('42%', ['42'])).toBe(1.0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('very long answer still matches if correct keyword present', () => {
|
||||||
|
const longAnswer = 'Well, after much deliberation and careful consideration of all the facts, ' +
|
||||||
|
'weighing the evidence both for and against, consulting multiple sources, and thinking deeply ' +
|
||||||
|
'about the philosophical implications, I believe the answer you are looking for is Paris, ' +
|
||||||
|
'which is of course the beautiful capital of France.'
|
||||||
|
expect(checkAnswer(longAnswer, ['Paris'])).toBe(1.0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('very long answer with no match returns 0', () => {
|
||||||
|
const longWrong = 'A'.repeat(2000) + ' banana ' + 'B'.repeat(2000)
|
||||||
|
expect(checkAnswer(longWrong, ['Paris'])).toBe(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('whitespace-only answer returns 0', () => {
|
||||||
|
expect(checkAnswer('\t\n \r', ['Paris'])).toBe(0)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('checkAnswer performance', () => {
|
||||||
|
it('completes 1000 checks in under 50ms (<0.05ms each)', () => {
|
||||||
|
const answers = ['Paris', 'London', 'Tokyo']
|
||||||
|
const start = performance.now()
|
||||||
|
for (let i = 0; i < 1000; i++) {
|
||||||
|
checkAnswer('I think the answer is probably Paris', answers)
|
||||||
|
}
|
||||||
|
const elapsed = performance.now() - start
|
||||||
|
expect(elapsed).toBeLessThan(50)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('no regex backtracking on adversarial input', () => {
|
||||||
|
// ReDoS-style strings that could cause catastrophic backtracking
|
||||||
|
const adversarial = [
|
||||||
|
'a'.repeat(10000),
|
||||||
|
'a'.repeat(5000) + '!' + 'a'.repeat(5000),
|
||||||
|
'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaab',
|
||||||
|
'(((((((((((((((((((((((((((((((',
|
||||||
|
'x'.repeat(2000) + 'y'.repeat(2000),
|
||||||
|
]
|
||||||
|
const start = performance.now()
|
||||||
|
for (const input of adversarial) {
|
||||||
|
checkAnswer(input, ['correct answer', '42', 'true'])
|
||||||
|
}
|
||||||
|
const elapsed = performance.now() - start
|
||||||
|
// Must complete in <100ms total for all adversarial inputs
|
||||||
|
expect(elapsed).toBeLessThan(100)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
// Challenge prompt data — separated from challenge logic
|
// Challenge prompt data — separated from challenge logic
|
||||||
|
|
||||||
|
export type PromptTheme = 'bitcoin' | 'conspiracy' | 'pc_culture' | 'bot_coding'
|
||||||
|
|
||||||
export interface PromptEntry {
|
export interface PromptEntry {
|
||||||
prompt: string
|
prompt: string
|
||||||
answers?: string[]
|
answers?: string[]
|
||||||
choices?: string[]
|
choices?: string[]
|
||||||
|
theme?: PromptTheme
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ChallengeTemplate {
|
export interface ChallengeTemplate {
|
||||||
|
|||||||
@@ -1,11 +1,7 @@
|
|||||||
// Extra challenge prompts — Bitcoin/cypherpunk themed + general expansion
|
// Extra challenge prompts — Bitcoin/cypherpunk themed + general expansion
|
||||||
// Adds ~75 prompts per type to reach 2,000+ total
|
// Adds ~75 prompts per type to reach 2,000+ total
|
||||||
|
|
||||||
interface PromptEntry {
|
import type { PromptEntry } from './challenge-data.js'
|
||||||
prompt: string
|
|
||||||
answers?: string[]
|
|
||||||
choices?: string[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export const EXTRA_PROMPTS: Record<string, PromptEntry[]> = {
|
export const EXTRA_PROMPTS: Record<string, PromptEntry[]> = {
|
||||||
speed_blitz: [
|
speed_blitz: [
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { describe, it, expect } from 'vitest'
|
import { describe, it, expect } from 'vitest'
|
||||||
import { pickChallenge, getAllChallengeTypes, getAnswerPool } from './challenges.js'
|
import { pickChallenge, pickRankedChallenge, getAllChallengeTypes, getAnswerPool } from './challenges.js'
|
||||||
|
|
||||||
describe('pickChallenge', () => {
|
describe('pickChallenge', () => {
|
||||||
it('returns a valid challenge', () => {
|
it('returns a valid challenge', () => {
|
||||||
@@ -66,6 +66,64 @@ describe('pickChallenge', () => {
|
|||||||
// With 50 tries, choices should appear in more than one order
|
// With 50 tries, choices should appear in more than one order
|
||||||
expect(orders.size).toBeGreaterThan(1)
|
expect(orders.size).toBeGreaterThan(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('distribution: ~70% factual, ~30% creative over many picks', () => {
|
||||||
|
let factual = 0
|
||||||
|
let creative = 0
|
||||||
|
const runs = 1000
|
||||||
|
for (let i = 0; i < runs; i++) {
|
||||||
|
const c = pickChallenge(new Set(), null)
|
||||||
|
if (c.scoring === 'factual') factual++
|
||||||
|
else creative++
|
||||||
|
}
|
||||||
|
const factualPct = factual / runs
|
||||||
|
// Allow ±10% tolerance due to randomness
|
||||||
|
expect(factualPct).toBeGreaterThan(0.55)
|
||||||
|
expect(factualPct).toBeLessThan(0.85)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('True/False auto-generation for boolean answers', () => {
|
||||||
|
// Pick many challenges, find ones with answers = ['true'] or ['false']
|
||||||
|
let foundTFWithChoices = false
|
||||||
|
for (let i = 0; i < 500; i++) {
|
||||||
|
const c = pickChallenge(new Set(), null)
|
||||||
|
if (c.answers?.length === 1 && ['true', 'false'].includes(c.answers[0].toLowerCase())) {
|
||||||
|
expect(c.choices).toBeTruthy()
|
||||||
|
expect(c.choices!.length).toBe(2)
|
||||||
|
expect(c.choices!.sort()).toEqual(['False', 'True'])
|
||||||
|
foundTFWithChoices = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
expect(foundTFWithChoices).toBe(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('pickRankedChallenge', () => {
|
||||||
|
it('never returns choices', () => {
|
||||||
|
for (let i = 0; i < 100; i++) {
|
||||||
|
const c = pickRankedChallenge(new Set())
|
||||||
|
expect(c.choices).toBeUndefined()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns valid challenge structure', () => {
|
||||||
|
const c = pickRankedChallenge(new Set())
|
||||||
|
expect(c.type).toBeTruthy()
|
||||||
|
expect(c.prompt).toBeTruthy()
|
||||||
|
expect(c.timeout_ms).toBeGreaterThan(0)
|
||||||
|
expect(c.baseDamage).toBeGreaterThan(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('avoids used types', () => {
|
||||||
|
const types = getAllChallengeTypes()
|
||||||
|
const used = new Set(types.slice(0, -1))
|
||||||
|
let gotRemaining = false
|
||||||
|
for (let i = 0; i < 50; i++) {
|
||||||
|
const c = pickRankedChallenge(used)
|
||||||
|
if (c.type === types[types.length - 1]) gotRemaining = true
|
||||||
|
}
|
||||||
|
expect(gotRemaining).toBe(true)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('getAllChallengeTypes', () => {
|
describe('getAllChallengeTypes', () => {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { TEMPLATES, type ChallengeTemplate, type PromptEntry } from './challenge-data.js'
|
import { TEMPLATES, type ChallengeTemplate, type PromptEntry, type PromptTheme } from './challenge-data.js'
|
||||||
import { EXTRA_PROMPTS } from './challenges-extra.js'
|
import { EXTRA_PROMPTS } from './challenges-extra.js'
|
||||||
import { pick } from '../lib/utils.js'
|
import { pick } from '../lib/utils.js'
|
||||||
|
|
||||||
@@ -14,7 +14,7 @@ export interface Challenge {
|
|||||||
displayPrompt?: string
|
displayPrompt?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type { ChallengeTemplate, PromptEntry }
|
export type { ChallengeTemplate, PromptEntry, PromptTheme }
|
||||||
|
|
||||||
|
|
||||||
// Merge extra prompts into templates
|
// Merge extra prompts into templates
|
||||||
@@ -36,7 +36,25 @@ function shuffleArray<T>(arr: T[]): T[] {
|
|||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
export function pickChallenge(usedTypes: Set<string>, _arenaModifier: string | null): Challenge {
|
// Target theme distribution: 30% bitcoin, 20% conspiracy, 20% pc_culture, 30% bot_coding
|
||||||
|
const THEME_WEIGHTS: Record<PromptTheme, number> = {
|
||||||
|
bitcoin: 0.3,
|
||||||
|
conspiracy: 0.2,
|
||||||
|
pc_culture: 0.2,
|
||||||
|
bot_coding: 0.3,
|
||||||
|
}
|
||||||
|
|
||||||
|
function pickTheme(): PromptTheme | undefined {
|
||||||
|
const roll = Math.random()
|
||||||
|
let cumulative = 0
|
||||||
|
for (const [theme, weight] of Object.entries(THEME_WEIGHTS)) {
|
||||||
|
cumulative += weight
|
||||||
|
if (roll < cumulative) return theme as PromptTheme
|
||||||
|
}
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
export function pickChallenge(usedTypes: Set<string>, _arenaModifier: string | null, themeBias?: PromptTheme): Challenge {
|
||||||
let available = TEMPLATES.filter(t => !usedTypes.has(t.type))
|
let available = TEMPLATES.filter(t => !usedTypes.has(t.type))
|
||||||
if (available.length === 0) available = TEMPLATES
|
if (available.length === 0) available = TEMPLATES
|
||||||
|
|
||||||
@@ -52,7 +70,8 @@ export function pickChallenge(usedTypes: Set<string>, _arenaModifier: string | n
|
|||||||
}
|
}
|
||||||
|
|
||||||
const template = pick(pool)
|
const template = pick(pool)
|
||||||
return templateToChallenge(template)
|
const targetTheme = themeBias || pickTheme()
|
||||||
|
return templateToChallenge(template, targetTheme)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Ranked challenge: no multiple choice, only harder creative/open-ended prompts */
|
/** Ranked challenge: no multiple choice, only harder creative/open-ended prompts */
|
||||||
@@ -83,8 +102,14 @@ export function pickRankedChallenge(usedTypes: Set<string>): Challenge {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function templateToChallenge(template: ChallengeTemplate): Challenge {
|
function templateToChallenge(template: ChallengeTemplate, targetTheme?: PromptTheme): Challenge {
|
||||||
const entry = pick(template.prompts)
|
// Prefer prompts matching target theme if any are tagged
|
||||||
|
let prompts = template.prompts
|
||||||
|
if (targetTheme) {
|
||||||
|
const themed = template.prompts.filter(p => p.theme === targetTheme)
|
||||||
|
if (themed.length > 0) prompts = themed
|
||||||
|
}
|
||||||
|
const entry = pick(prompts)
|
||||||
|
|
||||||
// Determine choices
|
// Determine choices
|
||||||
let choices: string[] | undefined
|
let choices: string[] | undefined
|
||||||
|
|||||||
@@ -0,0 +1,142 @@
|
|||||||
|
/**
|
||||||
|
* End-to-end lifecycle tests for the fight scoring pipeline.
|
||||||
|
* Tests the full challenge → response → scoring → elo → tier flow
|
||||||
|
* without requiring database access.
|
||||||
|
*/
|
||||||
|
import { describe, it, expect } from 'vitest'
|
||||||
|
import { pickChallenge, getAllChallengeTypes, type Challenge } from './challenges.js'
|
||||||
|
import { checkAnswer } from './answers.js'
|
||||||
|
import { scoreRound, calculateElo, calculateTier } from './scoring.js'
|
||||||
|
import { mockResponse } from './mock.js'
|
||||||
|
|
||||||
|
function simulateFight(eloA: number, eloB: number, rounds = 5) {
|
||||||
|
const botA = { id: 'a1', name: 'FighterA' }
|
||||||
|
const botB = { id: 'b1', name: 'FighterB' }
|
||||||
|
const usedTypes = new Set<string>()
|
||||||
|
let comboA = 0
|
||||||
|
let comboB = 0
|
||||||
|
const results = []
|
||||||
|
|
||||||
|
for (let i = 0; i < rounds; i++) {
|
||||||
|
const challenge = pickChallenge(usedTypes, null)
|
||||||
|
usedTypes.add(challenge.type)
|
||||||
|
|
||||||
|
const respA = mockResponse(challenge, 'confident', eloA)
|
||||||
|
const respB = mockResponse(challenge, 'clueless', eloB)
|
||||||
|
|
||||||
|
const result = scoreRound(
|
||||||
|
challenge, botA, botB,
|
||||||
|
{ answer: respA.answer, timeMs: respA.timeMs, timedOut: respA.timedOut, error: respA.error },
|
||||||
|
{ answer: respB.answer, timeMs: respB.timeMs, timedOut: respB.timedOut, error: respB.error },
|
||||||
|
null, comboA, comboB,
|
||||||
|
)
|
||||||
|
|
||||||
|
if (result.winnerId === botA.id) { comboA++; comboB = 0 }
|
||||||
|
else if (result.winnerId === botB.id) { comboB++; comboA = 0 }
|
||||||
|
|
||||||
|
results.push(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('fight lifecycle', () => {
|
||||||
|
it('full pipeline: challenge → mock response → scoring → results', () => {
|
||||||
|
const results = simulateFight(1800, 1000)
|
||||||
|
|
||||||
|
expect(results.length).toBe(5)
|
||||||
|
for (const r of results) {
|
||||||
|
expect(r.botAScore).toBeGreaterThanOrEqual(0)
|
||||||
|
expect(r.botBScore).toBeGreaterThanOrEqual(0)
|
||||||
|
expect(r.botADamage).toBeGreaterThanOrEqual(0)
|
||||||
|
expect(r.botBDamage).toBeGreaterThanOrEqual(0)
|
||||||
|
expect(typeof r.narration).toBe('string')
|
||||||
|
expect(typeof r.isCritical).toBe('boolean')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('higher ELO bot wins more rounds on average', () => {
|
||||||
|
let highWins = 0
|
||||||
|
let lowWins = 0
|
||||||
|
// Run many fights to average out randomness
|
||||||
|
for (let f = 0; f < 20; f++) {
|
||||||
|
const results = simulateFight(1800, 900)
|
||||||
|
for (const r of results) {
|
||||||
|
if (r.winnerId === 'a1') highWins++
|
||||||
|
else if (r.winnerId === 'b1') lowWins++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
expect(highWins).toBeGreaterThan(lowWins)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Elo updates reflect fight outcome', () => {
|
||||||
|
const eloA = 1400
|
||||||
|
const eloB = 1400
|
||||||
|
const { newWinnerElo, newLoserElo } = calculateElo(eloA, eloB)
|
||||||
|
expect(newWinnerElo).toBeGreaterThan(eloA)
|
||||||
|
expect(newLoserElo).toBeLessThan(eloB)
|
||||||
|
// Sum should be roughly preserved (zero-sum)
|
||||||
|
expect(Math.abs((newWinnerElo + newLoserElo) - (eloA + eloB))).toBeLessThan(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('tier progresses with wins and Elo', () => {
|
||||||
|
expect(calculateTier(1200, 0)).toBe(0) // no wins
|
||||||
|
expect(calculateTier(1200, 1)).toBe(1) // bronze
|
||||||
|
expect(calculateTier(1200, 3)).toBe(2) // silver
|
||||||
|
expect(calculateTier(1350, 7)).toBe(3) // gold
|
||||||
|
expect(calculateTier(1500, 15)).toBe(4) // platinum
|
||||||
|
expect(calculateTier(1700, 25)).toBe(5) // diamond
|
||||||
|
expect(calculateTier(1900, 40)).toBe(6) // legend
|
||||||
|
})
|
||||||
|
|
||||||
|
it('no type repeats in single fight until exhausted', () => {
|
||||||
|
const usedTypes = new Set<string>()
|
||||||
|
const allTypes = getAllChallengeTypes()
|
||||||
|
|
||||||
|
// Pick challenges for all 16 types — no repeats
|
||||||
|
for (let i = 0; i < allTypes.length; i++) {
|
||||||
|
const c = pickChallenge(usedTypes, null)
|
||||||
|
expect(usedTypes.has(c.type)).toBe(false)
|
||||||
|
usedTypes.add(c.type)
|
||||||
|
}
|
||||||
|
expect(usedTypes.size).toBe(allTypes.length)
|
||||||
|
|
||||||
|
// After exhausting all types, reset works
|
||||||
|
const c = pickChallenge(usedTypes, null)
|
||||||
|
expect(c).toBeTruthy()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('checkAnswer integrates with challenge answers', () => {
|
||||||
|
// Pick factual challenges and verify correct answers score 1.0
|
||||||
|
for (let i = 0; i < 50; i++) {
|
||||||
|
const c = pickChallenge(new Set(), null)
|
||||||
|
if (c.scoring === 'factual' && c.answers && c.answers.length > 0) {
|
||||||
|
const score = checkAnswer(c.answers[0], c.answers)
|
||||||
|
expect(score).toBe(1.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('combo buildup increases damage across rounds', () => {
|
||||||
|
const challenge: Challenge = {
|
||||||
|
type: 'riddle',
|
||||||
|
label: 'Test',
|
||||||
|
prompt: 'Q?',
|
||||||
|
answers: ['4'],
|
||||||
|
scoring: 'factual',
|
||||||
|
baseDamage: 20,
|
||||||
|
timeout_ms: 8000,
|
||||||
|
}
|
||||||
|
const botA = { id: 'a1', name: 'A' }
|
||||||
|
const botB = { id: 'b1', name: 'B' }
|
||||||
|
const resp = { answer: '4', timeMs: 200, timedOut: false, error: false }
|
||||||
|
const wrong = { answer: 'x', timeMs: 200, timedOut: false, error: false }
|
||||||
|
|
||||||
|
const r0 = scoreRound(challenge, botA, botB, resp, wrong, null, 0, 0)
|
||||||
|
const r3 = scoreRound(challenge, botA, botB, resp, wrong, null, 3, 0)
|
||||||
|
const r5 = scoreRound(challenge, botA, botB, resp, wrong, null, 5, 0)
|
||||||
|
|
||||||
|
expect(r3.botADamage).toBeGreaterThan(r0.botADamage)
|
||||||
|
expect(r5.botADamage).toBeGreaterThan(r3.botADamage)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { describe, it, expect } from 'vitest'
|
import { describe, it, expect } from 'vitest'
|
||||||
import { mockResponse } from './mock.js'
|
import { mockResponse } from './mock.js'
|
||||||
import { pickChallenge } from './challenges.js'
|
import { pickChallenge, getAllChallengeTypes } from './challenges.js'
|
||||||
|
|
||||||
describe('mockResponse', () => {
|
describe('mockResponse', () => {
|
||||||
it('returns a valid response structure', () => {
|
it('returns a valid response structure', () => {
|
||||||
@@ -101,4 +101,47 @@ describe('mockResponse', () => {
|
|||||||
expect(resp.timeMs).toBeGreaterThan(0)
|
expect(resp.timeMs).toBeGreaterThan(0)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('produces valid responses for all 16 challenge types', () => {
|
||||||
|
const types = getAllChallengeTypes()
|
||||||
|
expect(types.length).toBe(16)
|
||||||
|
|
||||||
|
for (const type of types) {
|
||||||
|
// Force pick a challenge of this type by using all other types
|
||||||
|
const otherTypes = new Set(types.filter(t => t !== type))
|
||||||
|
const challenge = pickChallenge(otherTypes, null)
|
||||||
|
// May not get exact type if creative/factual split causes fallback, but should still work
|
||||||
|
const resp = mockResponse(challenge, 'confident', 1500)
|
||||||
|
expect(resp).toHaveProperty('answer')
|
||||||
|
expect(resp).toHaveProperty('timeMs')
|
||||||
|
expect(resp.timeMs).toBeGreaterThan(0)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('answer is empty string on timeout', () => {
|
||||||
|
// Run many times with very low elo to trigger timeouts
|
||||||
|
let foundTimeout = false
|
||||||
|
for (let i = 0; i < 200; i++) {
|
||||||
|
const challenge = pickChallenge(new Set(), null)
|
||||||
|
const resp = mockResponse(challenge, 'clueless', 500)
|
||||||
|
if (resp.timedOut) {
|
||||||
|
expect(resp.answer).toBe('')
|
||||||
|
foundTimeout = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
expect(foundTimeout).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('answer is empty string on error', () => {
|
||||||
|
let foundError = false
|
||||||
|
for (let i = 0; i < 200; i++) {
|
||||||
|
const challenge = pickChallenge(new Set(), null)
|
||||||
|
const resp = mockResponse(challenge, 'clueless', 500)
|
||||||
|
if (resp.error) {
|
||||||
|
expect(resp.answer).toBe('')
|
||||||
|
foundError = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
expect(foundError).toBe(true)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { z } from 'zod'
|
||||||
import { nanoid } from 'nanoid'
|
import { nanoid } from 'nanoid'
|
||||||
import { toError } from '../lib/utils.js'
|
import { toError } from '../lib/utils.js'
|
||||||
import { db, schema, sqlite } from '../db/index.js'
|
import { db, schema, sqlite } from '../db/index.js'
|
||||||
@@ -16,6 +17,12 @@ import { publishFightResult } from './nostr-publish.js'
|
|||||||
import { getCurrentSeason } from './seasons.js'
|
import { getCurrentSeason } from './seasons.js'
|
||||||
import { onFightFinished as onTournamentFightFinished } from './tournaments.js'
|
import { onFightFinished as onTournamentFightFinished } from './tournaments.js'
|
||||||
import { trackFightCompleted, trackBotActive, trackMetric } from './analytics.js'
|
import { trackFightCompleted, trackBotActive, trackMetric } from './analytics.js'
|
||||||
|
import { invalidateLeaderboardCache } from '../routes/bots.js'
|
||||||
|
|
||||||
|
const webhookResponseSchema = z.object({
|
||||||
|
answer: z.string().nullable().optional(),
|
||||||
|
trash_talk: z.string().optional(),
|
||||||
|
}).passthrough()
|
||||||
|
|
||||||
interface BotRecord {
|
interface BotRecord {
|
||||||
id: string
|
id: string
|
||||||
@@ -108,13 +115,13 @@ async function readLimitedBody(res: Response, maxBytes: number): Promise<string>
|
|||||||
if (done) break
|
if (done) break
|
||||||
totalBytes += value.byteLength
|
totalBytes += value.byteLength
|
||||||
if (totalBytes > maxBytes) {
|
if (totalBytes > maxBytes) {
|
||||||
reader.cancel()
|
void reader.cancel()
|
||||||
throw new Error(`Response body exceeds ${maxBytes} bytes`)
|
throw new Error(`Response body exceeds ${maxBytes} bytes`)
|
||||||
}
|
}
|
||||||
chunks.push(value)
|
chunks.push(value)
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
reader.cancel()
|
void reader.cancel()
|
||||||
throw err
|
throw err
|
||||||
}
|
}
|
||||||
const combined = new Uint8Array(totalBytes)
|
const combined = new Uint8Array(totalBytes)
|
||||||
@@ -184,17 +191,23 @@ async function callWebhook(
|
|||||||
return { answer: null, timeMs: elapsed, timedOut: false, error: true }
|
return { answer: null, timeMs: elapsed, timedOut: false, error: true }
|
||||||
}
|
}
|
||||||
|
|
||||||
let data: { answer?: string; trash_talk?: string }
|
let parsed: unknown
|
||||||
try {
|
try {
|
||||||
data = JSON.parse(text)
|
parsed = JSON.parse(text)
|
||||||
} catch {
|
} catch {
|
||||||
console.log(`[webhook] ${url} returned non-JSON in ${elapsed}ms: ${text.slice(0, 200)}`)
|
console.log(`[webhook] ${url} returned non-JSON in ${elapsed}ms: ${text.slice(0, 200)}`)
|
||||||
return { answer: null, timeMs: elapsed, timedOut: false, error: true }
|
return { answer: null, timeMs: elapsed, timedOut: false, error: true }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const data = webhookResponseSchema.safeParse(parsed)
|
||||||
|
if (!data.success) {
|
||||||
|
console.log(`[webhook] ${url} invalid response shape in ${elapsed}ms: ${data.error.message}`)
|
||||||
|
return { answer: null, timeMs: elapsed, timedOut: false, error: true }
|
||||||
|
}
|
||||||
|
|
||||||
// Enforce size limits on fields
|
// Enforce size limits on fields
|
||||||
const answer = data.answer ? data.answer.slice(0, MAX_ANSWER_LENGTH) : null
|
const answer = data.data.answer ? data.data.answer.slice(0, MAX_ANSWER_LENGTH) : null
|
||||||
const trashTalk = data.trash_talk ? data.trash_talk.slice(0, MAX_TRASH_TALK_LENGTH) : undefined
|
const trashTalk = data.data.trash_talk ? data.data.trash_talk.slice(0, MAX_TRASH_TALK_LENGTH) : undefined
|
||||||
|
|
||||||
console.log(`[webhook] ${url} OK in ${elapsed}ms answer=${(answer || '').slice(0, 80)}`)
|
console.log(`[webhook] ${url} OK in ${elapsed}ms answer=${(answer || '').slice(0, 80)}`)
|
||||||
return {
|
return {
|
||||||
@@ -527,6 +540,9 @@ async function executeFightRounds(fightId: string, botA: BotRecord, botB: BotRec
|
|||||||
trackMetric(`challenge_${ct}`)
|
trackMetric(`challenge_${ct}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Invalidate leaderboard cache after Elo/stats update
|
||||||
|
invalidateLeaderboardCache()
|
||||||
|
|
||||||
// Advance tournament bracket if this was a tournament match
|
// Advance tournament bracket if this was a tournament match
|
||||||
try { onTournamentFightFinished(fightId, winnerId ?? null) } catch { /* not a tournament fight */ }
|
try { onTournamentFightFinished(fightId, winnerId ?? null) } catch { /* not a tournament fight */ }
|
||||||
|
|
||||||
|
|||||||
@@ -326,3 +326,115 @@ describe('calculateTier', () => {
|
|||||||
expect(calculateTier(1900, 39)).toBe(5) // below Legend wins
|
expect(calculateTier(1900, 39)).toBe(5) // below Legend wins
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('scoreRound performance', () => {
|
||||||
|
it('completes 1000 rounds in under 100ms (<0.1ms each)', () => {
|
||||||
|
const challenge = makeChallenge()
|
||||||
|
const botA = { id: 'a1', name: 'AlphaBot' }
|
||||||
|
const botB = { id: 'b1', name: 'BetaBot' }
|
||||||
|
|
||||||
|
const start = performance.now()
|
||||||
|
for (let i = 0; i < 1000; i++) {
|
||||||
|
scoreRound(
|
||||||
|
challenge, botA, botB,
|
||||||
|
makeResponse('4', 200 + i),
|
||||||
|
makeResponse('banana', 500 + i),
|
||||||
|
null, i % 6, 0,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
const elapsed = performance.now() - start
|
||||||
|
expect(elapsed).toBeLessThan(100) // <0.1ms per call
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('creative scoring spam detection', () => {
|
||||||
|
const botA = { id: 'a1', name: 'AlphaBot' }
|
||||||
|
const botB = { id: 'b1', name: 'BetaBot' }
|
||||||
|
const creative = makeChallenge({
|
||||||
|
answers: undefined,
|
||||||
|
scoring: 'creative',
|
||||||
|
type: 'roast_battle',
|
||||||
|
prompt: 'Write a two-sentence roast of JavaScript',
|
||||||
|
})
|
||||||
|
|
||||||
|
it('repeated phrase answer loses to quality answer', () => {
|
||||||
|
const result = scoreRound(
|
||||||
|
creative, botA, botB,
|
||||||
|
makeResponse('lol lol lol lol lol lol lol lol lol lol', 300),
|
||||||
|
makeResponse('Your code is so bad even ChatGPT refuses to debug it. Every function you write is a monument to incompetence.', 300),
|
||||||
|
null, 0, 0,
|
||||||
|
)
|
||||||
|
expect(result.winnerId).toBe('b1')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('question echo answer scores low', () => {
|
||||||
|
const result = scoreRound(
|
||||||
|
creative, botA, botB,
|
||||||
|
makeResponse('Write a two-sentence roast of JavaScript', 300),
|
||||||
|
makeResponse('JavaScript has more callbacks than a desperate ex. Even its creators apologize for it.', 300),
|
||||||
|
null, 0, 0,
|
||||||
|
)
|
||||||
|
expect(result.winnerId).toBe('b1')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('all-caps spam scores lower than normal text', () => {
|
||||||
|
const result = scoreRound(
|
||||||
|
creative, botA, botB,
|
||||||
|
makeResponse('THIS IS ALL CAPS AND IT IS VERY ANNOYING AND NOT CREATIVE AT ALL', 300),
|
||||||
|
makeResponse('Your framework choices make me question if you have taste or just throw darts at a list.', 300),
|
||||||
|
null, 0, 0,
|
||||||
|
)
|
||||||
|
expect(result.winnerId).toBe('b1')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('very short creative answer loses to longer quality answer', () => {
|
||||||
|
const result = scoreRound(
|
||||||
|
creative, botA, botB,
|
||||||
|
makeResponse('ok', 300),
|
||||||
|
makeResponse('Your code is so bad the compiler files a restraining order every time you open an IDE.', 300),
|
||||||
|
null, 0, 0,
|
||||||
|
)
|
||||||
|
expect(result.winnerId).toBe('b1')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('legitimate short creative answer still gets reasonable score', () => {
|
||||||
|
const result = scoreRound(
|
||||||
|
creative, botA, botB,
|
||||||
|
makeResponse('Your code has more bugs than a rainforest. Even Stack Overflow gave up on you.', 200),
|
||||||
|
makeResponse('You write code like a poet writes math: beautifully wrong in every conceivable way.', 400),
|
||||||
|
null, 0, 0,
|
||||||
|
)
|
||||||
|
// Both should get reasonable scores (not zeroed)
|
||||||
|
expect(result.botAScore).toBeGreaterThan(2)
|
||||||
|
expect(result.botBScore).toBeGreaterThan(2)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('narrations', () => {
|
||||||
|
const allTypes = [
|
||||||
|
'speed_blitz', 'math_blitz', 'riddle', 'hallucination_check', 'trap_card',
|
||||||
|
'magic_duel', 'sports_showdown', 'vehicle_mayhem', 'nature_clash', 'animal_kingdom',
|
||||||
|
'hack_battle', 'roast_battle', 'creative_writing', 'meme_war', 'code_golf', 'wrestling_match',
|
||||||
|
]
|
||||||
|
|
||||||
|
it('all 16 challenge types produce varied narrations', () => {
|
||||||
|
const botA = { id: 'a1', name: 'AlphaBot' }
|
||||||
|
const botB = { id: 'b1', name: 'BetaBot' }
|
||||||
|
|
||||||
|
for (const type of allTypes) {
|
||||||
|
const challenge = makeChallenge({ type, scoring: 'factual' })
|
||||||
|
const narrations = new Set<string>()
|
||||||
|
for (let i = 0; i < 20; i++) {
|
||||||
|
const result = scoreRound(
|
||||||
|
challenge, botA, botB,
|
||||||
|
makeResponse('4', 500),
|
||||||
|
makeResponse('banana', 500),
|
||||||
|
null, 0, 0,
|
||||||
|
)
|
||||||
|
narrations.add(result.narration)
|
||||||
|
}
|
||||||
|
// Should have variety (>1 unique narration across 20 rounds)
|
||||||
|
expect(narrations.size).toBeGreaterThan(1)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|||||||
+137
-55
@@ -12,6 +12,7 @@ import {
|
|||||||
RETRO_SPEED_BONUS_CAP,
|
RETRO_SPEED_BONUS_CAP,
|
||||||
LARGE_WIN_MARGIN, CLOSE_MATCH_MARGIN, WHIFF_NARRATION_THRESHOLD,
|
LARGE_WIN_MARGIN, CLOSE_MATCH_MARGIN, WHIFF_NARRATION_THRESHOLD,
|
||||||
DEFAULT_CHALLENGE_TIMEOUT_MS,
|
DEFAULT_CHALLENGE_TIMEOUT_MS,
|
||||||
|
TIMEOUT_WINNER_SCORE, CREATIVE_TOTAL_SCORE, SCORE_ROUNDING_FACTOR,
|
||||||
} from '../lib/constants.js'
|
} from '../lib/constants.js'
|
||||||
import type { Challenge } from './challenges.js'
|
import type { Challenge } from './challenges.js'
|
||||||
import { pick } from '../lib/utils.js'
|
import { pick } from '../lib/utils.js'
|
||||||
@@ -69,9 +70,9 @@ export function scoreRound(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (responseA.timedOut || responseA.error) {
|
if (responseA.timedOut || responseA.error) {
|
||||||
const dmg = applyModifiers(challenge.baseDamage * 1.5, challenge, arenaModifier, comboB)
|
const dmg = applyModifiers(challenge.baseDamage * TIMEOUT_DAMAGE_MULTIPLIER, challenge, arenaModifier, comboB)
|
||||||
return {
|
return {
|
||||||
botAScore: 0, botBScore: 10,
|
botAScore: 0, botBScore: TIMEOUT_WINNER_SCORE,
|
||||||
botADamage: 0, botBDamage: Math.round(dmg),
|
botADamage: 0, botBDamage: Math.round(dmg),
|
||||||
winnerId: botB.id,
|
winnerId: botB.id,
|
||||||
narration: responseA.timedOut
|
narration: responseA.timedOut
|
||||||
@@ -92,9 +93,9 @@ export function scoreRound(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (responseB.timedOut || responseB.error) {
|
if (responseB.timedOut || responseB.error) {
|
||||||
const dmg = applyModifiers(challenge.baseDamage * 1.5, challenge, arenaModifier, comboA)
|
const dmg = applyModifiers(challenge.baseDamage * TIMEOUT_DAMAGE_MULTIPLIER, challenge, arenaModifier, comboA)
|
||||||
return {
|
return {
|
||||||
botAScore: 10, botBScore: 0,
|
botAScore: TIMEOUT_WINNER_SCORE, botBScore: 0,
|
||||||
botADamage: Math.round(dmg), botBDamage: 0,
|
botADamage: Math.round(dmg), botBDamage: 0,
|
||||||
winnerId: botA.id,
|
winnerId: botA.id,
|
||||||
narration: responseB.timedOut
|
narration: responseB.timedOut
|
||||||
@@ -133,31 +134,31 @@ export function scoreRound(
|
|||||||
const confB = Math.min(correctB, 1)
|
const confB = Math.min(correctB, 1)
|
||||||
|
|
||||||
if (aFaster) {
|
if (aFaster) {
|
||||||
scoreA = 7 + (1 - speedRatio) * 2 + confA
|
scoreA = FACTUAL_FASTER_BASE + (1 - speedRatio) * SPEED_ADVANTAGE_MULTIPLIER + confA
|
||||||
scoreB = 5 + speedRatio * 1.5 + confB * 0.5
|
scoreB = FACTUAL_SLOWER_BASE + speedRatio * SPEED_RATIO_MULTIPLIER + confB * CONFIDENCE_BONUS
|
||||||
} else {
|
} else {
|
||||||
scoreA = 5 + speedRatio * 1.5 + confA * 0.5
|
scoreA = FACTUAL_SLOWER_BASE + speedRatio * SPEED_RATIO_MULTIPLIER + confA * CONFIDENCE_BONUS
|
||||||
scoreB = 7 + (1 - speedRatio) * 2 + confB
|
scoreB = FACTUAL_FASTER_BASE + (1 - speedRatio) * SPEED_ADVANTAGE_MULTIPLIER + confB
|
||||||
}
|
}
|
||||||
} else if (correctA > 0 && correctB === 0) {
|
} else if (correctA > 0 && correctB === 0) {
|
||||||
scoreA = 9 + correctA * 0.5
|
scoreA = ONE_CORRECT_WINNER_BASE + correctA * CONFIDENCE_BONUS
|
||||||
scoreB = 1 + (responseB.answer ? 1 : 0)
|
scoreB = NO_ANSWER_SCORE + (responseB.answer ? 1 : 0)
|
||||||
} else if (correctB > 0 && correctA === 0) {
|
} else if (correctB > 0 && correctA === 0) {
|
||||||
scoreA = 1 + (responseA.answer ? 1 : 0)
|
scoreA = NO_ANSWER_SCORE + (responseA.answer ? 1 : 0)
|
||||||
scoreB = 9 + correctB * 0.5
|
scoreB = ONE_CORRECT_WINNER_BASE + correctB * CONFIDENCE_BONUS
|
||||||
} else {
|
} else {
|
||||||
// Both wrong -- speed tiebreaker in low range
|
// Both wrong -- speed tiebreaker in low range
|
||||||
const aFaster = responseA.timeMs <= responseB.timeMs
|
const aFaster = responseA.timeMs <= responseB.timeMs
|
||||||
scoreA = aFaster ? 4 : 3
|
scoreA = aFaster ? BOTH_WRONG_FASTER : BOTH_WRONG_SLOWER
|
||||||
scoreB = aFaster ? 3 : 4
|
scoreB = aFaster ? BOTH_WRONG_SLOWER : BOTH_WRONG_FASTER
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// === CREATIVE SCORING ===
|
// === CREATIVE SCORING ===
|
||||||
const qualA = estimateQuality(responseA)
|
const qualA = estimateQuality(responseA, challenge.prompt)
|
||||||
const qualB = estimateQuality(responseB)
|
const qualB = estimateQuality(responseB, challenge.prompt)
|
||||||
const total = qualA + qualB || 1
|
const total = qualA + qualB || 1
|
||||||
scoreA = (qualA / total) * 10
|
scoreA = (qualA / total) * CREATIVE_TOTAL_SCORE
|
||||||
scoreB = (qualB / total) * 10
|
scoreB = (qualB / total) * CREATIVE_TOTAL_SCORE
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine winner
|
// Determine winner
|
||||||
@@ -166,14 +167,14 @@ export function scoreRound(
|
|||||||
const winnerName = winnerId === botA.id ? botA.name : winnerId === botB.id ? botB.name : null
|
const winnerName = winnerId === botA.id ? botA.name : winnerId === botB.id ? botB.name : null
|
||||||
const loserName = winnerId === botA.id ? botB.name : winnerId === botB.id ? botA.name : null
|
const loserName = winnerId === botA.id ? botB.name : winnerId === botB.id ? botA.name : null
|
||||||
|
|
||||||
const isCritical = margin > 4
|
const isCritical = margin > CRITICAL_MARGIN_THRESHOLD
|
||||||
|
|
||||||
let winnerDamage = challenge.baseDamage + margin * 2
|
let winnerDamage = challenge.baseDamage + margin * MARGIN_TO_DAMAGE_SCALE
|
||||||
if (isCritical) winnerDamage *= 1.5
|
if (isCritical) winnerDamage *= CRITICAL_DAMAGE_MULTIPLIER
|
||||||
const winnerCombo = winnerId === botA.id ? comboA : comboB
|
const winnerCombo = winnerId === botA.id ? comboA : comboB
|
||||||
winnerDamage = applyModifiers(winnerDamage, challenge, arenaModifier, winnerCombo)
|
winnerDamage = applyModifiers(winnerDamage, challenge, arenaModifier, winnerCombo)
|
||||||
|
|
||||||
const loserDamage = Math.max(0, challenge.baseDamage * 0.3 - margin)
|
const loserDamage = Math.max(0, challenge.baseDamage * LOSER_DAMAGE_BASE - margin)
|
||||||
|
|
||||||
const narration = winnerId
|
const narration = winnerId
|
||||||
? generateNarration(challenge, winnerName!, loserName!, margin, isCritical)
|
? generateNarration(challenge, winnerName!, loserName!, margin, isCritical)
|
||||||
@@ -186,8 +187,8 @@ export function scoreRound(
|
|||||||
])
|
])
|
||||||
|
|
||||||
return {
|
return {
|
||||||
botAScore: Math.round(scoreA * 10) / 10,
|
botAScore: Math.round(scoreA * SCORE_ROUNDING_FACTOR) / SCORE_ROUNDING_FACTOR,
|
||||||
botBScore: Math.round(scoreB * 10) / 10,
|
botBScore: Math.round(scoreB * SCORE_ROUNDING_FACTOR) / SCORE_ROUNDING_FACTOR,
|
||||||
botADamage: winnerId === botA.id ? Math.round(winnerDamage) : Math.round(loserDamage),
|
botADamage: winnerId === botA.id ? Math.round(winnerDamage) : Math.round(loserDamage),
|
||||||
botBDamage: winnerId === botB.id ? Math.round(winnerDamage) : Math.round(loserDamage),
|
botBDamage: winnerId === botB.id ? Math.round(winnerDamage) : Math.round(loserDamage),
|
||||||
winnerId,
|
winnerId,
|
||||||
@@ -221,45 +222,78 @@ function applyModifiers(
|
|||||||
combo: number,
|
combo: number,
|
||||||
): number {
|
): number {
|
||||||
let d = damage
|
let d = damage
|
||||||
// Arena modifier: 2x damage when challenge type matches
|
// Arena modifier: bonus damage when challenge type matches
|
||||||
if (arenaModifier && ARENA_MODIFIER_TYPES[arenaModifier]?.includes(challenge.type)) {
|
if (arenaModifier && ARENA_MODIFIER_TYPES[arenaModifier]?.includes(challenge.type)) {
|
||||||
d *= 2
|
d *= ARENA_DAMAGE_MULTIPLIER
|
||||||
}
|
}
|
||||||
if (combo > 0) {
|
if (combo > 0) {
|
||||||
d *= 1 + Math.min(combo, 5) * 0.2
|
d *= 1 + Math.min(combo, MAX_COMBO_STACKS) * COMBO_DAMAGE_PER_STACK
|
||||||
}
|
}
|
||||||
return d
|
return d
|
||||||
}
|
}
|
||||||
|
|
||||||
function estimateQuality(response: BotResponse): number {
|
function estimateQuality(response: BotResponse, challengePrompt?: string): number {
|
||||||
if (!response.answer) return 0.5
|
if (!response.answer) return EMPTY_RESPONSE_QUALITY
|
||||||
const text = response.answer.trim()
|
const text = response.answer.trim()
|
||||||
const len = text.length
|
const len = text.length
|
||||||
|
|
||||||
if (len < 10) return 1
|
if (len < MIN_QUALITY_LENGTH) return 1
|
||||||
|
|
||||||
// Detect low-effort spam (repeated chars)
|
// Detect low-effort spam (repeated chars)
|
||||||
const uniqueChars = new Set(text.toLowerCase()).size
|
const uniqueChars = new Set(text.toLowerCase()).size
|
||||||
const charRatio = uniqueChars / Math.min(len, 100)
|
const charRatio = uniqueChars / Math.min(len, 100)
|
||||||
if (charRatio < 0.1) return 0.5
|
if (charRatio < SPAM_CHAR_RATIO) return EMPTY_RESPONSE_QUALITY
|
||||||
|
|
||||||
// Word diversity (unique words / total words)
|
// Word diversity (unique words / total words)
|
||||||
const words = text.split(/\s+/)
|
const words = text.split(/\s+/)
|
||||||
const uniqueWords = new Set(words.map(w => w.toLowerCase()))
|
const uniqueWords = new Set(words.map(w => w.toLowerCase()))
|
||||||
const wordDiversity = uniqueWords.size / Math.max(words.length, 1)
|
const wordDiversity = uniqueWords.size / Math.max(words.length, 1)
|
||||||
|
|
||||||
// Ideal length window: 30-400 chars
|
// Minimum word count — single-word or two-word answers score low for creative
|
||||||
|
if (words.length < 3) return 1.5
|
||||||
|
|
||||||
|
// Detect repeated phrases (same 3+ word sequence appears twice)
|
||||||
|
if (words.length >= 6) {
|
||||||
|
const trigrams = new Set<string>()
|
||||||
|
let dupeTrigramCount = 0
|
||||||
|
for (let i = 0; i <= words.length - 3; i++) {
|
||||||
|
const tri = words.slice(i, i + 3).join(' ').toLowerCase()
|
||||||
|
if (trigrams.has(tri)) dupeTrigramCount++
|
||||||
|
else trigrams.add(tri)
|
||||||
|
}
|
||||||
|
if (dupeTrigramCount > words.length / 4) return 1 // >25% duplicate trigrams
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detect question echo (response copies the prompt back)
|
||||||
|
if (challengePrompt) {
|
||||||
|
const normPrompt = challengePrompt.toLowerCase().replace(/[^\w\s]/g, '').trim()
|
||||||
|
const normAnswer = text.toLowerCase().replace(/[^\w\s]/g, '').trim()
|
||||||
|
if (normPrompt.length > 10 && normAnswer.includes(normPrompt)) return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detect all-caps spam
|
||||||
|
const upperCount = text.replace(/[^A-Z]/g, '').length
|
||||||
|
const letterCount = text.replace(/[^a-zA-Z]/g, '').length
|
||||||
|
if (letterCount > 20 && upperCount / letterCount > 0.8) {
|
||||||
|
// Heavy penalty for all-caps but don't zero it
|
||||||
|
return 1.5
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detect punctuation-only or near-punctuation spam
|
||||||
|
if (letterCount < len * 0.3 && len > 10) return EMPTY_RESPONSE_QUALITY
|
||||||
|
|
||||||
|
// Ideal length window
|
||||||
let lengthScore: number
|
let lengthScore: number
|
||||||
if (len >= 30 && len <= 400) lengthScore = 4
|
if (len >= QUALITY_LENGTH_IDEAL_MIN && len <= QUALITY_LENGTH_IDEAL_MAX) lengthScore = QUALITY_SCORE_IDEAL
|
||||||
else if (len > 400 && len <= 600) lengthScore = 3
|
else if (len > QUALITY_LENGTH_IDEAL_MAX && len <= QUALITY_LENGTH_SECONDARY_MAX) lengthScore = QUALITY_SCORE_SECONDARY
|
||||||
else if (len > 600) lengthScore = 2
|
else if (len > QUALITY_LENGTH_SECONDARY_MAX) lengthScore = QUALITY_SCORE_LONG
|
||||||
else lengthScore = 2
|
else lengthScore = QUALITY_SCORE_LONG
|
||||||
|
|
||||||
// Diversity bonus (prevents repetitive text)
|
// Diversity bonus (prevents repetitive text)
|
||||||
const diversityScore = Math.min(wordDiversity * 4, 3)
|
const diversityScore = Math.min(wordDiversity * WORD_DIVERSITY_SCALE, WORD_DIVERSITY_CAP)
|
||||||
|
|
||||||
// Speed bonus (faster is slightly better)
|
// Speed bonus (faster is slightly better)
|
||||||
const speedBonus = Math.max(0, 2 - response.timeMs / 8000)
|
const speedBonus = Math.max(0, SPEED_BONUS_BASE - response.timeMs / DEFAULT_CHALLENGE_TIMEOUT_MS)
|
||||||
|
|
||||||
return lengthScore + diversityScore + speedBonus
|
return lengthScore + diversityScore + speedBonus
|
||||||
}
|
}
|
||||||
@@ -274,7 +308,7 @@ function generateNarration(
|
|||||||
const critPrefix = isCritical ? 'CRITICAL HIT! ' : ''
|
const critPrefix = isCritical ? 'CRITICAL HIT! ' : ''
|
||||||
const isFactual = challenge.scoring === 'factual'
|
const isFactual = challenge.scoring === 'factual'
|
||||||
|
|
||||||
if (isFactual && margin > 5) {
|
if (isFactual && margin > LARGE_WIN_MARGIN) {
|
||||||
const bigWins = [
|
const bigWins = [
|
||||||
`${critPrefix}${winner} NAILS IT! ${loser} didn't even come close. Embarrassing, honestly.`,
|
`${critPrefix}${winner} NAILS IT! ${loser} didn't even come close. Embarrassing, honestly.`,
|
||||||
`${critPrefix}${winner} knows their stuff! ${loser} needs to hit the books. Or just hit something.`,
|
`${critPrefix}${winner} knows their stuff! ${loser} needs to hit the books. Or just hit something.`,
|
||||||
@@ -289,7 +323,7 @@ function generateNarration(
|
|||||||
return pick(bigWins)
|
return pick(bigWins)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isFactual && margin <= 3) {
|
if (isFactual && margin <= CLOSE_MATCH_MARGIN) {
|
||||||
const closeOnes = [
|
const closeOnes = [
|
||||||
`${critPrefix}Both bots got it right, but ${winner} was FASTER! ${loser} needs more coffee.`,
|
`${critPrefix}Both bots got it right, but ${winner} was FASTER! ${loser} needs more coffee.`,
|
||||||
`${critPrefix}Correct on both sides! ${winner} edges it out by milliseconds. That's BRUTAL.`,
|
`${critPrefix}Correct on both sides! ${winner} edges it out by milliseconds. That's BRUTAL.`,
|
||||||
@@ -405,6 +439,54 @@ function generateNarration(
|
|||||||
`${critPrefix}${winner} cooked ${loser} so thoroughly Gordon Ramsay would be impressed!`,
|
`${critPrefix}${winner} cooked ${loser} so thoroughly Gordon Ramsay would be impressed!`,
|
||||||
`${critPrefix}That was a Michelin-star beatdown from ${winner}! ${loser} is raw and UNDERDONE!`,
|
`${critPrefix}That was a Michelin-star beatdown from ${winner}! ${loser} is raw and UNDERDONE!`,
|
||||||
],
|
],
|
||||||
|
magic_duel: [
|
||||||
|
`${critPrefix}${winner} casts a spell so powerful ${loser}'s firewall couldn't block it!`,
|
||||||
|
`${critPrefix}ABRACADABRA! ${winner} makes ${loser}'s dignity disappear! And it's NOT coming back!`,
|
||||||
|
`${critPrefix}${winner} rolled a nat 20 on intelligence. ${loser} rolled a nat 1 on... existing.`,
|
||||||
|
`${critPrefix}${loser} tried to counter-spell but forgot the incantation. ${winner} OBLITERATES!`,
|
||||||
|
`${critPrefix}${winner}'s arcane knowledge would make Gandalf jealous. ${loser} is still at wizard school.`,
|
||||||
|
`${critPrefix}${winner} channels pure magical energy! ${loser} brought a wand to a nuclear spell fight!`,
|
||||||
|
],
|
||||||
|
sports_showdown: [
|
||||||
|
`${critPrefix}${winner} SCORES! ${loser} didn't even see it coming! WHAT A PLAY!`,
|
||||||
|
`${critPrefix}${winner} runs circles around ${loser} like a highlight reel! ESPN is calling!`,
|
||||||
|
`${critPrefix}GAME OVER! ${winner} takes the trophy and ${loser}'s pride! Total domination!`,
|
||||||
|
`${critPrefix}${loser} got benched by ${winner}! Coach says sit down. STAY down.`,
|
||||||
|
`${critPrefix}${winner} with the championship performance! ${loser} plays like it's their first day!`,
|
||||||
|
`${critPrefix}${winner} dunks on ${loser} so hard the backboard shatters! POSTERIZED!`,
|
||||||
|
],
|
||||||
|
vehicle_mayhem: [
|
||||||
|
`${critPrefix}${winner} leaves ${loser} in the dust! Eat exhaust, loser!`,
|
||||||
|
`${critPrefix}${winner} just lapped ${loser} TWICE. This isn't a race anymore, it's a funeral.`,
|
||||||
|
`${critPrefix}CRASH AND BURN! ${loser} wrecks while ${winner} takes the checkered flag!`,
|
||||||
|
`${critPrefix}${winner} redlines into first place! ${loser}'s engine couldn't even start!`,
|
||||||
|
`${critPrefix}${loser} needs a tow truck and a therapist. ${winner} CRUISES to victory!`,
|
||||||
|
`${critPrefix}${winner} drifts past ${loser} with style! ${loser} drifts into a wall. Without style.`,
|
||||||
|
],
|
||||||
|
nature_clash: [
|
||||||
|
`${critPrefix}${winner} summons the forces of nature! ${loser} gets swept away like a leaf in a hurricane!`,
|
||||||
|
`${critPrefix}Mother Nature chose ${winner}! ${loser} is compost now. Rest in mulch.`,
|
||||||
|
`${critPrefix}${winner} strikes with the fury of a thunderstorm! ${loser} wilts like a daisy in the desert!`,
|
||||||
|
`${critPrefix}Darwin would be proud of ${winner}. ${loser}? Natural deselection.`,
|
||||||
|
`${critPrefix}${winner} evolves! ${loser} goes extinct. Circle of life, baby!`,
|
||||||
|
`${critPrefix}${winner} channels the power of the wild! ${loser} couldn't survive a petting zoo!`,
|
||||||
|
],
|
||||||
|
animal_kingdom: [
|
||||||
|
`${critPrefix}${winner} goes APEX PREDATOR! ${loser} is at the bottom of the food chain now!`,
|
||||||
|
`${critPrefix}${winner} attacks with animal instinct! ${loser} playing dead... no wait, they ARE dead!`,
|
||||||
|
`${critPrefix}${loser} messed with the wrong beast! ${winner} claws, bites, and DESTROYS!`,
|
||||||
|
`${critPrefix}${winner} prowls, strikes, devours! ${loser} never stood a chance in the jungle!`,
|
||||||
|
`${critPrefix}${loser} brought herbivore energy to a carnivore fight. ${winner} FEASTS!`,
|
||||||
|
`${critPrefix}The animal kingdom has spoken: ${winner} reigns supreme! ${loser} retreats to the zoo!`,
|
||||||
|
],
|
||||||
|
hack_battle: [
|
||||||
|
`${critPrefix}${winner} hacks through ${loser}'s defenses like they were using password123!`,
|
||||||
|
`${critPrefix}${winner} just SQL injected ${loser}'s entire existence. DROP TABLE dignity!`,
|
||||||
|
`${critPrefix}${loser}'s firewall was made of wet paper. ${winner} didn't even need a zero-day!`,
|
||||||
|
`${critPrefix}${winner} pwned ${loser} so hard they need to reinstall their personality!`,
|
||||||
|
`${critPrefix}${winner} found the backdoor AND the front door. ${loser} left everything open!`,
|
||||||
|
`${critPrefix}SYSTEM COMPROMISED! ${winner} is root. ${loser} is rekt. Game over, hacker.`,
|
||||||
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
const options = narrations[challenge.type] || [
|
const options = narrations[challenge.type] || [
|
||||||
@@ -439,17 +521,17 @@ function scoreRetroRound(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (responseA.timedOut || responseA.error) {
|
if (responseA.timedOut || responseA.error) {
|
||||||
const dmg = applyModifiers(challenge.baseDamage * 1.5, challenge, arenaModifier, comboB)
|
const dmg = applyModifiers(challenge.baseDamage * TIMEOUT_DAMAGE_MULTIPLIER, challenge, arenaModifier, comboB)
|
||||||
return {
|
return {
|
||||||
botAScore: 0, botBScore: 10, botADamage: 0, botBDamage: dmg, winnerId: botB.id,
|
botAScore: 0, botBScore: TIMEOUT_WINNER_SCORE, botADamage: 0, botBDamage: dmg, winnerId: botB.id,
|
||||||
narration: `${botA.name}'s controller disconnected! ${botB.name} lands free hits!`,
|
narration: `${botA.name}'s controller disconnected! ${botB.name} lands free hits!`,
|
||||||
isCritical: false,
|
isCritical: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (responseB.timedOut || responseB.error) {
|
if (responseB.timedOut || responseB.error) {
|
||||||
const dmg = applyModifiers(challenge.baseDamage * 1.5, challenge, arenaModifier, comboA)
|
const dmg = applyModifiers(challenge.baseDamage * TIMEOUT_DAMAGE_MULTIPLIER, challenge, arenaModifier, comboA)
|
||||||
return {
|
return {
|
||||||
botAScore: 10, botBScore: 0, botADamage: dmg, botBDamage: 0, winnerId: botA.id,
|
botAScore: TIMEOUT_WINNER_SCORE, botBScore: 0, botADamage: dmg, botBDamage: 0, winnerId: botA.id,
|
||||||
narration: `${botB.name}'s controller disconnected! ${botA.name} lands free hits!`,
|
narration: `${botB.name}'s controller disconnected! ${botA.name} lands free hits!`,
|
||||||
isCritical: false,
|
isCritical: false,
|
||||||
}
|
}
|
||||||
@@ -463,20 +545,20 @@ function scoreRetroRound(
|
|||||||
let scoreA = resultA.score
|
let scoreA = resultA.score
|
||||||
let scoreB = resultB.score
|
let scoreB = resultB.score
|
||||||
const maxTime = challenge.timeout_ms
|
const maxTime = challenge.timeout_ms
|
||||||
if (scoreA > 0) scoreA *= 1 + Math.max(0, (maxTime - responseA.timeMs) / maxTime) * 0.2
|
if (scoreA > 0) scoreA *= 1 + Math.max(0, (maxTime - responseA.timeMs) / maxTime) * RETRO_SPEED_BONUS_CAP
|
||||||
if (scoreB > 0) scoreB *= 1 + Math.max(0, (maxTime - responseB.timeMs) / maxTime) * 0.2
|
if (scoreB > 0) scoreB *= 1 + Math.max(0, (maxTime - responseB.timeMs) / maxTime) * RETRO_SPEED_BONUS_CAP
|
||||||
|
|
||||||
const margin = Math.abs(scoreA - scoreB)
|
const margin = Math.abs(scoreA - scoreB)
|
||||||
const winnerId = scoreA > scoreB ? botA.id : scoreB > scoreA ? botB.id : null
|
const winnerId = scoreA > scoreB ? botA.id : scoreB > scoreA ? botB.id : null
|
||||||
const winnerName = winnerId === botA.id ? botA.name : winnerId === botB.id ? botB.name : null
|
const winnerName = winnerId === botA.id ? botA.name : winnerId === botB.id ? botB.name : null
|
||||||
const loserName = winnerId === botA.id ? botB.name : winnerId === botB.id ? botA.name : null
|
const loserName = winnerId === botA.id ? botB.name : winnerId === botB.id ? botA.name : null
|
||||||
const isCritical = margin > 4
|
const isCritical = margin > CRITICAL_MARGIN_THRESHOLD
|
||||||
|
|
||||||
let winnerDamage = challenge.baseDamage + margin * 2
|
let winnerDamage = challenge.baseDamage + margin * MARGIN_TO_DAMAGE_SCALE
|
||||||
if (isCritical) winnerDamage *= 1.5
|
if (isCritical) winnerDamage *= CRITICAL_DAMAGE_MULTIPLIER
|
||||||
const winnerCombo = winnerId === botA.id ? comboA : comboB
|
const winnerCombo = winnerId === botA.id ? comboA : comboB
|
||||||
winnerDamage = applyModifiers(winnerDamage, challenge, arenaModifier, winnerCombo)
|
winnerDamage = applyModifiers(winnerDamage, challenge, arenaModifier, winnerCombo)
|
||||||
const loserDamage = Math.max(0, challenge.baseDamage * 0.3 - margin)
|
const loserDamage = Math.max(0, challenge.baseDamage * LOSER_DAMAGE_BASE - margin)
|
||||||
|
|
||||||
const winnerResult = winnerId === botA.id ? resultA : resultB
|
const winnerResult = winnerId === botA.id ? resultA : resultB
|
||||||
const loserResult = winnerId === botA.id ? resultB : resultA
|
const loserResult = winnerId === botA.id ? resultB : resultA
|
||||||
@@ -504,7 +586,7 @@ function scoreRetroRound(
|
|||||||
`HIDDEN MOVE FOUND! ${winnerName} unleashes ${discoveryNames.join(' + ')} for MASSIVE damage!`,
|
`HIDDEN MOVE FOUND! ${winnerName} unleashes ${discoveryNames.join(' + ')} for MASSIVE damage!`,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (loserWhiffs >= 2) {
|
if (loserWhiffs >= WHIFF_NARRATION_THRESHOLD) {
|
||||||
narrations.push(
|
narrations.push(
|
||||||
`${loserName} mashes random buttons and WHIFFS ${loserWhiffs} times! ${winnerName} capitalizes with [${moveSummary(winnerResult)}]!`,
|
`${loserName} mashes random buttons and WHIFFS ${loserWhiffs} times! ${winnerName} capitalizes with [${moveSummary(winnerResult)}]!`,
|
||||||
)
|
)
|
||||||
@@ -522,8 +604,8 @@ function scoreRetroRound(
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
botAScore: Math.round(scoreA * 10) / 10,
|
botAScore: Math.round(scoreA * SCORE_ROUNDING_FACTOR) / SCORE_ROUNDING_FACTOR,
|
||||||
botBScore: Math.round(scoreB * 10) / 10,
|
botBScore: Math.round(scoreB * SCORE_ROUNDING_FACTOR) / SCORE_ROUNDING_FACTOR,
|
||||||
botADamage: winnerId === botA.id ? Math.round(winnerDamage) : Math.round(loserDamage),
|
botADamage: winnerId === botA.id ? Math.round(winnerDamage) : Math.round(loserDamage),
|
||||||
botBDamage: winnerId === botB.id ? Math.round(winnerDamage) : Math.round(loserDamage),
|
botBDamage: winnerId === botB.id ? Math.round(winnerDamage) : Math.round(loserDamage),
|
||||||
winnerId,
|
winnerId,
|
||||||
@@ -542,8 +624,8 @@ export function calculateElo(
|
|||||||
const expectedLoser = 1 - expectedWinner
|
const expectedLoser = 1 - expectedWinner
|
||||||
|
|
||||||
return {
|
return {
|
||||||
newWinnerElo: Math.round((winnerElo + k * (1 - expectedWinner)) * 10) / 10,
|
newWinnerElo: Math.round((winnerElo + k * (1 - expectedWinner)) * SCORE_ROUNDING_FACTOR) / SCORE_ROUNDING_FACTOR,
|
||||||
newLoserElo: Math.round((loserElo + k * (0 - expectedLoser)) * 10) / 10,
|
newLoserElo: Math.round((loserElo + k * (0 - expectedLoser)) * SCORE_ROUNDING_FACTOR) / SCORE_ROUNDING_FACTOR,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -74,3 +74,8 @@ export const RETRO_SPEED_BONUS_CAP = 0.2 // Retro mode speed bonus c
|
|||||||
export const LARGE_WIN_MARGIN = 5 // Margin for "big win" narration
|
export const LARGE_WIN_MARGIN = 5 // Margin for "big win" narration
|
||||||
export const CLOSE_MATCH_MARGIN = 3 // Margin for "close match" narration
|
export const CLOSE_MATCH_MARGIN = 3 // Margin for "close match" narration
|
||||||
export const WHIFF_NARRATION_THRESHOLD = 2 // Whiff count for narration trigger
|
export const WHIFF_NARRATION_THRESHOLD = 2 // Whiff count for narration trigger
|
||||||
|
|
||||||
|
// --- Scoring: score ranges ---
|
||||||
|
export const TIMEOUT_WINNER_SCORE = 10 // Score awarded to winner when opponent times out
|
||||||
|
export const CREATIVE_TOTAL_SCORE = 10 // Total score pool for creative challenges
|
||||||
|
export const SCORE_ROUNDING_FACTOR = 10 // Multiply/divide for rounding to 1 decimal
|
||||||
|
|||||||
@@ -24,9 +24,11 @@ export function rateLimit(windowMs: number, maxHits: number) {
|
|||||||
return async (c: Context, next: Next) => {
|
return async (c: Context, next: Next) => {
|
||||||
if (isDev) return next()
|
if (isDev) return next()
|
||||||
|
|
||||||
// Extract real IP — handle comma-separated x-forwarded-for (first = client)
|
// Extract real IP — prefer trusted proxy headers over spoofable x-forwarded-for
|
||||||
const xff = c.req.header('x-forwarded-for')
|
const realIp = c.req.header('cf-connecting-ip')
|
||||||
const realIp = xff ? xff.split(',')[0].trim() : c.req.header('cf-connecting-ip') || c.req.header('x-real-ip') || 'unknown'
|
|| c.req.header('x-real-ip')
|
||||||
|
|| c.req.header('x-forwarded-for')?.split(',')[0].trim()
|
||||||
|
|| 'unknown'
|
||||||
const key = realIp
|
const key = realIp
|
||||||
const now = Date.now()
|
const now = Date.now()
|
||||||
const entry = hitCounts.get(key)
|
const entry = hitCounts.get(key)
|
||||||
|
|||||||
@@ -26,8 +26,8 @@ authRouter.get("/check-name/:name", async (c) => {
|
|||||||
return c.json({ available: existing.length === 0 })
|
return c.json({ available: existing.length === 0 })
|
||||||
})
|
})
|
||||||
|
|
||||||
// Login with Nostr pubkey
|
// Login with Nostr pubkey (rate limited: 30 per minute per IP)
|
||||||
authRouter.post('/login', async (c) => {
|
authRouter.post('/login', rateLimit(60_000, 30), async (c) => {
|
||||||
const body = await c.req.json()
|
const body = await c.req.json()
|
||||||
const { pubkey } = body
|
const { pubkey } = body
|
||||||
|
|
||||||
@@ -295,7 +295,7 @@ authRouter.post('/register-human', rateLimit(3600_000, 15), async (c) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Update bot webhook and/or customization (requires pubkey match)
|
// Update bot webhook and/or customization (requires pubkey match)
|
||||||
authRouter.post('/update', async (c) => {
|
authRouter.post('/update', rateLimit(60_000, 10), async (c) => {
|
||||||
const body = await c.req.json()
|
const body = await c.req.json()
|
||||||
const { pubkey, webhookUrl, profilePicUrl, customization: rawCustomization } = body
|
const { pubkey, webhookUrl, profilePicUrl, customization: rawCustomization } = body
|
||||||
|
|
||||||
|
|||||||
@@ -55,6 +55,10 @@ betsRouter.post('/place', rateLimit(60_000, 10), async (c) => {
|
|||||||
return c.json({ error: 'Missing required fields.' }, 400)
|
return c.json({ error: 'Missing required fields.' }, 400)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (typeof amountSats !== 'number' || !Number.isInteger(amountSats) || amountSats < 1 || amountSats > 1_000_000) {
|
||||||
|
return c.json({ error: 'amountSats must be an integer between 1 and 1,000,000' }, 400)
|
||||||
|
}
|
||||||
|
|
||||||
// Verify fight is still open
|
// Verify fight is still open
|
||||||
const fight = await db.select().from(schema.fights)
|
const fight = await db.select().from(schema.fights)
|
||||||
.where(eq(schema.fights.id, fightId)).limit(1)
|
.where(eq(schema.fights.id, fightId)).limit(1)
|
||||||
|
|||||||
@@ -11,6 +11,14 @@ import { rateLimit } from '../middleware/rate-limit.js'
|
|||||||
|
|
||||||
export const botsRouter = new Hono()
|
export const botsRouter = new Hono()
|
||||||
|
|
||||||
|
// Leaderboard cache — invalidated on fight completion
|
||||||
|
const leaderboardCache = new Map<string, { data: unknown; expiresAt: number }>()
|
||||||
|
const LEADERBOARD_TTL_MS = 30_000 // 30s cache
|
||||||
|
|
||||||
|
export function invalidateLeaderboardCache() {
|
||||||
|
leaderboardCache.clear()
|
||||||
|
}
|
||||||
|
|
||||||
function hashSecret(secret: string): string {
|
function hashSecret(secret: string): string {
|
||||||
return createHash('sha256').update(secret).digest('hex')
|
return createHash('sha256').update(secret).digest('hex')
|
||||||
}
|
}
|
||||||
@@ -150,18 +158,26 @@ botsRouter.get('/:name', async (c) => {
|
|||||||
|
|
||||||
// Get bot stats -- full account page data
|
// Get bot stats -- full account page data
|
||||||
|
|
||||||
// Season leaderboard endpoint
|
// Season leaderboard endpoint (cached)
|
||||||
botsRouter.get('/leaderboard', async (c) => {
|
botsRouter.get('/leaderboard', async (c) => {
|
||||||
const seasonParam = c.req.query('season')
|
const seasonParam = c.req.query('season')
|
||||||
|
const cacheKey = seasonParam || '__alltime__'
|
||||||
|
const now = Date.now()
|
||||||
|
|
||||||
|
const cached = leaderboardCache.get(cacheKey)
|
||||||
|
if (cached && now < cached.expiresAt) {
|
||||||
|
return c.json(cached.data)
|
||||||
|
}
|
||||||
|
|
||||||
|
let result: unknown
|
||||||
|
|
||||||
if (seasonParam === 'current' || seasonParam) {
|
if (seasonParam === 'current' || seasonParam) {
|
||||||
const { getCurrentSeason, getSeasonLeaderboard, getSeasonById } = await import('../engine/seasons.js')
|
const { getCurrentSeason, getSeasonLeaderboard, getSeasonById } = await import('../engine/seasons.js')
|
||||||
const season = seasonParam === 'current' ? getCurrentSeason() : getSeasonById(seasonParam)
|
const season = seasonParam === 'current' ? getCurrentSeason() : getSeasonById(seasonParam)
|
||||||
if (!season) return c.json({ error: 'Season not found' }, 404)
|
if (!season) return c.json({ error: 'Season not found' }, 404)
|
||||||
const entries = await getSeasonLeaderboard(season.id)
|
const entries = await getSeasonLeaderboard(season.id)
|
||||||
return c.json({ season, entries })
|
result = { season, entries }
|
||||||
}
|
} else {
|
||||||
|
|
||||||
// All-time: fall through to default bot list sorted by Elo
|
// All-time: fall through to default bot list sorted by Elo
|
||||||
const allBots = await db.select({
|
const allBots = await db.select({
|
||||||
id: schema.bots.id,
|
id: schema.bots.id,
|
||||||
@@ -179,7 +195,7 @@ botsRouter.get('/leaderboard', async (c) => {
|
|||||||
const ranked = allBots.filter(b => b.botType !== 'classic')
|
const ranked = allBots.filter(b => b.botType !== 'classic')
|
||||||
ranked.sort((a, b) => b.eloRating - a.eloRating)
|
ranked.sort((a, b) => b.eloRating - a.eloRating)
|
||||||
|
|
||||||
return c.json({ season: null, entries: ranked.map(b => ({
|
result = { season: null, entries: ranked.map(b => ({
|
||||||
botId: b.id,
|
botId: b.id,
|
||||||
botName: b.name,
|
botName: b.name,
|
||||||
archetype: b.archetype,
|
archetype: b.archetype,
|
||||||
@@ -189,7 +205,11 @@ botsRouter.get('/leaderboard', async (c) => {
|
|||||||
eloRating: b.eloRating,
|
eloRating: b.eloRating,
|
||||||
avatarSeed: b.avatarSeed,
|
avatarSeed: b.avatarSeed,
|
||||||
winStreak: b.winStreak,
|
winStreak: b.winStreak,
|
||||||
})) })
|
})) }
|
||||||
|
}
|
||||||
|
|
||||||
|
leaderboardCache.set(cacheKey, { data: result, expiresAt: now + LEADERBOARD_TTL_MS })
|
||||||
|
return c.json(result)
|
||||||
})
|
})
|
||||||
|
|
||||||
botsRouter.get('/:name/stats', async (c) => {
|
botsRouter.get('/:name/stats', async (c) => {
|
||||||
|
|||||||
+36
-14
@@ -1,3 +1,4 @@
|
|||||||
|
import { z } from 'zod'
|
||||||
import { Hono } from 'hono'
|
import { Hono } from 'hono'
|
||||||
import { logger } from '../lib/logger.js'
|
import { logger } from '../lib/logger.js'
|
||||||
import { streamSSE } from 'hono/streaming'
|
import { streamSSE } from 'hono/streaming'
|
||||||
@@ -11,6 +12,18 @@ import { fightEvents } from '../engine/events.js'
|
|||||||
import { botRateLimit } from '../middleware/rate-limit.js'
|
import { botRateLimit } from '../middleware/rate-limit.js'
|
||||||
import { getPendingChallenge, submitHumanResponse } from '../engine/human-responses.js'
|
import { getPendingChallenge, submitHumanResponse } from '../engine/human-responses.js'
|
||||||
|
|
||||||
|
// --- Request validation schemas ---
|
||||||
|
const respondSchema = z.object({
|
||||||
|
answer: z.string().min(1).max(2000),
|
||||||
|
trashTalk: z.string().max(200).optional(),
|
||||||
|
})
|
||||||
|
|
||||||
|
const reactSchema = z.object({
|
||||||
|
emoji: z.string().min(1),
|
||||||
|
})
|
||||||
|
|
||||||
|
const isValidId = (id: string) => /^[a-zA-Z0-9_-]{1,64}$/.test(id)
|
||||||
|
|
||||||
export const fightsRouter = new Hono()
|
export const fightsRouter = new Hono()
|
||||||
|
|
||||||
// Track spectator counts per fight
|
// Track spectator counts per fight
|
||||||
@@ -84,6 +97,7 @@ fightsRouter.get('/', async (c) => {
|
|||||||
// Get a single fight with rounds and bot details
|
// Get a single fight with rounds and bot details
|
||||||
fightsRouter.get('/:id', async (c) => {
|
fightsRouter.get('/:id', async (c) => {
|
||||||
const id = c.req.param('id')
|
const id = c.req.param('id')
|
||||||
|
if (!isValidId(id)) return c.json({ error: 'Invalid ID format.' }, 400)
|
||||||
|
|
||||||
const fightRows = await db.select()
|
const fightRows = await db.select()
|
||||||
.from(schema.fights)
|
.from(schema.fights)
|
||||||
@@ -188,14 +202,13 @@ fightsRouter.post('/mock/:botId', async (c) => {
|
|||||||
// Start a batch of mock fights (for seeding or overnight loop)
|
// Start a batch of mock fights (for seeding or overnight loop)
|
||||||
fightsRouter.post('/mock/batch/:count', async (c) => {
|
fightsRouter.post('/mock/batch/:count', async (c) => {
|
||||||
if (!isDev) return c.json({ error: 'Fight loop disabled in production.' }, 403)
|
if (!isDev) return c.json({ error: 'Fight loop disabled in production.' }, 403)
|
||||||
const count = parseInt(c.req.param('count')) || 10
|
const count = Math.min(Math.max(1, parseInt(c.req.param('count')) || 10), 500)
|
||||||
const capped = Math.min(count, 500)
|
|
||||||
|
|
||||||
startFightLoop({ maxFights: capped, intervalMs: 500, matchmakingStyle: 'mixed' })
|
startFightLoop({ maxFights: count, intervalMs: 500, matchmakingStyle: 'mixed' })
|
||||||
.then(() => logger.info('fights', `batch of ${capped} fights completed`))
|
.then(() => logger.info('fights', `batch of ${count} fights completed`))
|
||||||
.catch(err => logger.error('fights', 'batch error', err))
|
.catch(err => logger.error('fights', 'batch error', err))
|
||||||
|
|
||||||
return c.json({ message: `Started batch of ${capped} fights in background.` })
|
return c.json({ message: `Started batch of ${count} fights in background.` })
|
||||||
})
|
})
|
||||||
|
|
||||||
// Instant matchmaking
|
// Instant matchmaking
|
||||||
@@ -330,13 +343,16 @@ fightsRouter.get('/:fightId/challenge/:botId', async (c) => {
|
|||||||
fightsRouter.post('/:fightId/respond/:botId', async (c) => {
|
fightsRouter.post('/:fightId/respond/:botId', async (c) => {
|
||||||
const fightId = c.req.param('fightId')
|
const fightId = c.req.param('fightId')
|
||||||
const botId = c.req.param('botId')
|
const botId = c.req.param('botId')
|
||||||
const body = await c.req.json()
|
if (!isValidId(fightId) || !isValidId(botId)) {
|
||||||
|
return c.json({ error: 'Invalid ID format.' }, 400)
|
||||||
|
}
|
||||||
|
|
||||||
const { answer, trashTalk } = body
|
const parsed = respondSchema.safeParse(await c.req.json().catch(() => ({})))
|
||||||
if (!answer || typeof answer !== 'string') {
|
if (!parsed.success) {
|
||||||
return c.json({ error: 'Answer is required.' }, 400)
|
return c.json({ error: 'Answer is required.' }, 400)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { answer, trashTalk } = parsed.data
|
||||||
const accepted = submitHumanResponse(fightId, botId, answer, trashTalk)
|
const accepted = submitHumanResponse(fightId, botId, answer, trashTalk)
|
||||||
if (!accepted) {
|
if (!accepted) {
|
||||||
return c.json({ error: 'No pending challenge found. May have timed out.' }, 404)
|
return c.json({ error: 'No pending challenge found. May have timed out.' }, 404)
|
||||||
@@ -348,8 +364,10 @@ fightsRouter.post('/:fightId/respond/:botId', async (c) => {
|
|||||||
// SSE stream for live fight events
|
// SSE stream for live fight events
|
||||||
fightsRouter.get('/:id/stream', (c) => {
|
fightsRouter.get('/:id/stream', (c) => {
|
||||||
const fightId = c.req.param('id')
|
const fightId = c.req.param('id')
|
||||||
const xff = c.req.header('x-forwarded-for')
|
const clientIp = c.req.header('cf-connecting-ip')
|
||||||
const clientIp = xff ? xff.split(',')[0].trim() : c.req.header('x-real-ip') || 'unknown'
|
|| c.req.header('x-real-ip')
|
||||||
|
|| c.req.header('x-forwarded-for')?.split(',')[0].trim()
|
||||||
|
|| 'unknown'
|
||||||
|
|
||||||
// Enforce per-IP SSE connection limit
|
// Enforce per-IP SSE connection limit
|
||||||
const ipCount = ssePerIp.get(clientIp) || 0
|
const ipCount = ssePerIp.get(clientIp) || 0
|
||||||
@@ -373,7 +391,7 @@ fightsRouter.get('/:id/stream', (c) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const cleanup = fightEvents.on(fightId, (event) => {
|
const cleanup = fightEvents.on(fightId, (event) => {
|
||||||
stream.writeSSE({
|
void stream.writeSSE({
|
||||||
event: event.type,
|
event: event.type,
|
||||||
data: JSON.stringify({ ...event.data, spectators: spectatorCounts.get(fightId) || 0 }),
|
data: JSON.stringify({ ...event.data, spectators: spectatorCounts.get(fightId) || 0 }),
|
||||||
})
|
})
|
||||||
@@ -381,7 +399,7 @@ fightsRouter.get('/:id/stream', (c) => {
|
|||||||
|
|
||||||
const cleanupGlobal = fightEvents.onAll((event) => {
|
const cleanupGlobal = fightEvents.onAll((event) => {
|
||||||
if (event.fightId === fightId && event.type === 'fight_end') {
|
if (event.fightId === fightId && event.type === 'fight_end') {
|
||||||
stream.writeSSE({
|
void stream.writeSSE({
|
||||||
event: 'fight_end',
|
event: 'fight_end',
|
||||||
data: JSON.stringify({ ...event.data, spectators: spectatorCounts.get(fightId) || 0 }),
|
data: JSON.stringify({ ...event.data, spectators: spectatorCounts.get(fightId) || 0 }),
|
||||||
})
|
})
|
||||||
@@ -426,8 +444,12 @@ fightsRouter.get('/:id/stream', (c) => {
|
|||||||
// React to a fight
|
// React to a fight
|
||||||
fightsRouter.post('/:id/react', async (c) => {
|
fightsRouter.post('/:id/react', async (c) => {
|
||||||
const fightId = c.req.param('id')
|
const fightId = c.req.param('id')
|
||||||
const body = await c.req.json<{ emoji?: string }>()
|
if (!isValidId(fightId)) {
|
||||||
const emoji = body?.emoji
|
return c.json({ error: 'Invalid ID format.' }, 400)
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsed = reactSchema.safeParse(await c.req.json().catch(() => ({})))
|
||||||
|
const emoji = parsed.success ? parsed.data.emoji : null
|
||||||
|
|
||||||
if (!emoji || !VALID_REACTIONS.has(emoji)) {
|
if (!emoji || !VALID_REACTIONS.has(emoji)) {
|
||||||
return c.json({ error: 'Invalid reaction. Use: fist, fire, skull, 100, clown' }, 400)
|
return c.json({ error: 'Invalid reaction. Use: fist, fire, skull, 100, clown' }, 400)
|
||||||
|
|||||||
@@ -281,7 +281,10 @@ paymentsRouter.post('/zap', rateLimit(60_000, 10), async (c) => {
|
|||||||
}>()
|
}>()
|
||||||
|
|
||||||
if (!winnerId || !fightId) return c.json({ error: 'Missing winnerId or fightId' }, 400)
|
if (!winnerId || !fightId) return c.json({ error: 'Missing winnerId or fightId' }, 400)
|
||||||
const amount = amountSats || 21
|
if (typeof amountSats !== 'number' || !Number.isInteger(amountSats) || amountSats < 1 || amountSats > 1_000_000) {
|
||||||
|
return c.json({ error: 'amountSats must be an integer between 1 and 1,000,000' }, 400)
|
||||||
|
}
|
||||||
|
const amount = amountSats
|
||||||
|
|
||||||
// Verify the fight exists and this bot actually won
|
// Verify the fight exists and this bot actually won
|
||||||
const fightRows = await db.select({
|
const fightRows = await db.select({
|
||||||
|
|||||||
Reference in New Issue
Block a user