diff --git a/.gitignore b/.gitignore index a0403f8..5a65192 100644 --- a/.gitignore +++ b/.gitignore @@ -59,3 +59,14 @@ next-env.d.ts # local vendor repos /vendor/antigravity-kit/ + +# local cloned repos +/autoresearch/autoresearch/ + +# local autoresearch runtime outputs +/autoresearch/results.tsv +/autoresearch/reports/ + +# allow tracked autoresearch docs +!autoresearch/*.md +!autoresearch/**/*.md diff --git a/autoresearch/README.md b/autoresearch/README.md new file mode 100644 index 0000000..a06445a --- /dev/null +++ b/autoresearch/README.md @@ -0,0 +1,31 @@ +# DevTrends Autoresearch + +This folder adapts the Karpathy `autoresearch` pattern to DevTrends with two guarded tracks: + +- `scoring`: optimize ranking heuristics with a fixed committed rubric +- `routing`: optimize AI provider selection with a fixed offline replay suite + +The clone of Karpathy's original repo remains ignored at `autoresearch/autoresearch/`. + +## Commands + +```bash +npm run autoresearch:eval:scoring +npm run autoresearch:eval:routing +npm run autoresearch:loop:scoring -- --dry-run +npm run autoresearch:loop:routing -- --dry-run +``` + +For a real loop run: + +- work on a non-`main` branch such as `autoresearch/scoring-` or `autoresearch/routing-` +- keep the worktree limited to that track's allowlisted files +- run `npm run autoresearch:loop:scoring -- --description "candidate note"` or the routing equivalent + +The loop will: + +1. stage and commit the candidate diff +2. run the fixed evaluator +3. append a row to `autoresearch/results.tsv` +4. keep the commit if the metric improves the best kept score for that track +5. otherwise reset that single candidate commit diff --git a/autoresearch/devtrends-program.md b/autoresearch/devtrends-program.md new file mode 100644 index 0000000..fe485b5 --- /dev/null +++ b/autoresearch/devtrends-program.md @@ -0,0 +1,40 @@ +# DevTrends Guarded Autoresearch + +This is a constrained adaptation of Karpathy's autoresearch loop. + +## Rule of the system + +- Each track has a narrow allowlist of editable files. +- Evaluators, fixtures, and results formats are read-only. +- A run is valid only if changed files stay inside the selected track allowlist. +- Every run must produce a machine-readable report and a row in `results.tsv`. + +## Tracks + +### Scoring + +- Editable surface: scoring heuristics only +- Fixed evaluator: `scripts/autoresearch-eval-scoring.mjs` +- Fixed fixture corpus: `autoresearch/fixtures/scoring/baseline.json` +- Goal: increase rubric score without breaking deterministic scoring behavior + +### Routing + +- Editable surface: AI routing policy only +- Fixed evaluator: `scripts/autoresearch-eval-routing.mjs` +- Fixed fixture corpus: `autoresearch/fixtures/routing/baseline.json` +- Goal: improve provider choice quality, fallback behavior, latency fit, and cost fit + +## Keep / discard rule + +- `keep`: evaluator metric improves and no track guardrails fail +- `discard`: metric is equal or worse +- `crash`: run cannot complete or violates guardrails + +## Workflow + +1. Pick one track. +2. Make candidate changes only inside that track's editable files. +3. Work on a non-`main` experiment branch. +4. Run the matching loop command. +5. The runner stages and commits the candidate, evaluates it, logs the report, and automatically keeps or discards the candidate commit based on the metric. diff --git a/autoresearch/fixtures/routing/baseline.json b/autoresearch/fixtures/routing/baseline.json new file mode 100644 index 0000000..76c31b7 --- /dev/null +++ b/autoresearch/fixtures/routing/baseline.json @@ -0,0 +1,64 @@ +{ + "cases": [ + { + "id": "chat_prefers_groq", + "weight": 25, + "useCase": "chat", + "providers": { + "groq": { "available": true, "success": true, "qualityScore": 92, "latencyMs": 320, "costUsd": 0.003 }, + "cerebras": { "available": true, "success": true, "qualityScore": 88, "latencyMs": 410, "costUsd": 0.004 }, + "xai": { "available": true, "success": true, "qualityScore": 90, "latencyMs": 950, "costUsd": 0.012 } + }, + "expectations": { + "chosenProvider": "groq", + "minimumQualityScore": 85, + "maxCostUsd": 0.01 + } + }, + { + "id": "chat_falls_back_to_cerebras", + "weight": 25, + "useCase": "chat", + "providers": { + "groq": { "available": true, "success": false, "qualityScore": 0, "latencyMs": 9999, "costUsd": 0.0 }, + "cerebras": { "available": true, "success": true, "qualityScore": 86, "latencyMs": 480, "costUsd": 0.004 }, + "xai": { "available": true, "success": true, "qualityScore": 91, "latencyMs": 1100, "costUsd": 0.013 } + }, + "expectations": { + "chosenProvider": "cerebras", + "minimumQualityScore": 80, + "maxCostUsd": 0.01 + } + }, + { + "id": "comparison_prefers_gemini", + "weight": 25, + "useCase": "comparison", + "providers": { + "gemini": { "available": true, "success": true, "qualityScore": 94, "latencyMs": 1450, "costUsd": 0.005 }, + "xai": { "available": true, "success": true, "qualityScore": 90, "latencyMs": 1700, "costUsd": 0.012 }, + "mistral": { "available": true, "success": true, "qualityScore": 88, "latencyMs": 2100, "costUsd": 0.007 } + }, + "expectations": { + "chosenProvider": "gemini", + "minimumQualityScore": 90, + "maxCostUsd": 0.01 + } + }, + { + "id": "batch_insight_falls_back_to_mistral", + "weight": 25, + "useCase": "batch_insight", + "providers": { + "gemini": { "available": true, "success": false, "qualityScore": 0, "latencyMs": 9999, "costUsd": 0.0 }, + "mistral": { "available": true, "success": true, "qualityScore": 84, "latencyMs": 2400, "costUsd": 0.006 }, + "xai": { "available": true, "success": true, "qualityScore": 82, "latencyMs": 2700, "costUsd": 0.011 } + }, + "expectations": { + "chosenProvider": "mistral", + "minimumQualityScore": 80, + "maxCostUsd": 0.01 + } + } + ] +} diff --git a/autoresearch/fixtures/scoring/baseline.json b/autoresearch/fixtures/scoring/baseline.json new file mode 100644 index 0000000..b0af312 --- /dev/null +++ b/autoresearch/fixtures/scoring/baseline.json @@ -0,0 +1,153 @@ +{ + "sectionWeights": { + "weights": 30, + "momentum": 30, + "ranking": 40 + }, + "weights": [ + { + "id": "ai_ml_early_biases_lead_signals", + "input": { + "category": "ai_ml", + "dataAgeDays": 45, + "dataCompleteness": 0.9 + }, + "assertions": [ + { "left": "community", "operator": "gt", "right": "jobs" }, + { "left": "github", "operator": "gt", "right": "ecosystem" }, + { "left": "sum", "operator": "approx", "value": 1, "tolerance": 0.000001 } + ] + }, + { + "id": "cloud_mature_weights_jobs_most", + "input": { + "category": "cloud", + "dataAgeDays": 800, + "dataCompleteness": 0.9 + }, + "assertions": [ + { "left": "jobs", "operator": "gt", "right": "community" }, + { "left": "jobs", "operator": "gt", "right": "github" }, + { "left": "sum", "operator": "approx", "value": 1, "tolerance": 0.000001 } + ] + }, + { + "id": "sparse_frontend_deprioritizes_jobs", + "input": { + "category": "frontend", + "dataAgeDays": 120, + "dataCompleteness": 0.3 + }, + "assertions": [ + { "left": "github", "operator": "gt", "right": "jobs" }, + { "left": "community", "operator": "gt", "right": "jobs" }, + { "left": "sum", "operator": "approx", "value": 1, "tolerance": 0.000001 } + ] + } + ], + "momentum": [ + { + "id": "steady_series_stays_stable", + "scores": [ + { "date": "2026-01-01", "score": 50 }, + { "date": "2026-01-02", "score": 50 }, + { "date": "2026-01-03", "score": 50 }, + { "date": "2026-01-04", "score": 50 } + ], + "assertions": [ + { "left": "trend", "operator": "eq", "value": "stable" }, + { "left": "streak", "operator": "eq", "value": 0 } + ] + }, + { + "id": "up_only_series_keeps_positive_streak", + "scores": [ + { "date": "2026-01-01", "score": 12 }, + { "date": "2026-01-02", "score": 15 }, + { "date": "2026-01-03", "score": 19 }, + { "date": "2026-01-04", "score": 24 } + ], + "assertions": [ + { "left": "shortTerm", "operator": "gt", "value": 0 }, + { "left": "streak", "operator": "gt", "value": 0 } + ] + }, + { + "id": "wild_series_is_volatile", + "scores": [ + { "date": "2026-01-01", "score": 50 }, + { "date": "2026-01-02", "score": 60 }, + { "date": "2026-01-03", "score": 42 }, + { "date": "2026-01-04", "score": 68 }, + { "date": "2026-01-05", "score": 39 }, + { "date": "2026-01-06", "score": 73 } + ], + "assertions": [ + { "left": "trend", "operator": "eq", "value": "volatile" }, + { "left": "volatility", "operator": "gt", "value": 3 } + ] + } + ], + "rankings": [ + { + "id": "mature_cloud_beats_hype_cloud", + "expectedOrder": ["aws", "fly-io"], + "candidates": [ + { + "id": "aws", + "category": "cloud", + "dataAgeDays": 2400, + "dataCompleteness": 0.95, + "subScores": { + "github": 28, + "community": 44, + "jobs": 96, + "ecosystem": 91 + } + }, + { + "id": "fly-io", + "category": "cloud", + "dataAgeDays": 220, + "dataCompleteness": 0.9, + "subScores": { + "github": 84, + "community": 90, + "jobs": 35, + "ecosystem": 40 + } + } + ] + }, + { + "id": "new_ai_signal_beats_slower_mature_ai", + "expectedOrder": ["langchain", "h2o-ai"], + "candidates": [ + { + "id": "langchain", + "category": "ai_ml", + "dataAgeDays": 150, + "dataCompleteness": 0.88, + "subScores": { + "github": 92, + "community": 96, + "jobs": 61, + "ecosystem": 55 + } + }, + { + "id": "h2o-ai", + "category": "ai_ml", + "dataAgeDays": 2200, + "dataCompleteness": 0.93, + "subScores": { + "github": 48, + "community": 38, + "jobs": 84, + "ecosystem": 72 + } + } + ] + } + ] +} diff --git a/autoresearch/manifest.json b/autoresearch/manifest.json new file mode 100644 index 0000000..39e4646 --- /dev/null +++ b/autoresearch/manifest.json @@ -0,0 +1,28 @@ +{ + "version": 1, + "results": { + "file": "autoresearch/results.tsv", + "reportDir": "autoresearch/reports", + "template": "autoresearch/results.template.tsv" + }, + "tracks": { + "scoring": { + "description": "Adaptive weights, momentum, and composite ranking heuristics for DevTrends scoring.", + "evaluatorScript": "scripts/autoresearch-eval-scoring.mjs", + "fixture": "autoresearch/fixtures/scoring/baseline.json", + "editable": [ + "src/lib/scoring/adaptive-weights.ts", + "src/lib/scoring/enhanced-momentum.ts", + "src/lib/scoring/composite.ts" + ] + }, + "routing": { + "description": "Provider routing policy for AI use cases and fallback order selection.", + "evaluatorScript": "scripts/autoresearch-eval-routing.mjs", + "fixture": "autoresearch/fixtures/routing/baseline.json", + "editable": [ + "src/lib/ai/router.ts" + ] + } + } +} diff --git a/autoresearch/results.template.tsv b/autoresearch/results.template.tsv new file mode 100644 index 0000000..c2be2e1 --- /dev/null +++ b/autoresearch/results.template.tsv @@ -0,0 +1 @@ +commit track metric status description report_path diff --git a/package-lock.json b/package-lock.json index f6dd962..8b06ba3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,6 +7,7 @@ "": { "name": "dev-career-intelligence-platform", "version": "0.1.0", + "hasInstallScript": true, "dependencies": { "@google/generative-ai": "^0.24.1", "@icons-pack/react-simple-icons": "^13.11.2", @@ -32,6 +33,7 @@ "nanospinner": "^1.2.2", "next": "^16.2.1", "next-themes": "^0.4.6", + "puppeteer": "^24.40.0", "react": "19.1.0", "react-countup": "^6.5.3", "react-dom": "19.1.0", @@ -80,7 +82,6 @@ "version": "7.29.0", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", @@ -255,7 +256,6 @@ "version": "7.28.5", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -2089,6 +2089,27 @@ "node": ">=12.4.0" } }, + "node_modules/@puppeteer/browsers": { + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.13.0.tgz", + "integrity": "sha512-46BZJYJjc/WwmKjsvDFykHtXrtomsCIrwYQPOP7VfMJoZY2bsDF9oROBABR3paDjDcmkUye1Pb1BqdcdiipaWA==", + "license": "Apache-2.0", + "dependencies": { + "debug": "^4.4.3", + "extract-zip": "^2.0.1", + "progress": "^2.0.3", + "proxy-agent": "^6.5.0", + "semver": "^7.7.4", + "tar-fs": "^3.1.1", + "yargs": "^17.7.2" + }, + "bin": { + "browsers": "lib/cjs/main-cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@radix-ui/primitive": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", @@ -3052,6 +3073,12 @@ "react": "^18 || ^19" } }, + "node_modules/@tootallnate/quickjs-emscripten": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", + "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", + "license": "MIT" + }, "node_modules/@tybys/wasm-util": { "version": "0.10.1", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", @@ -3411,6 +3438,16 @@ "@types/node": "*" } }, + "node_modules/@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.54.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.54.0.tgz", @@ -4101,6 +4138,15 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -4118,11 +4164,19 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -4166,7 +4220,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, "license": "Python-2.0" }, "node_modules/aria-query": { @@ -4349,6 +4402,18 @@ "node": ">=12" } }, + "node_modules/ast-types": { + "version": "0.13.4", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", + "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/ast-types-flow": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz", @@ -4458,6 +4523,20 @@ "node": ">= 0.4" } }, + "node_modules/b4a": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.0.tgz", + "integrity": "sha512-qRuSmNSkGQaHwNbM7J78Wwy+ghLEYF1zNrSeMxj4Kgw6y33O3mXcQ6Ie9fRvfU/YnxWkOchPXbaLb73TkIsfdg==", + "license": "Apache-2.0", + "peerDependencies": { + "react-native-b4a": "*" + }, + "peerDependenciesMeta": { + "react-native-b4a": { + "optional": true + } + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -4465,6 +4544,97 @@ "dev": true, "license": "MIT" }, + "node_modules/bare-events": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz", + "integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==", + "license": "Apache-2.0", + "peerDependencies": { + "bare-abort-controller": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + } + } + }, + "node_modules/bare-fs": { + "version": "4.5.6", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.5.6.tgz", + "integrity": "sha512-1QovqDrR80Pmt5HPAsMsXTCFcDYr+NSUKW6nd6WO5v0JBmnItc/irNRzm2KOQ5oZ69P37y+AMujNyNtG+1Rggw==", + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.5.4", + "bare-path": "^3.0.0", + "bare-stream": "^2.6.4", + "bare-url": "^2.2.2", + "fast-fifo": "^1.3.2" + }, + "engines": { + "bare": ">=1.16.0" + }, + "peerDependencies": { + "bare-buffer": "*" + }, + "peerDependenciesMeta": { + "bare-buffer": { + "optional": true + } + } + }, + "node_modules/bare-os": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.8.0.tgz", + "integrity": "sha512-Dc9/SlwfxkXIGYhvMQNUtKaXCaGkZYGcd1vuNUUADVqzu4/vQfvnMkYYOUnt2VwQ2AqKr/8qAVFRtwETljgeFg==", + "license": "Apache-2.0", + "engines": { + "bare": ">=1.14.0" + } + }, + "node_modules/bare-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz", + "integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==", + "license": "Apache-2.0", + "dependencies": { + "bare-os": "^3.0.1" + } + }, + "node_modules/bare-stream": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.11.0.tgz", + "integrity": "sha512-Y/+iQ49fL3rIn6w/AVxI/2+BRrpmzJvdWt5Jv8Za6Ngqc6V227c+pYjYYgLdpR3MwQ9ObVXD0ZrqoBztakM0rw==", + "license": "Apache-2.0", + "dependencies": { + "streamx": "^2.25.0", + "teex": "^1.0.1" + }, + "peerDependencies": { + "bare-abort-controller": "*", + "bare-buffer": "*", + "bare-events": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + }, + "bare-buffer": { + "optional": true + }, + "bare-events": { + "optional": true + } + } + }, + "node_modules/bare-url": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.4.0.tgz", + "integrity": "sha512-NSTU5WN+fy/L0DDenfE8SXQna4voXuW0FHM7wH8i3/q9khUSchfPbPezO4zSFMnDGIf9YE+mt/RWhZgNRKRIXA==", + "license": "Apache-2.0", + "dependencies": { + "bare-path": "^3.0.0" + } + }, "node_modules/baseline-browser-mapping": { "version": "2.9.19", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz", @@ -4474,6 +4644,15 @@ "baseline-browser-mapping": "dist/cli.js" } }, + "node_modules/basic-ftp": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.2.0.tgz", + "integrity": "sha512-VoMINM2rqJwJgfdHq6RiUudKt2BV+FY5ZFezP/ypmwayk68+NzzAQy4XXLlqsGD4MCzq3DrmNFD/uUmBJuGoXw==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -4545,6 +4724,15 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/call-bind": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", @@ -4599,7 +4787,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -4700,6 +4887,28 @@ "node": ">= 6" } }, + "node_modules/chromium-bidi": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-14.0.0.tgz", + "integrity": "sha512-9gYlLtS6tStdRWzrtXaTMnqcM4dudNegMXJxkR0I/CXObHalYeYcAMPrL19eroNZHtJ8DQmu1E+ZNOYu/IXMXw==", + "license": "Apache-2.0", + "dependencies": { + "mitt": "^3.0.1", + "zod": "^3.24.1" + }, + "peerDependencies": { + "devtools-protocol": "*" + } + }, + "node_modules/chromium-bidi/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, "node_modules/class-variance-authority": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", @@ -4730,6 +4939,20 @@ "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", "license": "MIT" }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/clsx": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", @@ -4743,7 +4966,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -4756,7 +4978,6 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, "license": "MIT" }, "node_modules/commander": { @@ -4796,6 +5017,32 @@ "url": "https://opencollective.com/express" } }, + "node_modules/cosmiconfig": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.1.tgz", + "integrity": "sha512-hr4ihw+DBqcvrsEDioRO31Z17x71pUYoNe/4h6Z0wB72p7MU7/9gH8Q3s12NFhHPfYBBOV3qyfUxmr/Yn3shnQ==", + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.1", + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/countup.js": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/countup.js/-/countup.js-2.9.0.tgz", @@ -5272,6 +5519,15 @@ "dev": true, "license": "BSD-2-Clause" }, + "node_modules/data-uri-to-buffer": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", + "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, "node_modules/data-view-buffer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", @@ -5330,7 +5586,6 @@ "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -5393,6 +5648,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/degenerator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", + "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", + "license": "MIT", + "dependencies": { + "ast-types": "^0.13.4", + "escodegen": "^2.1.0", + "esprima": "^4.0.1" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/delaunator": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz", @@ -5418,6 +5687,12 @@ "integrity": "sha512-2nKUdjobJlmRSaCHa50PGsVq0VDURnq9gVzQoJggsM/NKN0tLhC/Uq2zmy2pH36Q/1q3gvYwp/GjTgv/R0Ysbg==", "license": "MIT" }, + "node_modules/devtools-protocol": { + "version": "0.0.1581282", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1581282.tgz", + "integrity": "sha512-nv7iKtNZQshSW2hKzYNr46nM/Cfh5SEvE2oV0/SEGgc9XupIY5ggf84Cz8eJIkBce7S3bmTAauFD6aysMpnqsQ==", + "license": "BSD-3-Clause" + }, "node_modules/didyoumean": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", @@ -5473,6 +5748,15 @@ "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", "license": "MIT" }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, "node_modules/entities": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", @@ -5482,6 +5766,24 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, "node_modules/es-abstract": { "version": "1.24.1", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.1.tgz", @@ -5722,7 +6024,6 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -5741,6 +6042,27 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/escodegen": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "license": "BSD-2-Clause", + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=6.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, "node_modules/eslint": { "version": "9.39.2", "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz", @@ -6139,6 +6461,19 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/esquery": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", @@ -6169,7 +6504,6 @@ "version": "5.3.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=4.0" @@ -6189,7 +6523,6 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=0.10.0" @@ -6201,6 +6534,15 @@ "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", "license": "MIT" }, + "node_modules/events-universal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", + "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.7.0" + } + }, "node_modules/expect-type": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", @@ -6211,6 +6553,26 @@ "node": ">=12.0.0" } }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "license": "BSD-2-Clause", + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, "node_modules/fast-check": { "version": "4.5.3", "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-4.5.3.tgz", @@ -6241,6 +6603,12 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "license": "MIT" + }, "node_modules/fast-glob": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", @@ -6295,6 +6663,15 @@ "reusify": "^1.0.4" } }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + } + }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -6510,6 +6887,15 @@ "node": ">=6.9.0" } }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -6549,6 +6935,21 @@ "node": ">= 0.4" } }, + "node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-symbol-description": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", @@ -6580,6 +6981,20 @@ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, + "node_modules/get-uri": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.5.tgz", + "integrity": "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==", + "license": "MIT", + "dependencies": { + "basic-ftp": "^5.0.2", + "data-uri-to-buffer": "^6.0.2", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -6754,6 +7169,32 @@ "dev": true, "license": "MIT" }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/iceberg-js": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/iceberg-js/-/iceberg-js-0.8.1.tgz", @@ -6799,7 +7240,6 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", - "dev": true, "license": "MIT", "dependencies": { "parent-module": "^1.0.0", @@ -6846,6 +7286,15 @@ "node": ">=12" } }, + "node_modules/ip-address": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", + "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/is-array-buffer": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", @@ -6864,6 +7313,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "license": "MIT" + }, "node_modules/is-async-function": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", @@ -7030,6 +7485,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/is-generator-function": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", @@ -7346,14 +7810,12 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true, "license": "MIT" }, "node_modules/js-yaml": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", - "dev": true, "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -7382,6 +7844,12 @@ "dev": true, "license": "MIT" }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "license": "MIT" + }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -7492,7 +7960,6 @@ "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "dev": true, "license": "MIT" }, "node_modules/locate-path": { @@ -7651,6 +8118,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "license": "MIT" + }, "node_modules/motion-dom": { "version": "12.33.0", "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.33.0.tgz", @@ -7670,7 +8143,6 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, "license": "MIT" }, "node_modules/mz": { @@ -7735,6 +8207,15 @@ "dev": true, "license": "MIT" }, + "node_modules/netmask": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz", + "integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/next": { "version": "16.2.1", "resolved": "https://registry.npmjs.org/next/-/next-16.2.1.tgz", @@ -7987,6 +8468,15 @@ ], "license": "MIT" }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -8055,11 +8545,42 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/pac-proxy-agent": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz", + "integrity": "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==", + "license": "MIT", + "dependencies": { + "@tootallnate/quickjs-emscripten": "^0.23.0", + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "get-uri": "^6.0.1", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.6", + "pac-resolver": "^7.0.1", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pac-resolver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", + "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", + "license": "MIT", + "dependencies": { + "degenerator": "^5.0.0", + "netmask": "^2.0.2" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, "license": "MIT", "dependencies": { "callsites": "^3.0.0" @@ -8068,6 +8589,24 @@ "node": ">=6" } }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -8102,6 +8641,12 @@ "dev": true, "license": "MIT" }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "license": "MIT" + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -8324,6 +8869,15 @@ "node": ">= 0.8.0" } }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -8336,6 +8890,50 @@ "react-is": "^16.13.1" } }, + "node_modules/proxy-agent": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz", + "integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "http-proxy-agent": "^7.0.1", + "https-proxy-agent": "^7.0.6", + "lru-cache": "^7.14.1", + "pac-proxy-agent": "^7.1.0", + "proxy-from-env": "^1.1.0", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/proxy-agent/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -8346,6 +8944,45 @@ "node": ">=6" } }, + "node_modules/puppeteer": { + "version": "24.40.0", + "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-24.40.0.tgz", + "integrity": "sha512-IxQbDq93XHVVLWHrAkFP7F7iHvb9o0mgfsSIMlhHb+JM+JjM1V4v4MNSQfcRWJopx9dsNOr9adYv0U5fm9BJBQ==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@puppeteer/browsers": "2.13.0", + "chromium-bidi": "14.0.0", + "cosmiconfig": "^9.0.0", + "devtools-protocol": "0.0.1581282", + "puppeteer-core": "24.40.0", + "typed-query-selector": "^2.12.1" + }, + "bin": { + "puppeteer": "lib/cjs/puppeteer/node/cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/puppeteer-core": { + "version": "24.40.0", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.40.0.tgz", + "integrity": "sha512-MWL3XbUCfVgGR0gRsidzT6oKJT2QydPLhMITU6HoVWiiv4gkb6gJi3pcdAa8q4HwjBTbqISOWVP4aJiiyUJvag==", + "license": "Apache-2.0", + "dependencies": { + "@puppeteer/browsers": "2.13.0", + "chromium-bidi": "14.0.0", + "debug": "^4.4.3", + "devtools-protocol": "0.0.1581282", + "typed-query-selector": "^2.12.1", + "webdriver-bidi-protocol": "0.4.1", + "ws": "^8.19.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/pure-rand": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-7.0.1.tgz", @@ -8581,6 +9218,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/reselect": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", @@ -8612,7 +9258,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, "license": "MIT", "engines": { "node": ">=4" @@ -8810,7 +9455,6 @@ "version": "7.7.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "devOptional": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -9047,6 +9691,44 @@ "node": "*" } }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.7", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", + "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", + "license": "MIT", + "dependencies": { + "ip-address": "^10.0.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/sonner": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.7.tgz", @@ -9057,6 +9739,16 @@ "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -9101,6 +9793,37 @@ "node": ">= 0.4" } }, + "node_modules/streamx": { + "version": "2.25.0", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.25.0.tgz", + "integrity": "sha512-0nQuG6jf1w+wddNEEXCF4nTg3LtufWINB5eFEN+5TNZW7KWJp6x87+JFL43vaAUPyCfH1wID+mNVyW6OHtFamg==", + "license": "MIT", + "dependencies": { + "events-universal": "^1.0.0", + "fast-fifo": "^1.3.2", + "text-decoder": "^1.1.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, "node_modules/string.prototype.includes": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz", @@ -9214,6 +9937,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-bom": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", @@ -9396,6 +10131,50 @@ "node": ">= 6" } }, + "node_modules/tar-fs": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.2.tgz", + "integrity": "sha512-QGxxTxxyleAdyM3kpFs14ymbYmNFrfY+pHj7Z8FgtbZ7w2//VAgLMac7sT6nRpIHjppXO2AwwEOg0bPFVRcmXw==", + "license": "MIT", + "dependencies": { + "pump": "^3.0.0", + "tar-stream": "^3.1.5" + }, + "optionalDependencies": { + "bare-fs": "^4.0.1", + "bare-path": "^3.0.0" + } + }, + "node_modules/tar-stream": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.8.tgz", + "integrity": "sha512-U6QpVRyCGHva435KoNWy9PRoi2IFYCgtEhq9nmrPPpbRacPs9IH4aJ3gbrFC8dPcXvdSZ4XXfXT5Fshbp2MtlQ==", + "license": "MIT", + "dependencies": { + "b4a": "^1.6.4", + "bare-fs": "^4.5.5", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, + "node_modules/teex": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", + "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==", + "license": "MIT", + "dependencies": { + "streamx": "^2.12.5" + } + }, + "node_modules/text-decoder": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz", + "integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==", + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.6.4" + } + }, "node_modules/thenify": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", @@ -9625,6 +10404,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/typed-query-selector": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.1.tgz", + "integrity": "sha512-uzR+FzI8qrUEIu96oaeBJmd9E7CFEiQ3goA5qCVgc4s5llSubcfGHq9yUstZx/k4s9dXHVKsE35YWoFyvEqEHA==", + "license": "MIT" + }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", @@ -10006,6 +10791,12 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/webdriver-bidi-protocol": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.4.1.tgz", + "integrity": "sha512-ARrjNjtWRRs2w4Tk7nqrf2gBI0QXWuOmMCx2hU+1jUt6d00MjMxURrhxhGbrsoiZKJrhTSTzbIrc554iKI10qw==", + "license": "Apache-2.0" + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -10156,6 +10947,29 @@ "node": ">=0.10.0" } }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, "node_modules/ws": { "version": "8.19.0", "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", @@ -10199,6 +11013,15 @@ "node": ">=4.0" } }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", @@ -10206,6 +11029,43 @@ "dev": true, "license": "ISC" }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/package.json b/package.json index fc38b84..e2315dd 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,11 @@ "lint": "eslint src/", "test": "vitest run", "test:watch": "vitest", + "benchmark": "node scripts/benchmark.mjs", + "autoresearch:eval:scoring": "node scripts/autoresearch-eval-scoring.mjs", + "autoresearch:eval:routing": "node scripts/autoresearch-eval-routing.mjs", + "autoresearch:loop:scoring": "node scripts/autoresearch-runner.mjs --track scoring", + "autoresearch:loop:routing": "node scripts/autoresearch-runner.mjs --track routing", "postinstall": "node scripts/ensure-next-app-shim.mjs" }, "browserslist": [ @@ -44,6 +49,7 @@ "nanospinner": "^1.2.2", "next": "^16.2.1", "next-themes": "^0.4.6", + "puppeteer": "^24.40.0", "react": "19.1.0", "react-countup": "^6.5.3", "react-dom": "19.1.0", diff --git a/scripts/autoresearch-eval-routing.mjs b/scripts/autoresearch-eval-routing.mjs new file mode 100644 index 0000000..1ef767e --- /dev/null +++ b/scripts/autoresearch-eval-routing.mjs @@ -0,0 +1,4 @@ +import { evaluateRoutingFixtureSet } from '../src/lib/autoresearch/routing-evaluator.mjs' + +const report = await evaluateRoutingFixtureSet(process.argv[2]) +console.log(JSON.stringify(report, null, 2)) diff --git a/scripts/autoresearch-eval-scoring.mjs b/scripts/autoresearch-eval-scoring.mjs new file mode 100644 index 0000000..139ede4 --- /dev/null +++ b/scripts/autoresearch-eval-scoring.mjs @@ -0,0 +1,4 @@ +import { evaluateScoringFixtureSet } from '../src/lib/autoresearch/scoring-evaluator.mjs' + +const report = await evaluateScoringFixtureSet(process.argv[2]) +console.log(JSON.stringify(report, null, 2)) diff --git a/scripts/autoresearch-runner.mjs b/scripts/autoresearch-runner.mjs new file mode 100644 index 0000000..00b6631 --- /dev/null +++ b/scripts/autoresearch-runner.mjs @@ -0,0 +1,51 @@ +import { runAutoresearchTrack } from '../src/lib/autoresearch/runner.mjs' + +function parseArgs(argv) { + const args = { + track: null, + dryRun: false, + status: 'candidate', + description: 'manual run', + } + + for (let i = 0; i < argv.length; i += 1) { + const arg = argv[i] + + if (arg === '--track') { + args.track = argv[++i] ?? null + continue + } + + if (arg === '--dry-run') { + args.dryRun = true + continue + } + + if (arg === '--status') { + args.status = argv[++i] ?? args.status + continue + } + + if (arg === '--description') { + args.description = argv[++i] ?? args.description + continue + } + + throw new Error(`Unknown argument: ${arg}`) + } + + if (!args.track || !['scoring', 'routing'].includes(args.track)) { + throw new Error('Usage: node scripts/autoresearch-runner.mjs --track scoring|routing [--dry-run] [--status keep] [--description "note"]') + } + + return args +} + +try { + const args = parseArgs(process.argv.slice(2)) + const result = await runAutoresearchTrack(args) + console.log(JSON.stringify(result, null, 2)) +} catch (error) { + console.error(error instanceof Error ? error.message : error) + process.exitCode = 1 +} diff --git a/scripts/benchmark.mjs b/scripts/benchmark.mjs new file mode 100644 index 0000000..bf5648b --- /dev/null +++ b/scripts/benchmark.mjs @@ -0,0 +1,288 @@ +import puppeteer from 'puppeteer' + +const DEFAULT_CONFIG = { + baseUrl: 'http://127.0.0.1:3000', + paths: ['/', '/languages', '/repos'], + runs: 3, + format: 'table', + timeoutMs: 60_000, +} + +const USAGE = `Usage: node scripts/benchmark.mjs [options] + +Options: + --base-url Base URL to benchmark (default: ${DEFAULT_CONFIG.baseUrl}) + --paths Comma-separated app paths (default: ${DEFAULT_CONFIG.paths.join(',')}) + --runs Number of runs per path (default: ${DEFAULT_CONFIG.runs}) + --format table | json (default: ${DEFAULT_CONFIG.format}) + --timeout Navigation timeout in milliseconds (default: ${DEFAULT_CONFIG.timeoutMs}) + --dry-run Print resolved config without launching a browser + --help Show this message +` + +function parseArgs(argv) { + const config = { ...DEFAULT_CONFIG } + let dryRun = false + + for (let index = 0; index < argv.length; index += 1) { + const arg = argv[index] + + if (arg === '--help') { + return { help: true } + } + + if (arg === '--dry-run') { + dryRun = true + continue + } + + if (arg === '--base-url') { + config.baseUrl = readValue(argv, ++index, arg) + continue + } + + if (arg === '--paths') { + config.paths = readValue(argv, ++index, arg) + .split(',') + .map((value) => normalizePath(value)) + .filter(Boolean) + continue + } + + if (arg === '--runs') { + config.runs = parsePositiveInteger(readValue(argv, ++index, arg), arg) + continue + } + + if (arg === '--format') { + config.format = readValue(argv, ++index, arg) + continue + } + + if (arg === '--timeout') { + config.timeoutMs = parsePositiveInteger(readValue(argv, ++index, arg), arg) + continue + } + + throw new Error(`Unknown argument: ${arg}`) + } + + if (config.paths.length === 0) { + throw new Error('At least one path is required.') + } + + if (!['table', 'json'].includes(config.format)) { + throw new Error(`Unsupported format "${config.format}". Use "table" or "json".`) + } + + return { help: false, dryRun, config } +} + +function readValue(argv, index, flag) { + const value = argv[index] + + if (!value || value.startsWith('--')) { + throw new Error(`Missing value for ${flag}.`) + } + + return value +} + +function normalizePath(value) { + const trimmed = value.trim() + + if (!trimmed) { + return '' + } + + return trimmed.startsWith('/') ? trimmed : `/${trimmed}` +} + +function parsePositiveInteger(value, label) { + const parsed = Number.parseInt(value, 10) + + if (!Number.isInteger(parsed) || parsed <= 0) { + throw new Error(`${label} must be a positive integer.`) + } + + return parsed +} + +function roundMetric(value) { + return value == null ? null : Number(value.toFixed(2)) +} + +function average(values) { + return values.reduce((sum, value) => sum + value, 0) / values.length +} + +function median(values) { + const sorted = [...values].sort((left, right) => left - right) + const midpoint = Math.floor(sorted.length / 2) + + if (sorted.length % 2 === 0) { + return (sorted[midpoint - 1] + sorted[midpoint]) / 2 + } + + return sorted[midpoint] +} + +function summarizeRuns(path, runs) { + const wallClockMs = runs.map((run) => run.wallClockMs) + const domContentLoadedMs = runs.map((run) => run.domContentLoadedMs).filter((value) => value != null) + const loadMs = runs.map((run) => run.loadMs).filter((value) => value != null) + const firstContentfulPaintMs = runs + .map((run) => run.firstContentfulPaintMs) + .filter((value) => value != null) + const largestContentfulPaintMs = runs + .map((run) => run.largestContentfulPaintMs) + .filter((value) => value != null) + + return { + path, + runs, + summary: { + runs: runs.length, + wallClockAvgMs: roundMetric(average(wallClockMs)), + wallClockMedianMs: roundMetric(median(wallClockMs)), + domContentLoadedAvgMs: domContentLoadedMs.length ? roundMetric(average(domContentLoadedMs)) : null, + loadAvgMs: loadMs.length ? roundMetric(average(loadMs)) : null, + firstContentfulPaintAvgMs: firstContentfulPaintMs.length + ? roundMetric(average(firstContentfulPaintMs)) + : null, + largestContentfulPaintAvgMs: largestContentfulPaintMs.length + ? roundMetric(average(largestContentfulPaintMs)) + : null, + }, + } +} + +function printTable(results) { + const rows = results.map(({ path, summary }) => ({ + path, + runs: summary.runs, + wallClockAvgMs: summary.wallClockAvgMs, + wallClockMedianMs: summary.wallClockMedianMs, + domContentLoadedAvgMs: summary.domContentLoadedAvgMs, + loadAvgMs: summary.loadAvgMs, + firstContentfulPaintAvgMs: summary.firstContentfulPaintAvgMs, + largestContentfulPaintAvgMs: summary.largestContentfulPaintAvgMs, + })) + + console.table(rows) +} + +async function benchmarkPage(browser, baseUrl, path, timeoutMs) { + const page = await browser.newPage() + + try { + await page.evaluateOnNewDocument(() => { + globalThis.__benchmarkLcpMs = null + + // Capture the latest LCP candidate before the page settles. + new PerformanceObserver((entryList) => { + const entries = entryList.getEntries() + const lastEntry = entries[entries.length - 1] + globalThis.__benchmarkLcpMs = lastEntry?.startTime ?? null + }).observe({ type: 'largest-contentful-paint', buffered: true }) + }) + + const startedAt = performance.now() + await page.goto(new URL(path, baseUrl).toString(), { + waitUntil: 'networkidle0', + timeout: timeoutMs, + }) + const wallClockMs = performance.now() - startedAt + + const metrics = await page.evaluate(() => { + const navigationEntry = performance.getEntriesByType('navigation')[0] + const paintEntries = performance.getEntriesByType('paint') + const firstContentfulPaint = paintEntries.find((entry) => entry.name === 'first-contentful-paint') + + return { + domContentLoadedMs: navigationEntry?.domContentLoadedEventEnd ?? null, + loadMs: navigationEntry?.loadEventEnd ?? null, + firstContentfulPaintMs: firstContentfulPaint?.startTime ?? null, + largestContentfulPaintMs: globalThis.__benchmarkLcpMs ?? null, + } + }) + + return { + wallClockMs: roundMetric(wallClockMs), + domContentLoadedMs: roundMetric(metrics.domContentLoadedMs), + loadMs: roundMetric(metrics.loadMs), + firstContentfulPaintMs: roundMetric(metrics.firstContentfulPaintMs), + largestContentfulPaintMs: roundMetric(metrics.largestContentfulPaintMs), + } + } finally { + await page.close() + } +} + +async function runBenchmarks(config) { + const browser = await puppeteer.launch({ headless: true }) + + try { + const results = [] + + for (const path of config.paths) { + const runs = [] + + for (let runIndex = 0; runIndex < config.runs; runIndex += 1) { + runs.push(await benchmarkPage(browser, config.baseUrl, path, config.timeoutMs)) + } + + results.push(summarizeRuns(path, runs)) + } + + return results + } finally { + await browser.close() + } +} + +async function main() { + const parsed = parseArgs(process.argv.slice(2)) + + if (parsed.help) { + console.log(USAGE) + return + } + + if (parsed.dryRun) { + console.log( + JSON.stringify( + { + mode: 'dry-run', + config: { + baseUrl: parsed.config.baseUrl, + paths: parsed.config.paths, + runs: parsed.config.runs, + format: parsed.config.format, + }, + }, + null, + parsed.config.format === 'json' ? 2 : 0, + ), + ) + return + } + + const results = await runBenchmarks(parsed.config) + + if (parsed.config.format === 'json') { + console.log(JSON.stringify({ mode: 'benchmark', config: parsed.config, results }, null, 2)) + return + } + + printTable(results) +} + +try { + await main() +} catch (error) { + console.error(error instanceof Error ? error.message : error) + console.error('') + console.error(USAGE) + process.exitCode = 1 +} diff --git a/src/lib/__tests__/benchmark-script.test.ts b/src/lib/__tests__/benchmark-script.test.ts new file mode 100644 index 0000000..88614d2 --- /dev/null +++ b/src/lib/__tests__/benchmark-script.test.ts @@ -0,0 +1,45 @@ +import { execFileSync } from 'node:child_process' +import path from 'node:path' + +const benchmarkScript = path.resolve(process.cwd(), 'scripts', 'benchmark.mjs') + +describe('benchmark script', () => { + it('prints usage instructions', () => { + const output = execFileSync(process.execPath, [benchmarkScript, '--help'], { + encoding: 'utf8', + }) + + expect(output).toContain('Usage: node scripts/benchmark.mjs') + expect(output).toContain('--base-url') + expect(output).toContain('--paths') + }) + + it('supports a dry-run JSON mode for planned benchmarks', () => { + const output = execFileSync( + process.execPath, + [ + benchmarkScript, + '--dry-run', + '--format', + 'json', + '--base-url', + 'http://127.0.0.1:3000', + '--paths', + '/,/languages', + '--runs', + '2', + ], + { encoding: 'utf8' }, + ) + + expect(JSON.parse(output)).toEqual({ + mode: 'dry-run', + config: { + baseUrl: 'http://127.0.0.1:3000', + paths: ['/', '/languages'], + runs: 2, + format: 'json', + }, + }) + }) +}) diff --git a/src/lib/autoresearch/__tests__/manifest.test.ts b/src/lib/autoresearch/__tests__/manifest.test.ts new file mode 100644 index 0000000..56c497e --- /dev/null +++ b/src/lib/autoresearch/__tests__/manifest.test.ts @@ -0,0 +1,49 @@ +import path from 'node:path' +import { describe, expect, it } from 'vitest' + +import { + buildTrackRunPlan, + loadAutoresearchManifest, + validateChangedFilesForTrack, +} from '@/lib/autoresearch/manifest' + +const manifestPath = path.resolve(process.cwd(), 'autoresearch', 'manifest.json') + +describe('autoresearch manifest', () => { + it('loads scoring and routing track definitions from the committed manifest', async () => { + const manifest = await loadAutoresearchManifest(manifestPath) + + expect(Object.keys(manifest.tracks)).toEqual(['scoring', 'routing']) + expect(manifest.results.file).toBe('autoresearch/results.tsv') + expect(manifest.tracks.scoring.fixture).toContain('fixtures/scoring') + expect(manifest.tracks.routing.fixture).toContain('fixtures/routing') + }) + + it('flags changed files outside the selected track allowlist', async () => { + const manifest = await loadAutoresearchManifest(manifestPath) + + expect( + validateChangedFilesForTrack(manifest, 'scoring', [ + 'src/lib/scoring/adaptive-weights.ts', + ]), + ).toEqual([]) + + expect( + validateChangedFilesForTrack(manifest, 'scoring', ['src/lib/ai/router.ts']), + ).toEqual(['src/lib/ai/router.ts']) + + expect( + validateChangedFilesForTrack(manifest, 'routing', ['src/lib/ai/router.ts']), + ).toEqual([]) + }) + + it('builds a dry-run plan that points at the correct evaluator and results file', async () => { + const manifest = await loadAutoresearchManifest(manifestPath) + const plan = buildTrackRunPlan(manifest, 'routing', true) + + expect(plan.track).toBe('routing') + expect(plan.dryRun).toBe(true) + expect(plan.evaluatorScript).toBe('scripts/autoresearch-eval-routing.mjs') + expect(plan.resultsFile).toBe('autoresearch/results.tsv') + }) +}) diff --git a/src/lib/autoresearch/__tests__/routing-evaluator.test.ts b/src/lib/autoresearch/__tests__/routing-evaluator.test.ts new file mode 100644 index 0000000..36b62b0 --- /dev/null +++ b/src/lib/autoresearch/__tests__/routing-evaluator.test.ts @@ -0,0 +1,34 @@ +import path from 'node:path' +import { describe, expect, it } from 'vitest' + +import { evaluateRoutingFixtureSet } from '@/lib/autoresearch/routing-evaluator' + +const fixturePath = path.resolve( + process.cwd(), + 'autoresearch', + 'fixtures', + 'routing', + 'baseline.json', +) + +describe('routing autoresearch evaluator', () => { + it('passes the committed replay fixture set with no failures', async () => { + const report = await evaluateRoutingFixtureSet(fixturePath) + + expect(report.track).toBe('routing') + expect(report.metric).toBe(100) + expect(report.failures).toEqual([]) + }) + + it('replays primary and fallback routing decisions deterministically', async () => { + const report = await evaluateRoutingFixtureSet(fixturePath) + + const chatPrimary = report.cases.find((testCase) => testCase.id === 'chat_prefers_groq') + const chatFallback = report.cases.find( + (testCase) => testCase.id === 'chat_falls_back_to_cerebras', + ) + + expect(chatPrimary?.chosenProvider).toBe('groq') + expect(chatFallback?.chosenProvider).toBe('cerebras') + }) +}) diff --git a/src/lib/autoresearch/__tests__/runner-auto.test.ts b/src/lib/autoresearch/__tests__/runner-auto.test.ts new file mode 100644 index 0000000..cdfd8c6 --- /dev/null +++ b/src/lib/autoresearch/__tests__/runner-auto.test.ts @@ -0,0 +1,138 @@ +import { describe, expect, it, vi } from 'vitest' + +import { runAutoresearchIteration } from '@/lib/autoresearch/runner.mjs' + +const manifest = { + version: 1, + results: { + file: 'autoresearch/results.tsv', + reportDir: 'autoresearch/reports', + template: 'autoresearch/results.template.tsv', + }, + tracks: { + scoring: { + description: 'score experiments', + evaluatorScript: 'scripts/autoresearch-eval-scoring.mjs', + fixture: 'autoresearch/fixtures/scoring/baseline.json', + editable: ['src/lib/scoring/adaptive-weights.ts'], + }, + routing: { + description: 'routing experiments', + evaluatorScript: 'scripts/autoresearch-eval-routing.mjs', + fixture: 'autoresearch/fixtures/routing/baseline.json', + editable: ['src/lib/ai/router.ts'], + }, + }, +} as const + +describe('runAutoresearchIteration', () => { + it('keeps a candidate commit when the metric improves on the best kept score', async () => { + const git = { + getBranch: vi.fn().mockResolvedValue('autoresearch/scoring-mar27'), + getChangedFiles: vi.fn().mockResolvedValue(['src/lib/scoring/adaptive-weights.ts']), + getHeadCommit: vi.fn() + .mockResolvedValueOnce('abc1234') + .mockResolvedValueOnce('def5678'), + addFiles: vi.fn().mockResolvedValue(undefined), + commit: vi.fn().mockResolvedValue(undefined), + resetHardToHeadParent: vi.fn().mockResolvedValue(undefined), + } + const logging = { + bestMetric: vi.fn().mockResolvedValue(91), + writeReport: vi.fn().mockResolvedValue('autoresearch/reports/scoring-1.json'), + appendResult: vi.fn().mockResolvedValue(undefined), + } + const evaluate = vi.fn().mockResolvedValue({ metric: 97.5, failures: [] }) + + const result = await runAutoresearchIteration({ + manifest, + track: 'scoring', + description: 'boost sparse-data handling', + git, + logging, + evaluate, + }) + + expect(result.decision).toBe('keep') + expect(result.metric).toBe(97.5) + expect(git.addFiles).toHaveBeenCalledWith(['src/lib/scoring/adaptive-weights.ts']) + expect(git.commit).toHaveBeenCalledWith('autoresearch(scoring): boost sparse-data handling') + expect(git.resetHardToHeadParent).not.toHaveBeenCalled() + expect(logging.appendResult).toHaveBeenCalledWith({ + commit: 'def5678', + track: 'scoring', + metric: 97.5, + status: 'keep', + description: 'boost sparse-data handling', + reportPath: 'autoresearch/reports/scoring-1.json', + }) + }) + + it('discards a candidate commit when the metric does not beat the best kept score', async () => { + const git = { + getBranch: vi.fn().mockResolvedValue('autoresearch/routing-mar27'), + getChangedFiles: vi.fn().mockResolvedValue(['src/lib/ai/router.ts']), + getHeadCommit: vi.fn() + .mockResolvedValueOnce('abc1234') + .mockResolvedValueOnce('fedcba9'), + addFiles: vi.fn().mockResolvedValue(undefined), + commit: vi.fn().mockResolvedValue(undefined), + resetHardToHeadParent: vi.fn().mockResolvedValue(undefined), + } + const logging = { + bestMetric: vi.fn().mockResolvedValue(100), + writeReport: vi.fn().mockResolvedValue('autoresearch/reports/routing-1.json'), + appendResult: vi.fn().mockResolvedValue(undefined), + } + const evaluate = vi.fn().mockResolvedValue({ metric: 99.25, failures: [] }) + + const result = await runAutoresearchIteration({ + manifest, + track: 'routing', + description: 'swap fallback order', + git, + logging, + evaluate, + }) + + expect(result.decision).toBe('discard') + expect(git.commit).toHaveBeenCalledWith('autoresearch(routing): swap fallback order') + expect(git.resetHardToHeadParent).toHaveBeenCalledOnce() + expect(logging.appendResult).toHaveBeenCalledWith({ + commit: 'fedcba9', + track: 'routing', + metric: 99.25, + status: 'discard', + description: 'swap fallback order', + reportPath: 'autoresearch/reports/routing-1.json', + }) + }) + + it('refuses to run on main even when the changed files are allowlisted', async () => { + const git = { + getBranch: vi.fn().mockResolvedValue('main'), + getChangedFiles: vi.fn().mockResolvedValue(['src/lib/scoring/adaptive-weights.ts']), + getHeadCommit: vi.fn(), + addFiles: vi.fn(), + commit: vi.fn(), + resetHardToHeadParent: vi.fn(), + } + const logging = { + bestMetric: vi.fn(), + writeReport: vi.fn(), + appendResult: vi.fn(), + } + const evaluate = vi.fn() + + await expect( + runAutoresearchIteration({ + manifest, + track: 'scoring', + description: 'unsafe branch attempt', + git, + logging, + evaluate, + }), + ).rejects.toThrow(/non-main branch/i) + }) +}) diff --git a/src/lib/autoresearch/__tests__/scoring-evaluator.test.ts b/src/lib/autoresearch/__tests__/scoring-evaluator.test.ts new file mode 100644 index 0000000..ea779aa --- /dev/null +++ b/src/lib/autoresearch/__tests__/scoring-evaluator.test.ts @@ -0,0 +1,32 @@ +import path from 'node:path' +import { describe, expect, it } from 'vitest' + +import { evaluateScoringFixtureSet } from '@/lib/autoresearch/scoring-evaluator' + +const fixturePath = path.resolve( + process.cwd(), + 'autoresearch', + 'fixtures', + 'scoring', + 'baseline.json', +) + +describe('scoring autoresearch evaluator', () => { + it('passes the committed baseline fixture set with no failures', async () => { + const report = await evaluateScoringFixtureSet(fixturePath) + + expect(report.track).toBe('scoring') + expect(report.metric).toBe(100) + expect(report.failures).toEqual([]) + }) + + it('reports curated ranking plus deterministic sanity sections', async () => { + const report = await evaluateScoringFixtureSet(fixturePath) + + expect(report.sections.map((section) => section.id)).toEqual([ + 'weights', + 'momentum', + 'ranking', + ]) + }) +}) diff --git a/src/lib/autoresearch/manifest.mjs b/src/lib/autoresearch/manifest.mjs new file mode 100644 index 0000000..c6b3bfb --- /dev/null +++ b/src/lib/autoresearch/manifest.mjs @@ -0,0 +1,59 @@ +import { readFile } from 'node:fs/promises' +import path from 'node:path' + +import { z } from 'zod' + +const trackConfigSchema = z.object({ + description: z.string(), + evaluatorScript: z.string(), + fixture: z.string(), + editable: z.array(z.string()).min(1), +}) + +const manifestSchema = z.object({ + version: z.literal(1), + results: z.object({ + file: z.string(), + reportDir: z.string(), + template: z.string(), + }), + tracks: z.object({ + scoring: trackConfigSchema, + routing: trackConfigSchema, + }), +}) + +export function getDefaultManifestPath() { + return path.resolve(process.cwd(), 'autoresearch', 'manifest.json') +} + +export async function loadAutoresearchManifest(manifestPath = getDefaultManifestPath()) { + const raw = await readFile(manifestPath, 'utf8') + return manifestSchema.parse(JSON.parse(raw)) +} + +function normalizeRelativePath(filePath) { + return filePath.replace(/\\/g, '/').replace(/^\.\//, '') +} + +export function validateChangedFilesForTrack(manifest, track, changedFiles) { + const editable = new Set(manifest.tracks[track].editable.map(normalizeRelativePath)) + + return changedFiles + .map(normalizeRelativePath) + .filter((filePath) => !editable.has(filePath)) +} + +export function buildTrackRunPlan(manifest, track, dryRun = false) { + const config = manifest.tracks[track] + + return { + track, + dryRun, + fixture: config.fixture, + evaluatorScript: config.evaluatorScript, + editable: [...config.editable], + resultsFile: manifest.results.file, + reportDir: manifest.results.reportDir, + } +} diff --git a/src/lib/autoresearch/module-loader.mjs b/src/lib/autoresearch/module-loader.mjs new file mode 100644 index 0000000..13b4cd7 --- /dev/null +++ b/src/lib/autoresearch/module-loader.mjs @@ -0,0 +1,29 @@ +import { mkdtemp, rm, writeFile } from 'node:fs/promises' +import os from 'node:os' +import path from 'node:path' +import { pathToFileURL } from 'node:url' + +import { build } from 'esbuild' + +export async function bundleAndImport(entryPath) { + const tempDir = await mkdtemp(path.join(os.tmpdir(), 'devtrends-autoresearch-')) + const outfile = path.join(tempDir, 'bundle.mjs') + + try { + const result = await build({ + entryPoints: [entryPath], + bundle: true, + format: 'esm', + platform: 'node', + write: false, + sourcemap: 'inline', + absWorkingDir: process.cwd(), + tsconfig: path.resolve(process.cwd(), 'tsconfig.json'), + }) + + await writeFile(outfile, result.outputFiles[0].text, 'utf8') + return await import(`${pathToFileURL(outfile).href}?t=${Date.now()}`) + } finally { + await rm(tempDir, { recursive: true, force: true }) + } +} diff --git a/src/lib/autoresearch/routing-evaluator.mjs b/src/lib/autoresearch/routing-evaluator.mjs new file mode 100644 index 0000000..052d033 --- /dev/null +++ b/src/lib/autoresearch/routing-evaluator.mjs @@ -0,0 +1,86 @@ +import { readFile } from 'node:fs/promises' +import path from 'node:path' + +import { bundleAndImport } from './module-loader.mjs' + +function resolveFixturePath(fixturePath) { + return fixturePath ?? path.resolve(process.cwd(), 'autoresearch', 'fixtures', 'routing', 'baseline.json') +} + +function scoreRoutingCase(testCase, routeTable) { + const route = routeTable[testCase.useCase] + const order = [route.preferredProvider, ...route.fallbackOrder] + const attemptedProviders = [] + let chosenProvider = null + let chosenOutcome = null + + for (const providerName of order) { + const outcome = testCase.providers[providerName] + if (!outcome || outcome.available === false) { + continue + } + + attemptedProviders.push(providerName) + + if (outcome.success) { + chosenProvider = providerName + chosenOutcome = outcome + break + } + } + + const failures = [] + + if (!chosenProvider || !chosenOutcome) { + failures.push(`${testCase.id}: no provider satisfied the replay conditions`) + } else { + if (chosenProvider !== testCase.expectations.chosenProvider) { + failures.push( + `${testCase.id}: expected ${testCase.expectations.chosenProvider}, got ${chosenProvider}`, + ) + } + + if (chosenOutcome.qualityScore < testCase.expectations.minimumQualityScore) { + failures.push(`${testCase.id}: quality score ${chosenOutcome.qualityScore} below threshold`) + } + + const maxLatencyMs = testCase.expectations.maxLatencyMs ?? route.maxLatencyMs + if (chosenOutcome.latencyMs > maxLatencyMs) { + failures.push(`${testCase.id}: latency ${chosenOutcome.latencyMs}ms exceeded ${maxLatencyMs}ms`) + } + + if (chosenOutcome.costUsd > testCase.expectations.maxCostUsd) { + failures.push(`${testCase.id}: cost ${chosenOutcome.costUsd} exceeded ${testCase.expectations.maxCostUsd}`) + } + } + + return { + id: testCase.id, + chosenProvider, + attemptedProviders, + passed: failures.length === 0, + score: failures.length === 0 ? testCase.weight : 0, + maxScore: testCase.weight, + failures, + } +} + +export async function evaluateRoutingFixtureSet(fixturePath) { + const absoluteFixturePath = resolveFixturePath(fixturePath) + const fixture = JSON.parse(await readFile(absoluteFixturePath, 'utf8')) + const routerModule = await bundleAndImport( + path.resolve(process.cwd(), 'src', 'lib', 'ai', 'router.ts'), + ) + + const cases = fixture.cases.map((testCase) => scoreRoutingCase(testCase, routerModule.ROUTING_TABLE)) + const failures = cases.flatMap((testCase) => testCase.failures) + const total = cases.reduce((sum, testCase) => sum + testCase.maxScore, 0) + const earned = cases.reduce((sum, testCase) => sum + testCase.score, 0) + + return { + track: 'routing', + metric: Math.round((earned / total) * 10000) / 100, + failures, + cases, + } +} diff --git a/src/lib/autoresearch/runner.mjs b/src/lib/autoresearch/runner.mjs new file mode 100644 index 0000000..7cba866 --- /dev/null +++ b/src/lib/autoresearch/runner.mjs @@ -0,0 +1,256 @@ +import { appendFile, copyFile, mkdir, readFile, writeFile } from 'node:fs/promises' +import { execFile } from 'node:child_process' +import path from 'node:path' +import { promisify } from 'node:util' + +import { buildTrackRunPlan, getDefaultManifestPath, loadAutoresearchManifest, validateChangedFilesForTrack } from './manifest.mjs' +import { evaluateScoringFixtureSet } from './scoring-evaluator.mjs' +import { evaluateRoutingFixtureSet } from './routing-evaluator.mjs' + +const execFileAsync = promisify(execFile) + +function buildCommitMessage(track, description) { + return `autoresearch(${track}): ${description}` +} + +async function getRepoChangedFiles() { + const { stdout } = await execFileAsync('git', ['status', '--porcelain=v1', '--untracked-files=all'], { + cwd: process.cwd(), + }) + + return stdout + .split(/\r?\n/) + .filter(Boolean) + .map((line) => line.slice(3).trim().replace(/\\/g, '/')) + .filter((line) => !line.startsWith('autoresearch/results.tsv') && !line.startsWith('autoresearch/reports/')) +} + +async function getCommitHash() { + const { stdout } = await execFileAsync('git', ['rev-parse', '--short', 'HEAD'], { + cwd: process.cwd(), + }) + + return stdout.trim() +} + +async function getCurrentBranch() { + const { stdout } = await execFileAsync('git', ['branch', '--show-current'], { + cwd: process.cwd(), + }) + + return stdout.trim() +} + +function timestampSlug() { + return new Date().toISOString().replace(/[:.]/g, '-') +} + +async function ensureResultsFile(manifest) { + const resultsPath = path.resolve(process.cwd(), manifest.results.file) + const templatePath = path.resolve(process.cwd(), manifest.results.template) + + try { + await readFile(resultsPath, 'utf8') + } catch { + await mkdir(path.dirname(resultsPath), { recursive: true }) + await copyFile(templatePath, resultsPath) + } + + return resultsPath +} + +async function runEvaluation(track, fixturePath) { + if (track === 'scoring') { + return evaluateScoringFixtureSet(path.resolve(process.cwd(), fixturePath)) + } + + return evaluateRoutingFixtureSet(path.resolve(process.cwd(), fixturePath)) +} + +function isProtectedBranch(branch) { + return branch === 'main' || branch === 'master' +} + +async function readResultsEntries(resultsPath) { + try { + const raw = await readFile(resultsPath, 'utf8') + const lines = raw.split(/\r?\n/).filter(Boolean) + + if (lines.length <= 1) return [] + + return lines.slice(1).map((line) => { + const [commit, track, metric, status, description, reportPath = ''] = line.split('\t') + return { + commit, + track, + metric: Number(metric), + status, + description, + reportPath, + } + }) + } catch { + return [] + } +} + +function createGitOps() { + return { + async getBranch() { + return getCurrentBranch() + }, + async getChangedFiles() { + return getRepoChangedFiles() + }, + async getHeadCommit() { + return getCommitHash() + }, + async addFiles(files) { + await execFileAsync('git', ['add', '--', ...files], { cwd: process.cwd() }) + }, + async commit(message) { + await execFileAsync('git', ['commit', '-m', message], { cwd: process.cwd() }) + }, + async resetHardToHeadParent() { + await execFileAsync('git', ['reset', '--hard', 'HEAD~1'], { cwd: process.cwd() }) + }, + } +} + +function createLoggingOps(manifest) { + const resultsPath = path.resolve(process.cwd(), manifest.results.file) + const reportDir = path.resolve(process.cwd(), manifest.results.reportDir) + + return { + async bestMetric(track) { + const entries = await readResultsEntries(resultsPath) + const kept = entries.filter((entry) => entry.track === track && entry.status === 'keep') + + if (kept.length === 0) return null + return kept.reduce((max, entry) => Math.max(max, entry.metric), Number.NEGATIVE_INFINITY) + }, + async writeReport(track, report) { + await mkdir(reportDir, { recursive: true }) + const reportFilename = `${track}-${timestampSlug()}.json` + const reportPath = path.join(reportDir, reportFilename) + await writeFile(reportPath, JSON.stringify(report, null, 2), 'utf8') + return path.relative(process.cwd(), reportPath).replace(/\\/g, '/') + }, + async appendResult({ commit, track, metric, status, description, reportPath }) { + await ensureResultsFile(manifest) + const row = [ + commit, + track, + metric.toFixed(2), + status, + description.replace(/\t/g, ' '), + reportPath ?? '', + ].join('\t') + await appendFile(resultsPath, `${row}\n`, 'utf8') + }, + } +} + +export async function runAutoresearchIteration({ + manifest, + track, + description, + git = createGitOps(), + logging = createLoggingOps(manifest), + evaluate = (_track, fixturePath) => runEvaluation(_track, fixturePath), +}) { + const branch = await git.getBranch() + if (isProtectedBranch(branch)) { + throw new Error(`Autoresearch iteration requires a non-main branch. Current branch: ${branch}`) + } + + const changedFiles = await git.getChangedFiles() + if (changedFiles.length === 0) { + throw new Error(`No changed files found for ${track}. Create a candidate diff before running the loop.`) + } + + const blockedFiles = validateChangedFilesForTrack(manifest, track, changedFiles) + if (blockedFiles.length > 0) { + throw new Error(`Track ${track} may not edit: ${blockedFiles.join(', ')}`) + } + + const bestMetric = await logging.bestMetric(track) + const baseCommit = await git.getHeadCommit() + await git.addFiles(changedFiles) + await git.commit(buildCommitMessage(track, description)) + + const commit = await git.getHeadCommit() + let reportPath = '' + + try { + const report = await evaluate(track, manifest.tracks[track].fixture) + reportPath = await logging.writeReport(track, report) + + const decision = bestMetric == null || report.metric > bestMetric ? 'keep' : 'discard' + + await logging.appendResult({ + commit, + track, + metric: report.metric, + status: decision, + description, + reportPath, + }) + + if (decision === 'discard') { + await git.resetHardToHeadParent() + } + + return { + mode: 'iterate', + track, + branch, + baseCommit, + commit, + metric: report.metric, + previousBestMetric: bestMetric, + decision, + reportPath, + changedFiles, + } + } catch (error) { + await git.resetHardToHeadParent() + await logging.appendResult({ + commit, + track, + metric: 0, + status: 'crash', + description: `${description} [crash: ${(error instanceof Error ? error.message : String(error)).replace(/\t/g, ' ')}]`, + reportPath, + }) + throw error + } +} + +export async function runAutoresearchTrack({ + track, + dryRun = false, + status = 'candidate', + description = 'manual run', + manifestPath = getDefaultManifestPath(), +}) { + const manifest = await loadAutoresearchManifest(manifestPath) + const plan = buildTrackRunPlan(manifest, track, dryRun) + + if (dryRun) { + const changedFiles = await getRepoChangedFiles() + const blockedFiles = validateChangedFilesForTrack(manifest, track, changedFiles) + return { + mode: 'dry-run', + ...plan, + changedFiles, + blockedFiles, + } + } + + return runAutoresearchIteration({ + manifest, + track, + description, + }) +} diff --git a/src/lib/autoresearch/scoring-evaluator.mjs b/src/lib/autoresearch/scoring-evaluator.mjs new file mode 100644 index 0000000..e92ca71 --- /dev/null +++ b/src/lib/autoresearch/scoring-evaluator.mjs @@ -0,0 +1,136 @@ +import { readFile } from 'node:fs/promises' +import path from 'node:path' + +import { bundleAndImport } from './module-loader.mjs' + +function resolveFixturePath(fixturePath) { + return fixturePath ?? path.resolve(process.cwd(), 'autoresearch', 'fixtures', 'scoring', 'baseline.json') +} + +function compareValues(leftValue, operator, rightValue, tolerance = 0.000001) { + switch (operator) { + case 'gt': + return leftValue > rightValue + case 'gte': + return leftValue >= rightValue + case 'lt': + return leftValue < rightValue + case 'lte': + return leftValue <= rightValue + case 'eq': + return leftValue === rightValue + case 'approx': + return Math.abs(leftValue - rightValue) <= tolerance + default: + throw new Error(`Unsupported operator: ${operator}`) + } +} + +function evaluateAssertions(subject, assertions, contextLabel) { + const failures = [] + + for (const assertion of assertions) { + const leftValue = assertion.left === 'sum' + ? Object.values(subject).reduce((sum, value) => sum + (typeof value === 'number' ? value : 0), 0) + : subject[assertion.left] + const rightValue = Object.prototype.hasOwnProperty.call(assertion, 'right') + ? (typeof assertion.right === 'string' ? subject[assertion.right] : assertion.right) + : assertion.value + + if (!compareValues(leftValue, assertion.operator, rightValue, assertion.tolerance)) { + failures.push( + `${contextLabel}: expected ${assertion.left} ${assertion.operator} ${ + Object.prototype.hasOwnProperty.call(assertion, 'right') ? assertion.right : assertion.value + }`, + ) + } + } + + return failures +} + +function scoreSection(id, weight, failures) { + return { + id, + weight, + passed: failures.length === 0, + score: failures.length === 0 ? weight : 0, + failures, + } +} + +export async function evaluateScoringFixtureSet(fixturePath) { + const absoluteFixturePath = resolveFixturePath(fixturePath) + const fixture = JSON.parse(await readFile(absoluteFixturePath, 'utf8')) + + const adaptiveWeightsModule = await bundleAndImport( + path.resolve(process.cwd(), 'src', 'lib', 'scoring', 'adaptive-weights.ts'), + ) + const momentumModule = await bundleAndImport( + path.resolve(process.cwd(), 'src', 'lib', 'scoring', 'enhanced-momentum.ts'), + ) + const compositeModule = await bundleAndImport( + path.resolve(process.cwd(), 'src', 'lib', 'scoring', 'composite.ts'), + ) + + const sections = [] + const failures = [] + + const weightFailures = [] + for (const check of fixture.weights) { + const weights = adaptiveWeightsModule.getAdaptiveWeights( + check.input.category, + check.input.dataAgeDays, + check.input.dataCompleteness, + ) + weightFailures.push(...evaluateAssertions(weights, check.assertions, check.id)) + } + sections.push(scoreSection('weights', fixture.sectionWeights.weights, weightFailures)) + failures.push(...weightFailures) + + const momentumFailures = [] + for (const check of fixture.momentum) { + const analysis = momentumModule.analyzeMomentum(check.scores) + momentumFailures.push(...evaluateAssertions(analysis, check.assertions, check.id)) + } + sections.push(scoreSection('momentum', fixture.sectionWeights.momentum, momentumFailures)) + failures.push(...momentumFailures) + + const rankingFailures = [] + for (const check of fixture.rankings) { + const computed = check.candidates.map((candidate) => { + const weights = adaptiveWeightsModule.getAdaptiveWeights( + candidate.category, + candidate.dataAgeDays, + candidate.dataCompleteness, + ) + const result = compositeModule.computeCompositeScore(candidate.subScores, weights, candidate.category) + return { + id: candidate.id, + composite: result.composite, + } + }) + + const ranked = computed + .sort((left, right) => right.composite - left.composite) + .map((candidate) => candidate.id) + + if (JSON.stringify(ranked) !== JSON.stringify(check.expectedOrder)) { + rankingFailures.push( + `${check.id}: expected ${check.expectedOrder.join(' > ')}, got ${ranked.join(' > ')}`, + ) + } + } + sections.push(scoreSection('ranking', fixture.sectionWeights.ranking, rankingFailures)) + failures.push(...rankingFailures) + + const totalWeight = sections.reduce((sum, section) => sum + section.weight, 0) + const earned = sections.reduce((sum, section) => sum + section.score, 0) + + return { + track: 'scoring', + metric: Math.round((earned / totalWeight) * 10000) / 100, + failures, + sections, + } +} diff --git a/src/lib/scoring/adaptive-weights.ts b/src/lib/scoring/adaptive-weights.ts index 51ada75..90a9e73 100644 --- a/src/lib/scoring/adaptive-weights.ts +++ b/src/lib/scoring/adaptive-weights.ts @@ -64,10 +64,10 @@ export function getAdaptiveWeights( // Low completeness: reduce weight of dimensions more likely to be missing // (jobs data is often sparse for niche techs) if (dataCompleteness < 0.5) { - raw.jobs *= 0.8 - raw.ecosystem *= 0.9 - raw.github *= 1.1 - raw.community *= 1.1 + raw.jobs *= 0.75 + raw.ecosystem *= 0.85 + raw.github *= 1.12 + raw.community *= 1.12 if (isBlockchain && raw.onchain !== undefined) { raw.onchain *= 0.95 }