From 62835971b99e111882f147b88e19665e75cf4a9b Mon Sep 17 00:00:00 2001 From: ya-luotao Date: Sat, 10 Oct 2026 06:11:20 +0800 Subject: [PATCH 1/5] build: emit Node-compatible ESM and keep tests out of dist and the tarball The build used moduleResolution "bundler" with extensionless relative imports, so dist/index.js failed to load under Node ESM (ERR_MODULE_NOT_FOUND) and only worked through a bundler. Switch to NodeNext and add .js extensions. tsc also compiled *.test.ts into dist/ and failed on node:crypto for lack of @types/node. Split the emitting config into tsconfig.build.json (excludes tests), add @types/node, and add typecheck/format/check:package scripts with Prettier, publint and @arethetypeswrong/cli. package.json: list "types" first in exports, add "default", expose ./package.json, mark sideEffects false, exclude tests from "files", declare engines and publishConfig. Stop tracking .claude/settings.local.json. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01FGXjqn5CZbnVLNtHjkp45t --- .editorconfig | 12 + .gitignore | 15 + .node-version | 1 + .prettierignore | 5 + .prettierrc | 5 + package-lock.json | 2083 ++++++++++++++++++++++++-------------- package.json | 45 +- src/index.ts | 20 +- src/purrjungQuestions.ts | 14 +- src/questions.test.ts | 8 +- src/questions.ts | 778 ++++++++++++-- src/quickScoring.test.ts | 124 ++- src/scoring.test.ts | 6 +- tsconfig.build.json | 7 + tsconfig.json | 13 +- 15 files changed, 2192 insertions(+), 944 deletions(-) create mode 100644 .editorconfig create mode 100644 .node-version create mode 100644 .prettierignore create mode 100644 .prettierrc create mode 100644 tsconfig.build.json diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..8c52ff9 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,12 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_style = space +indent_size = 2 +insert_final_newline = true +trim_trailing_whitespace = true + +[*.md] +trim_trailing_whitespace = false diff --git a/.gitignore b/.gitignore index dd6e803..788ce30 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,19 @@ +# Dependencies node_modules/ + +# Build output dist/ +coverage/ +*.tgz + +# Logs *.log +npm-debug.log* + +# OS / editor .DS_Store +.idea/ +.vscode/ + +# Local-only tool state +.claude/settings.local.json diff --git a/.node-version b/.node-version new file mode 100644 index 0000000..a45fd52 --- /dev/null +++ b/.node-version @@ -0,0 +1 @@ +24 diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..6aa19a0 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,5 @@ +dist/ +coverage/ +node_modules/ +package-lock.json +CHANGELOG.md diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..789ac2e --- /dev/null +++ b/.prettierrc @@ -0,0 +1,5 @@ +{ + "printWidth": 100, + "singleQuote": true, + "trailingComma": "es5" +} diff --git a/package-lock.json b/package-lock.json index c53e319..fbfafac 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,150 +9,145 @@ "version": "1.0.1", "license": "MIT", "devDependencies": { - "typescript": "^5.7.0", + "@arethetypeswrong/cli": "^0.18.2", + "@types/node": "^24.0.0", + "prettier": "^3.6.0", + "publint": "^0.3.12", + "typescript": "^5.9.0", "vitest": "^4.0.16" - } - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.1.tgz", - "integrity": "sha512-HHB50pdsBX6k47S4u5g/CaLjqS3qwaOVE5ILsq64jyzgMhLuCuZ8rGzM9yhsAjfjkbgUPMzZEPa7DAp7yz6vuA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], + }, "engines": { "node": ">=18" } }, - "node_modules/@esbuild/android-arm": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.1.tgz", - "integrity": "sha512-kFqa6/UcaTbGm/NncN9kzVOODjhZW8e+FRdSeypWe6j33gzclHtwlANs26JrupOntlcWmB0u8+8HZo8s7thHvg==", - "cpu": [ - "arm" - ], + "node_modules/@andrewbranch/untar.js": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@andrewbranch/untar.js/-/untar.js-1.0.4.tgz", + "integrity": "sha512-pVXSwPsLuw8IGLo2Di0EaOfsk+ntVvpkk942J/sHYIkwvtKUakEcPh7HBgZ6tuimgzKSEHgCvO4XgQ05DEbwDw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@arethetypeswrong/cli": { + "version": "0.18.5", + "resolved": "https://registry.npmjs.org/@arethetypeswrong/cli/-/cli-0.18.5.tgz", + "integrity": "sha512-gM+8vRsQOD/Uc7EnBedUhkG5OCsDWE4uoak5QvomGpMpaky0Eh41p04nIMgrWb8EOmqZUJGc6zz9hsP6E56R7g==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "android" - ], + "dependencies": { + "@arethetypeswrong/core": "0.18.5", + "chalk": "^4.1.2", + "cli-table3": "^0.6.3", + "commander": "^10.0.1", + "marked": "^9.1.2", + "marked-terminal": "^7.1.0", + "semver": "^7.5.4" + }, + "bin": { + "attw": "dist/index.js" + }, "engines": { - "node": ">=18" + "node": ">=20" } }, - "node_modules/@esbuild/android-arm64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.1.tgz", - "integrity": "sha512-45fuKmAJpxnQWixOGCrS+ro4Uvb4Re9+UTieUY2f8AEc+t7d4AaZ6eUJ3Hva7dtrxAAWHtlEFsXFMAgNnGU9uQ==", - "cpu": [ - "arm64" - ], + "node_modules/@arethetypeswrong/core": { + "version": "0.18.5", + "resolved": "https://registry.npmjs.org/@arethetypeswrong/core/-/core-0.18.5.tgz", + "integrity": "sha512-9ytjzGwxjm9Uz7I9avfbt5vlQt6uk9uRRESzJjqrznl6WKvI6dwYTo+vJ3U02Wrq/mR3iql/PzhvHhKdJIAjDQ==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "android" - ], + "dependencies": { + "@andrewbranch/untar.js": "^1.0.3", + "@loaderkit/resolve": "^1.0.2", + "cjs-module-lexer": "^1.2.3", + "fflate": "^0.8.3", + "lru-cache": "^11.0.1", + "semver": "^7.5.4", + "typescript": "5.6.1-rc", + "validate-npm-package-name": "^5.0.0" + }, "engines": { - "node": ">=18" + "node": ">=20" } }, - "node_modules/@esbuild/android-x64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.1.tgz", - "integrity": "sha512-LBEpOz0BsgMEeHgenf5aqmn/lLNTFXVfoWMUox8CtWWYK9X4jmQzWjoGoNb8lmAYml/tQ/Ysvm8q7szu7BoxRQ==", - "cpu": [ - "x64" - ], + "node_modules/@arethetypeswrong/core/node_modules/typescript": { + "version": "5.6.1-rc", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.1-rc.tgz", + "integrity": "sha512-E3b2+1zEFu84jB0YQi9BORDjz9+jGbwwy1Zi3G0LUNw7a7cePUrHMRNy8aPh53nXpkFGVHSxIZo5vKTfYaFiBQ==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, "engines": { - "node": ">=18" + "node": ">=14.17" } }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.1.tgz", - "integrity": "sha512-veg7fL8eMSCVKL7IW4pxb54QERtedFDfY/ASrumK/SbFsXnRazxY4YykN/THYqFnFwJ0aVjiUrVG2PwcdAEqQQ==", - "cpu": [ - "arm64" - ], + "node_modules/@braidai/lang": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@braidai/lang/-/lang-1.1.2.tgz", + "integrity": "sha512-qBcknbBufNHlui137Hft8xauQMTZDKdophmLFv05r2eNmdIv/MlPuP4TdUknHG68UdWLgVZwgxVe735HzJNIwA==", + "dev": true, + "license": "ISC" + }, + "node_modules/@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", "dev": true, "license": "MIT", "optional": true, - "os": [ - "darwin" - ], "engines": { - "node": ">=18" + "node": ">=0.1.90" } }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.1.tgz", - "integrity": "sha512-+3ELd+nTzhfWb07Vol7EZ+5PTbJ/u74nC6iv4/lwIU99Ip5uuY6QoIf0Hn4m2HoV0qcnRivN3KSqc+FyCHjoVQ==", - "cpu": [ - "x64" - ], + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" + "license": "MIT" + }, + "node_modules/@loaderkit/resolve": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@loaderkit/resolve/-/resolve-1.0.6.tgz", + "integrity": "sha512-G8FdIoF5CypfwmD9rl8BXod5HDn8JqB0CCNBXDTaRZ+yRYhARrrSToX1zg1zy9jX3zLqigsELwhT4gNtkdQAUg==", + "dev": true, + "license": "ISC", + "dependencies": { + "@braidai/lang": "^1.0.0" } }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.1.tgz", - "integrity": "sha512-/8Rfgns4XD9XOSXlzUDepG8PX+AVWHliYlUkFI3K3GB6tqbdjYqdhcb4BKRd7C0BhZSoaCxhv8kTcBrcZWP+xg==", - "cpu": [ - "arm64" - ], + "node_modules/@oxc-project/types": { + "version": "0.153.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.153.0.tgz", + "integrity": "sha512-jSNrRIyvPVLL5E1z/tS5zLAprosGz/FuI1PWwEOz59j3E4TTTzSpqKb/RDGqJhhW0QHYISGXxq582wGx6T11Tw==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" + "funding": { + "url": "https://github.com/sponsors/oxc-project" } }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.1.tgz", - "integrity": "sha512-GITpD8dK9C+r+5yRT/UKVT36h/DQLOHdwGVwwoHidlnA168oD3uxA878XloXebK4Ul3gDBBIvEdL7go9gCUFzQ==", - "cpu": [ - "x64" - ], + "node_modules/@publint/pack": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/@publint/pack/-/pack-0.1.7.tgz", + "integrity": "sha512-4EDEmvxWtgsCnnVeBvtFIFZtUhPPt1+bA9JrSwU4Sa//6oKtzCSlGGXYJr44OD9aGISymbieJ4mCKHUygUDU+g==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], + "dependencies": { + "tinyexec": "^1.3.0" + }, "engines": { "node": ">=18" + }, + "funding": { + "url": "https://bjornlu.com/sponsor" } }, - "node_modules/@esbuild/linux-arm": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.1.tgz", - "integrity": "sha512-ieMID0JRZY/ZeCrsFQ3Y3NlHNCqIhTprJfDgSB3/lv5jJZ8FX3hqPyXWhe+gvS5ARMBJ242PM+VNz/ctNj//eA==", + "node_modules/@rolldown/binding-android-arm-eabi": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.13.tgz", + "integrity": "sha512-Vu/NICrgdRvUOeaJZIyB2FP63s6fO/4ud5l8dXmxDx9yDyn+mN0y+gsvsVviSli+T361Aqo0h46EuMoobrwawA==", "cpu": [ "arm" ], @@ -160,16 +155,16 @@ "license": "MIT", "optional": true, "os": [ - "linux" + "android" ], "engines": { - "node": ">=18" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.1.tgz", - "integrity": "sha512-W9//kCrh/6in9rWIBdKaMtuTTzNj6jSeG/haWBADqLLa9P8O5YSRDzgD5y9QBok4AYlzS6ARHifAb75V6G670Q==", + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.13.tgz", + "integrity": "sha512-6LEoIrGy5vvCd2boNnv2soPSUZj/PRvhp8R/t9KlX+mIZxThIE/ZqxrGXxmuNX0FuvQFvLdZr7S4HrYpYBVhEQ==", "cpu": [ "arm64" ], @@ -177,69 +172,69 @@ "license": "MIT", "optional": true, "os": [ - "linux" + "android" ], "engines": { - "node": ">=18" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.1.tgz", - "integrity": "sha512-VIUV4z8GD8rtSVMfAj1aXFahsi/+tcoXXNYmXgzISL+KB381vbSTNdeZHHHIYqFyXcoEhu9n5cT+05tRv13rlw==", + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.13.tgz", + "integrity": "sha512-CaJSRzlQ6Q+zkDMVA9OkVxBjVoPRCDEE3v0FSJ9kIQBgRjFApA3Si4ijhbNVUji7ZJt+PfO0e46qy4nPInHUPA==", "cpu": [ - "ia32" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "linux" + "darwin" ], "engines": { - "node": ">=18" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.1.tgz", - "integrity": "sha512-l4rfiiJRN7sTNI//ff65zJ9z8U+k6zcCg0LALU5iEWzY+a1mVZ8iWC1k5EsNKThZ7XCQ6YWtsZ8EWYm7r1UEsg==", + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.13.tgz", + "integrity": "sha512-e4r340I5hxSzJkD3YJBxgzq/rJ3wwxwdLLb2FPt1Zpyem20nrQPT11DDEEkazwxaHumH8wpglsp/Ozgj/wT6mw==", "cpu": [ - "loong64" + "x64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "linux" + "darwin" ], "engines": { - "node": ">=18" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.1.tgz", - "integrity": "sha512-U0bEuAOLvO/DWFdygTHWY8C067FXz+UbzKgxYhXC0fDieFa0kDIra1FAhsAARRJbvEyso8aAqvPdNxzWuStBnA==", + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.13.tgz", + "integrity": "sha512-hoBWDZ4/K/mJKVMbsgUd1jfpA2DiOrJ54mpegObU9TBHBBrtX4FLIXJJQRDPZ/2Dn91GvPob+Z2hwKJzf4E5SQ==", "cpu": [ - "mips64el" + "x64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "linux" + "freebsd" ], "engines": { - "node": ">=18" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.1.tgz", - "integrity": "sha512-NzdQ/Xwu6vPSf/GkdmRNsOfIeSGnh7muundsWItmBsVpMoNPVpM61qNzAVY3pZ1glzzAxLR40UyYM23eaDDbYQ==", + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.13.tgz", + "integrity": "sha512-oF9fZu3vTWnTiVAkDFxU9PJLt1nh20K9RRpuHsR7ZnE2L7XookBbDBVLojCVSLGbvtSlxeUNnjAj6DbNsKyzbg==", "cpu": [ - "ppc64" + "arm" ], "dev": true, "license": "MIT", @@ -248,15 +243,15 @@ "linux" ], "engines": { - "node": ">=18" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.1.tgz", - "integrity": "sha512-7zlw8p3IApcsN7mFw0O1Z1PyEk6PlKMu18roImfl3iQHTnr/yAfYv6s4hXPidbDoI2Q0pW+5xeoM4eTCC0UdrQ==", + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.13.tgz", + "integrity": "sha512-DTpb/+jMiqWMPgphn4ksW1zW+XLJ6dJJ9FMvnIk9I291LhCctMPhMYJrP5t9p6C9qa3KXadDdUEZHBaPh3kNxQ==", "cpu": [ - "riscv64" + "arm64" ], "dev": true, "license": "MIT", @@ -265,15 +260,15 @@ "linux" ], "engines": { - "node": ">=18" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.1.tgz", - "integrity": "sha512-cGj5wli+G+nkVQdZo3+7FDKC25Uh4ZVwOAK6A06Hsvgr8WqBBuOy/1s+PUEd/6Je+vjfm6stX0kmib5b/O2Ykw==", + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.13.tgz", + "integrity": "sha512-xU+Kvv9N2A3SY35zFh1FWWNjxbadE/BKeqnkXe7vScU4GZsw757BlVmDLITZEq8DPpk6m7uoDYkK7P+x/uU5Ow==", "cpu": [ - "s390x" + "arm64" ], "dev": true, "license": "MIT", @@ -282,15 +277,15 @@ "linux" ], "engines": { - "node": ">=18" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@esbuild/linux-x64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.1.tgz", - "integrity": "sha512-z3H/HYI9MM0HTv3hQZ81f+AKb+yEoCRlUby1F80vbQ5XdzEMyY/9iNlAmhqiBKw4MJXwfgsh7ERGEOhrM1niMA==", + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.13.tgz", + "integrity": "sha512-XMk/hnhh25oQ756Lclb1Q11h4ll0xC0Esa5w4Vzp+mAE2S3AGJgeM0SonrIgY8fHhjarb6hcoTuwoFG0VVP2ig==", "cpu": [ - "x64" + "ppc64" ], "dev": true, "license": "MIT", @@ -299,30 +294,30 @@ "linux" ], "engines": { - "node": ">=18" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.1.tgz", - "integrity": "sha512-wzC24DxAvk8Em01YmVXyjl96Mr+ecTPyOuADAvjGg+fyBpGmxmcr2E5ttf7Im8D0sXZihpxzO1isus8MdjMCXQ==", + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.13.tgz", + "integrity": "sha512-U1BoTHYmlywE2n3HUWjzZ+8EQsKr+OwpxYuDWaubx42tUL8QSL7nOqRz/NVHanibbFP3M1Rxuprz/wNcbMKjqw==", "cpu": [ - "arm64" + "s390x" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "netbsd" + "linux" ], "engines": { - "node": ">=18" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.1.tgz", - "integrity": "sha512-1YQ8ybGi2yIXswu6eNzJsrYIGFpnlzEWRl6iR5gMgmsrR0FcNoV1m9k9sc3PuP5rUBLshOZylc9nqSgymI+TYg==", + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.13.tgz", + "integrity": "sha512-J4cm80Pc6wEwV5AZ42o62sIQgDk+Y7R2s646jIwuvO+4V46QB58N4vVuMPWCBaNgursXLpmUWJV//WQAkPvHBQ==", "cpu": [ "x64" ], @@ -330,33 +325,16 @@ "license": "MIT", "optional": true, "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.1.tgz", - "integrity": "sha512-5Z+DzLCrq5wmU7RDaMDe2DVXMRm2tTDvX2KU14JJVBN2CT/qov7XVix85QoJqHltpvAOZUAc3ndU56HSMWrv8g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" + "linux" ], "engines": { - "node": ">=18" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.1.tgz", - "integrity": "sha512-Q73ENzIdPF5jap4wqLtsfh8YbYSZ8Q0wnxplOlZUOyZy7B4ZKW8DXGWgTCZmF8VWD7Tciwv5F4NsRf6vYlZtqg==", + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.13.tgz", + "integrity": "sha512-W0euZS40U4V6JkmEnVCIX9mcsL6cbXQ4X8c2w+MMOIjFg68TXUW+u3RT+2e1t1pv9mg/EP3pDtK3POXdQ7Rxyw==", "cpu": [ "x64" ], @@ -364,16 +342,16 @@ "license": "MIT", "optional": true, "os": [ - "openbsd" + "linux" ], "engines": { - "node": ">=18" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.1.tgz", - "integrity": "sha512-ajbHrGM/XiK+sXM0JzEbJAen+0E+JMQZ2l4RR4VFwvV9JEERx+oxtgkpoKv1SevhjavK2z2ReHk32pjzktWbGg==", + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.13.tgz", + "integrity": "sha512-do/AF/4n9MRshia3a5aXoy8xM2c6mgfT6D7H/dY6my5z+hfCIR4ab7mk4IRj++nsEQpKy39kd0DQnDq+r692tw==", "cpu": [ "arm64" ], @@ -384,30 +362,13 @@ "openharmony" ], "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.1.tgz", - "integrity": "sha512-IPUW+y4VIjuDVn+OMzHc5FV4GubIwPnsz6ubkvN8cuhEqH81NovB53IUlrlBkPMEPxvNnf79MGBoz8rZ2iW8HA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.1.tgz", - "integrity": "sha512-RIVRWiljWA6CdVu8zkWcRmGP7iRRIIwvhDKem8UMBjPql2TXM5PkDVvvrzMtj1V+WFPB4K7zkIGM7VzRtFkjdg==", + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.13.tgz", + "integrity": "sha512-6FtUf2cJZobutJdxnGYX8z0vtbgqU0b2v8vfBhkXb9wp2MppMFtkunE+IaOntyi01rkIQoPW2GHABtfzsC0BRQ==", "cpu": [ "arm64" ], @@ -418,15 +379,15 @@ "win32" ], "engines": { - "node": ">=18" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.1.tgz", - "integrity": "sha512-2BR5M8CPbptC1AK5JbJT1fWrHLvejwZidKx3UMSF0ecHMa+smhi16drIrCEggkgviBwLYd5nwrFLSl5Kho96RQ==", + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.13.tgz", + "integrity": "sha512-UKhFaZTeTcbKrb3iU82m17jVcPEAoqDkCN5MBqgPyZSwp/cQmib21crTS0DdH7T1B7wZRWEI89YQAOJMet5yhg==", "cpu": [ - "ia32" + "x64" ], "dev": true, "license": "MIT", @@ -435,620 +396,888 @@ "win32" ], "engines": { - "node": ">=18" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@esbuild/win32-x64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.1.tgz", - "integrity": "sha512-d5X6RMYv6taIymSk8JBP+nxv8DQAMY6A51GPgusqLdK9wBz5wWIXy1KjTck6HnjE9hqJzJRdk+1p/t5soSbCtw==", - "cpu": [ - "x64" - ], + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sindresorhus/is": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", + "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "win32" - ], "engines": { - "node": ">=18" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" } }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", "dev": true, "license": "MIT" }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.53.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.5.tgz", - "integrity": "sha512-iDGS/h7D8t7tvZ1t6+WPK04KD0MwzLZrG0se1hzBjSi5fyxlsiggoJHwh18PCFNn7tG43OWb6pdZ6Y+rMlmyNQ==", - "cpu": [ - "arm" - ], + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "android" - ] + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.53.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.5.tgz", - "integrity": "sha512-wrSAViWvZHBMMlWk6EJhvg8/rjxzyEhEdgfMMjREHEq11EtJ6IP6yfcCH57YAEca2Oe3FNCE9DSTgU70EIGmVw==", - "cpu": [ - "arm64" - ], + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] + "license": "MIT" }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.53.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.5.tgz", - "integrity": "sha512-S87zZPBmRO6u1YXQLwpveZm4JfPpAa6oHBX7/ghSiGH3rz/KDgAu1rKdGutV+WUI6tKDMbaBJomhnT30Y2t4VQ==", - "cpu": [ - "arm64" - ], + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] + "license": "MIT" }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.53.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.5.tgz", - "integrity": "sha512-YTbnsAaHo6VrAczISxgpTva8EkfQus0VPEVJCEaboHtZRIb6h6j0BNxRBOwnDciFTZLDPW5r+ZBmhL/+YpTZgA==", - "cpu": [ - "x64" - ], + "node_modules/@types/node": { + "version": "24.19.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.19.2.tgz", + "integrity": "sha512-93d49wCWJkCyN87MxLpNN6KXPCwFw7bVUg79mGUV1OS40JGWwVCr8n61DXCSqz7CV6/VfPkb7xec7uP2M+QHWQ==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] + "peer": true, + "dependencies": { + "undici-types": ">=7.24.0 <7.24.7" + } }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.53.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.5.tgz", - "integrity": "sha512-1T8eY2J8rKJWzaznV7zedfdhD1BqVs1iqILhmHDq/bqCUZsrMt+j8VCTHhP0vdfbHK3e1IQ7VYx3jlKqwlf+vw==", - "cpu": [ - "arm64" - ], + "node_modules/@vitest/expect": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.11.tgz", + "integrity": "sha512-VX2x5vNJXET47KAFzwERI+KRMtTTCSWTfSMKsW7JsUsXV4psq++e3DvZpuTDOpHcxytiDs6p2nhVb2tVDiiUYw==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.11", + "@vitest/utils": "4.1.11", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.53.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.5.tgz", - "integrity": "sha512-sHTiuXyBJApxRn+VFMaw1U+Qsz4kcNlxQ742snICYPrY+DDL8/ZbaC4DVIB7vgZmp3jiDaKA0WpBdP0aqPJoBQ==", - "cpu": [ - "x64" - ], + "node_modules/@vitest/mocker": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.11.tgz", + "integrity": "sha512-2XJVD55d1o5AZous5CCGKS74g/riOj9odEt2bQpCVZeblHyHdnMeFl4jl0XjU21stf4mbjUkew2eXQZt65g5CQ==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] + "dependencies": { + "@vitest/spy": "4.1.11", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.53.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.5.tgz", - "integrity": "sha512-dV3T9MyAf0w8zPVLVBptVlzaXxka6xg1f16VAQmjg+4KMSTWDvhimI/Y6mp8oHwNrmnmVl9XxJ/w/mO4uIQONA==", - "cpu": [ - "arm" - ], + "node_modules/@vitest/pretty-format": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.11.tgz", + "integrity": "sha512-yiZzPbGTS9Sr/JpFl8zHrcIkAofNbFV6k21vIgQN/cY/oxZeXhJv5sc/MBJ5jFKWmWs+oJHw0UXLZjmf931+Vw==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.53.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.5.tgz", - "integrity": "sha512-wIGYC1x/hyjP+KAu9+ewDI+fi5XSNiUi9Bvg6KGAh2TsNMA3tSEs+Sh6jJ/r4BV/bx/CyWu2ue9kDnIdRyafcQ==", - "cpu": [ - "arm" - ], + "node_modules/@vitest/runner": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.11.tgz", + "integrity": "sha512-LztvUgdwMNJMIkj3hQnnxiC2Xy1zNxq928W/xhjCLaNCzqTZOudjwbQf6v9IntZGPw132i2Lq2rgTRZHD3JHNw==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "dependencies": { + "@vitest/utils": "4.1.11", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.53.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.5.tgz", - "integrity": "sha512-Y+qVA0D9d0y2FRNiG9oM3Hut/DgODZbU9I8pLLPwAsU0tUKZ49cyV1tzmB/qRbSzGvY8lpgGkJuMyuhH7Ma+Vg==", - "cpu": [ - "arm64" - ], + "node_modules/@vitest/snapshot": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.11.tgz", + "integrity": "sha512-pN7ikn1ON7h8ee4gIAp4AzyK+zBtJPzVbqOgu5LCEh4VaJVbPQcgYQYJIMGQPXVeJJq1fnfazis7a5pFNPahog==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "dependencies": { + "@vitest/pretty-format": "4.1.11", + "@vitest/utils": "4.1.11", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.53.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.5.tgz", - "integrity": "sha512-juaC4bEgJsyFVfqhtGLz8mbopaWD+WeSOYr5E16y+1of6KQjc0BpwZLuxkClqY1i8sco+MdyoXPNiCkQou09+g==", - "cpu": [ - "arm64" - ], + "node_modules/@vitest/spy": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.11.tgz", + "integrity": "sha512-apNa/prQy2qCeywhnixOHPRCgGNhvg7T4Dapfl1GahLp/R+uhBm5cPyFoNVyqsNd2h1nJxL6BqqdIjiABL60YA==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "funding": { + "url": "https://opencollective.com/vitest" + } }, - "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.53.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.5.tgz", - "integrity": "sha512-rIEC0hZ17A42iXtHX+EPJVL/CakHo+tT7W0pbzdAGuWOt2jxDFh7A/lRhsNHBcqL4T36+UiAgwO8pbmn3dE8wA==", - "cpu": [ - "loong64" - ], + "node_modules/@vitest/utils": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.11.tgz", + "integrity": "sha512-zTCVGpyFsGWBhllOyKlTw/vnr6D9qxsfSDyfbyZmTyjHw5N/VuvzHpHoQjm2ZJzn4RJgx5w4r7V0er69CmLgPQ==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "dependencies": { + "@vitest/pretty-format": "4.1.11", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } }, - "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.53.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.5.tgz", - "integrity": "sha512-T7l409NhUE552RcAOcmJHj3xyZ2h7vMWzcwQI0hvn5tqHh3oSoclf9WgTl+0QqffWFG8MEVZZP1/OBglKZx52Q==", - "cpu": [ - "ppc64" - ], + "node_modules/ansi-escapes": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.3.0.tgz", + "integrity": "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "dependencies": { + "environment": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.53.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.5.tgz", - "integrity": "sha512-7OK5/GhxbnrMcxIFoYfhV/TkknarkYC1hqUw1wU2xUN3TVRLNT5FmBv4KkheSG2xZ6IEbRAhTooTV2+R5Tk0lQ==", - "cpu": [ - "riscv64" - ], + "node_modules/ansi-regex": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.4.0.tgz", + "integrity": "sha512-KzTVk2tCWAHtYrvvvaP8bJKJq2pVinhLcGEQdtLIYPbmNGNyYe8QwNaTUYQp2J7/vIsUKt5QCqAfUkYyG9DkOw==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.53.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.5.tgz", - "integrity": "sha512-GwuDBE/PsXaTa76lO5eLJTyr2k8QkPipAyOrs4V/KJufHCZBJ495VCGJol35grx9xryk4V+2zd3Ri+3v7NPh+w==", - "cpu": [ - "riscv64" - ], + "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", - "optional": true, - "os": [ - "linux" - ] + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.53.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.5.tgz", - "integrity": "sha512-IAE1Ziyr1qNfnmiQLHBURAD+eh/zH1pIeJjeShleII7Vj8kyEm2PF77o+lf3WTHDpNJcu4IXJxNO0Zluro8bOw==", - "cpu": [ - "s390x" - ], + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "license": "MIT" }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.53.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.5.tgz", - "integrity": "sha512-Pg6E+oP7GvZ4XwgRJBuSXZjcqpIW3yCBhK4BcsANvb47qMvAbCjR6E+1a/U2WXz1JJxp9/4Dno3/iSJLcm5auw==", - "cpu": [ - "x64" - ], + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": ">=12" + } }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.53.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.5.tgz", - "integrity": "sha512-txGtluxDKTxaMDzUduGP0wdfng24y1rygUMnmlUJ88fzCCULCLn7oE5kb2+tRB+MWq1QDZT6ObT5RrR8HFRKqg==", - "cpu": [ - "x64" - ], + "node_modules/chai": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.3.0.tgz", + "integrity": "sha512-XWAtwJ6OHO+tj0EKCs0Y2UamnyOxseZWltU4x2U2wh8g4AigdjwvtUjvLP2tqkA/avxHEtzxNaqGq/YGNwckKg==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": ">=18" + } }, - "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.53.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.5.tgz", - "integrity": "sha512-3DFiLPnTxiOQV993fMc+KO8zXHTcIjgaInrqlG8zDp1TlhYl6WgrOHuJkJQ6M8zHEcntSJsUp1XFZSY8C1DYbg==", - "cpu": [ - "arm64" - ], + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ] + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.53.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.5.tgz", - "integrity": "sha512-nggc/wPpNTgjGg75hu+Q/3i32R00Lq1B6N1DO7MCU340MRKL3WZJMjA9U4K4gzy3dkZPXm9E1Nc81FItBVGRlA==", - "cpu": [ - "arm64" - ], + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "win32" - ] + "engines": { + "node": ">=10" + } }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.53.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.5.tgz", - "integrity": "sha512-U/54pTbdQpPLBdEzCT6NBCFAfSZMvmjr0twhnD9f4EIvlm9wy3jjQ38yQj1AGznrNO65EWQMgm/QUjuIVrYF9w==", - "cpu": [ - "ia32" - ], + "node_modules/cjs-module-lexer": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", + "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] + "license": "MIT" }, - "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.53.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.5.tgz", - "integrity": "sha512-2NqKgZSuLH9SXBBV2dWNRCZmocgSOx8OJSdpRaEcRlIfX8YrKxUT6z0F1NpvDVhOsl190UFTRh2F2WDWWCYp3A==", - "cpu": [ - "x64" - ], + "node_modules/cli-highlight": { + "version": "2.1.11", + "resolved": "https://registry.npmjs.org/cli-highlight/-/cli-highlight-2.1.11.tgz", + "integrity": "sha512-9KDcoEVwyUXrjcJNvHD0NFc/hiwe/WPVYIleQh2O1N2Zro5gWJZ/K+3DGn8w8P/F6FxOgzyC5bxDyHIgCSPhGg==", + "dev": true, + "license": "ISC", + "dependencies": { + "chalk": "^4.0.0", + "highlight.js": "^10.7.1", + "mz": "^2.4.0", + "parse5": "^5.1.1", + "parse5-htmlparser2-tree-adapter": "^6.0.0", + "yargs": "^16.0.0" + }, + "bin": { + "highlight": "bin/highlight" + }, + "engines": { + "node": ">=8.0.0", + "npm": ">=5.0.0" + } + }, + "node_modules/cli-table3": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.5.tgz", + "integrity": "sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "win32" - ] + "dependencies": { + "string-width": "^4.2.0" + }, + "engines": { + "node": "10.* || >= 12.*" + }, + "optionalDependencies": { + "@colors/colors": "1.5.0" + } }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.53.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.5.tgz", - "integrity": "sha512-JRpZUhCfhZ4keB5v0fe02gQJy05GqboPOaxvjugW04RLSYYoB/9t2lx2u/tMs/Na/1NXfY8QYjgRljRpN+MjTQ==", - "cpu": [ - "x64" - ], + "node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/color-convert": { + "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", - "optional": true, - "os": [ - "win32" - ] + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } }, - "node_modules/@standard-schema/spec": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", - "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "node_modules/color-name": { + "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/@types/chai": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", - "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", "dev": true, "license": "MIT", - "dependencies": { - "@types/deep-eql": "*", - "assertion-error": "^2.0.1" + "engines": { + "node": ">=14" } }, - "node_modules/@types/deep-eql": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", - "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "dev": true, "license": "MIT" }, - "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "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==", "dev": true, "license": "MIT" }, - "node_modules/@vitest/expect": { - "version": "4.0.16", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.16.tgz", - "integrity": "sha512-eshqULT2It7McaJkQGLkPjPjNph+uevROGuIMJdG3V+0BSR2w9u6J9Lwu+E8cK5TETlfou8GRijhafIMhXsimA==", + "node_modules/emojilib": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/emojilib/-/emojilib-2.4.0.tgz", + "integrity": "sha512-5U0rVMU5Y2n2+ykNLQqMoqklN9ICBT/KsvC1Gz6vqHbz2AXXGkG+Pm5rMWk/8Vjrr/mY9985Hi8DYzn1F09Nyw==", + "dev": true, + "license": "MIT" + }, + "node_modules/environment": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", "dev": true, "license": "MIT", - "dependencies": { - "@standard-schema/spec": "^1.0.0", - "@types/chai": "^5.2.2", - "@vitest/spy": "4.0.16", - "@vitest/utils": "4.0.16", - "chai": "^6.2.1", - "tinyrainbow": "^3.0.3" + "engines": { + "node": ">=18" }, "funding": { - "url": "https://opencollective.com/vitest" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@vitest/mocker": { - "version": "4.0.16", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.16.tgz", - "integrity": "sha512-yb6k4AZxJTB+q9ycAvsoxGn+j/po0UaPgajllBgt1PzoMAAmJGYFdDk0uCcRcxb3BrME34I6u8gHZTQlkqSZpg==", + "node_modules/es-module-lexer": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.2.tgz", + "integrity": "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==", + "dev": true, + "license": "MIT" + }, + "node_modules/escalade": { + "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" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "4.0.16", - "estree-walker": "^3.0.3", - "magic-string": "^0.30.21" - }, - "funding": { - "url": "https://opencollective.com/vitest" + "@types/estree": "^1.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" }, "peerDependencies": { - "msw": "^2.4.9", - "vite": "^6.0.0 || ^7.0.0-0" + "picomatch": "^3 || ^4" }, "peerDependenciesMeta": { - "msw": { - "optional": true - }, - "vite": { + "picomatch": { "optional": true } } }, - "node_modules/@vitest/pretty-format": { - "version": "4.0.16", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.16.tgz", - "integrity": "sha512-eNCYNsSty9xJKi/UdVD8Ou16alu7AYiS2fCPRs0b1OdhJiV89buAXQLpTbe+X8V9L6qrs9CqyvU7OaAopJYPsA==", + "node_modules/fflate": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.3.tgz", + "integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==", + "dev": true, + "license": "MIT" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.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==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/highlight.js": { + "version": "10.7.3", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", + "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": "*" + } + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "dev": true, + "license": "MPL-2.0", "dependencies": { - "tinyrainbow": "^3.0.3" + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" }, "funding": { - "url": "https://opencollective.com/vitest" + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" } }, - "node_modules/@vitest/runner": { - "version": "4.0.16", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.16.tgz", - "integrity": "sha512-VWEDm5Wv9xEo80ctjORcTQRJ539EGPB3Pb9ApvVRAY1U/WkHXmmYISqU5E79uCwcW7xYUV38gwZD+RV755fu3Q==", + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/utils": "4.0.16", - "pathe": "^2.0.3" + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" }, "funding": { - "url": "https://opencollective.com/vitest" + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@vitest/snapshot": { - "version": "4.0.16", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.16.tgz", - "integrity": "sha512-sf6NcrYhYBsSYefxnry+DR8n3UV4xWZwWxYbCJUt2YdvtqzSPR7VfGrY0zsv090DAbjFZsi7ZaMi1KnSRyK1XA==", + "node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/pretty-format": "4.0.16", - "magic-string": "^0.30.21", - "pathe": "^2.0.3" + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" }, "funding": { - "url": "https://opencollective.com/vitest" + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@vitest/spy": { - "version": "4.0.16", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.16.tgz", - "integrity": "sha512-4jIOWjKP0ZUaEmJm00E0cOBLU+5WE0BpeNr3XN6TEF05ltro6NJqHWxXD0kA8/Zc8Nh23AT8WQxwNG+WeROupw==", + "node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT", + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, "funding": { - "url": "https://opencollective.com/vitest" + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@vitest/utils": { - "version": "4.0.16", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.16.tgz", - "integrity": "sha512-h8z9yYhV3e1LEfaQ3zdypIrnAg/9hguReGZoS7Gl0aBG5xgA410zBqECqmaF/+RkTggRsfnzc1XaAHA6bmUufA==", + "node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/pretty-format": "4.0.16", - "tinyrainbow": "^3.0.3" + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" }, "funding": { - "url": "https://opencollective.com/vitest" + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/assertion-error": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", - "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "node_modules/lru-cache": { + "version": "11.5.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.3.tgz", + "integrity": "sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg==", "dev": true, - "license": "MIT", + "license": "BlueOak-1.0.0", "engines": { - "node": ">=12" + "node": "20 || >=22" } }, - "node_modules/chai": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.1.tgz", - "integrity": "sha512-p4Z49OGG5W/WBCPSS/dH3jQ73kD6tiMmUM+bckNK6Jr5JHMG3k9bg/BvKR8lKmtVBKmOiuVaV2ws8s9oSbwysg==", + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", "dev": true, "license": "MIT", - "engines": { - "node": ">=18" + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" } }, - "node_modules/es-module-lexer": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", - "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", - "dev": true, - "license": "MIT" - }, - "node_modules/esbuild": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.1.tgz", - "integrity": "sha512-yY35KZckJJuVVPXpvjgxiCuVEJT67F6zDeVTv4rizyPrfGBUpZQsvmxnN+C371c2esD/hNMjj4tpBhuueLN7aA==", + "node_modules/marked": { + "version": "9.1.6", + "resolved": "https://registry.npmjs.org/marked/-/marked-9.1.6.tgz", + "integrity": "sha512-jcByLnIFkd5gSXZmjNvS1TlmRhCXZjIzHYlaGkPlLIekG55JDR2Z4va9tZwCiP+/RDERiNhMOFu01xd6O5ct1Q==", "dev": true, - "hasInstallScript": true, "license": "MIT", + "peer": true, "bin": { - "esbuild": "bin/esbuild" + "marked": "bin/marked.js" }, "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.1", - "@esbuild/android-arm": "0.27.1", - "@esbuild/android-arm64": "0.27.1", - "@esbuild/android-x64": "0.27.1", - "@esbuild/darwin-arm64": "0.27.1", - "@esbuild/darwin-x64": "0.27.1", - "@esbuild/freebsd-arm64": "0.27.1", - "@esbuild/freebsd-x64": "0.27.1", - "@esbuild/linux-arm": "0.27.1", - "@esbuild/linux-arm64": "0.27.1", - "@esbuild/linux-ia32": "0.27.1", - "@esbuild/linux-loong64": "0.27.1", - "@esbuild/linux-mips64el": "0.27.1", - "@esbuild/linux-ppc64": "0.27.1", - "@esbuild/linux-riscv64": "0.27.1", - "@esbuild/linux-s390x": "0.27.1", - "@esbuild/linux-x64": "0.27.1", - "@esbuild/netbsd-arm64": "0.27.1", - "@esbuild/netbsd-x64": "0.27.1", - "@esbuild/openbsd-arm64": "0.27.1", - "@esbuild/openbsd-x64": "0.27.1", - "@esbuild/openharmony-arm64": "0.27.1", - "@esbuild/sunos-x64": "0.27.1", - "@esbuild/win32-arm64": "0.27.1", - "@esbuild/win32-ia32": "0.27.1", - "@esbuild/win32-x64": "0.27.1" + "node": ">= 16" } }, - "node_modules/estree-walker": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", - "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "node_modules/marked-terminal": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/marked-terminal/-/marked-terminal-7.3.0.tgz", + "integrity": "sha512-t4rBvPsHc57uE/2nJOLmMbZCQ4tgAccAED3ngXQqW6g+TxA488JzJ+FK3lQkzBQOI1mRV/r/Kq+1ZlJ4D0owQw==", "dev": true, "license": "MIT", "dependencies": { - "@types/estree": "^1.0.0" - } - }, - "node_modules/expect-type": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", - "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", - "dev": true, - "license": "Apache-2.0", + "ansi-escapes": "^7.0.0", + "ansi-regex": "^6.1.0", + "chalk": "^5.4.1", + "cli-highlight": "^2.1.11", + "cli-table3": "^0.6.5", + "node-emoji": "^2.2.0", + "supports-hyperlinks": "^3.1.0" + }, "engines": { - "node": ">=12.0.0" + "node": ">=16.0.0" + }, + "peerDependencies": { + "marked": ">=1 <16" } }, - "node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "node_modules/marked-terminal/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", "dev": true, "license": "MIT", "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" + "node": "^12.17.0 || ^14.13 || >=16.0.0" }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "node_modules/mri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", + "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", "dev": true, - "hasInstallScript": true, "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + "node": ">=4" } }, - "node_modules/magic-string": { - "version": "0.30.21", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", - "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.5" + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" } }, "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "version": "3.3.20", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.20.tgz", + "integrity": "sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==", "dev": true, "funding": [ { @@ -1064,6 +1293,32 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/node-emoji": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-2.2.0.tgz", + "integrity": "sha512-Z3lTE9pLaJF47NyMhd4ww1yFTAP8YhYI8SleJiHzM46Fgpm5cnNzSl9XfzFNqbaz+VlJrIj3fXQ4DeN1Rjm6cw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/is": "^4.6.0", + "char-regex": "^1.0.2", + "emojilib": "^2.4.0", + "skin-tone": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/obug": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", @@ -1075,6 +1330,37 @@ ], "license": "MIT" }, + "node_modules/package-manager-detector": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.9.0.tgz", + "integrity": "sha512-zAMfbta9nPA8AKkA4WdmNAT8R8s6Dk0/xwPsqNnRidMFcLo1YkLuPDTgs2f64XdzS8vbLNnoJ+uUIkgbz7oRHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/parse5": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.1.tgz", + "integrity": "sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==", + "dev": true, + "license": "MIT" + }, + "node_modules/parse5-htmlparser2-tree-adapter": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz", + "integrity": "sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "parse5": "^6.0.1" + } + }, + "node_modules/parse5-htmlparser2-tree-adapter/node_modules/parse5": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", + "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==", + "dev": true, + "license": "MIT" + }, "node_modules/pathe": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", @@ -1090,11 +1376,12 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -1103,9 +1390,9 @@ } }, "node_modules/postcss": { - "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "version": "8.5.29", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.29.tgz", + "integrity": "sha512-49cGhUbXj8Qenv0iTMxA1cFBzxXoctpC9Ujd77t1WcbJIr6nF/eI7g/8MgxrYldFRuAXvja7xQRwavoW7kgrxQ==", "dev": true, "funding": [ { @@ -1123,54 +1410,120 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.11", + "nanoid": "^3.3.19", "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" + "source-map-js": "^1.2.2" }, "engines": { "node": "^10 || ^12 || >=14" } }, - "node_modules/rollup": { - "version": "4.53.5", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.5.tgz", - "integrity": "sha512-iTNAbFSlRpcHeeWu73ywU/8KuU/LZmNCSxp6fjQkJBD3ivUb8tpDrXhIxEzA05HlYMEwmtaUnb3RP+YNv162OQ==", + "node_modules/prettier": { + "version": "3.9.9", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.9.tgz", + "integrity": "sha512-Z/CJHIkdujO/OtN7nXUii0Rf3VT5SRuhjBA82Xvu2XhBUgX3nhP67T0LHceBdQLex7OOFGTox+Q5Yg8Jk2Qivg==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/publint": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/publint/-/publint-0.3.25.tgz", + "integrity": "sha512-WMMbn/d7JqRlfXQL/JUI4n5skC8cJ5WVr8/wT5mmiYWkUdENkZr1I5rgs2+dQPVRgoJd+JRYtB46MTsuDylw9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@publint/pack": "^0.1.7", + "package-manager-detector": "^1.8.0", + "picocolors": "^1.1.1", + "sade": "^1.8.1" + }, + "bin": { + "publint": "src/cli.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://bjornlu.com/sponsor" + } + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rolldown": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.13.tgz", + "integrity": "sha512-/5apOkwWxtACcQtZ5WlrAsKS4JwXVThQPJZ3RqIruRY9UG9M8e0VlXN068xum/JRKA4aIUoJ13ghe/kGCvidiQ==", "dev": true, "license": "MIT", "dependencies": { - "@types/estree": "1.0.8" + "@oxc-project/types": "=0.153.0", + "@rolldown/pluginutils": "^1.0.0" }, "bin": { - "rollup": "dist/bin/rollup" + "rolldown": "bin/cli.mjs" }, "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" + "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.53.5", - "@rollup/rollup-android-arm64": "4.53.5", - "@rollup/rollup-darwin-arm64": "4.53.5", - "@rollup/rollup-darwin-x64": "4.53.5", - "@rollup/rollup-freebsd-arm64": "4.53.5", - "@rollup/rollup-freebsd-x64": "4.53.5", - "@rollup/rollup-linux-arm-gnueabihf": "4.53.5", - "@rollup/rollup-linux-arm-musleabihf": "4.53.5", - "@rollup/rollup-linux-arm64-gnu": "4.53.5", - "@rollup/rollup-linux-arm64-musl": "4.53.5", - "@rollup/rollup-linux-loong64-gnu": "4.53.5", - "@rollup/rollup-linux-ppc64-gnu": "4.53.5", - "@rollup/rollup-linux-riscv64-gnu": "4.53.5", - "@rollup/rollup-linux-riscv64-musl": "4.53.5", - "@rollup/rollup-linux-s390x-gnu": "4.53.5", - "@rollup/rollup-linux-x64-gnu": "4.53.5", - "@rollup/rollup-linux-x64-musl": "4.53.5", - "@rollup/rollup-openharmony-arm64": "4.53.5", - "@rollup/rollup-win32-arm64-msvc": "4.53.5", - "@rollup/rollup-win32-ia32-msvc": "4.53.5", - "@rollup/rollup-win32-x64-gnu": "4.53.5", - "@rollup/rollup-win32-x64-msvc": "4.53.5", - "fsevents": "~2.3.2" + "@rolldown/binding-android-arm-eabi": "1.2.13", + "@rolldown/binding-android-arm64": "1.2.13", + "@rolldown/binding-darwin-arm64": "1.2.13", + "@rolldown/binding-darwin-x64": "1.2.13", + "@rolldown/binding-freebsd-x64": "1.2.13", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.13", + "@rolldown/binding-linux-arm64-gnu": "1.2.13", + "@rolldown/binding-linux-arm64-musl": "1.2.13", + "@rolldown/binding-linux-ppc64-gnu": "1.2.13", + "@rolldown/binding-linux-s390x-gnu": "1.2.13", + "@rolldown/binding-linux-x64-gnu": "1.2.13", + "@rolldown/binding-linux-x64-musl": "1.2.13", + "@rolldown/binding-openharmony-arm64": "1.2.13", + "@rolldown/binding-win32-arm64-msvc": "1.2.13", + "@rolldown/binding-win32-x64-msvc": "1.2.13" + } + }, + "node_modules/sade": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", + "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "mri": "^1.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" } }, "node_modules/siginfo": { @@ -1180,10 +1533,23 @@ "dev": true, "license": "ISC" }, + "node_modules/skin-tone": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/skin-tone/-/skin-tone-2.0.0.tgz", + "integrity": "sha512-kUMbT1oBJCpgrnKoSr0o6wPtvRWT9W9UKvGLwfJYO2WuahZRHOpEyL1ckyMGgMWh0UdpmaoFqKKD29WTomNEGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "unicode-emoji-modifier-base": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz", + "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==", "dev": true, "license": "BSD-3-Clause", "engines": { @@ -1198,12 +1564,103 @@ "license": "MIT" }, "node_modules/std-env": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", - "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.3.0.tgz", + "integrity": "sha512-OtU/EgQ1kIm5KwqQpBC6ZEMXrZRui11w8zgfTWp8cdO9B8OaPsbA8bTHO2P+HNo1VlUTGMVBwPhydu6poeXiag==", "dev": true, "license": "MIT" }, + "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==", + "dev": true, + "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/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi/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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-hyperlinks": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-3.2.0.tgz", + "integrity": "sha512-zFObLMyZeEwzAoKCyu1B91U79K2t7ApXuQfo8OuxwXLDgcKxuwM+YvcbIhm6QWqz7mHUH1TVytR1PwVVjEuMig==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0", + "supports-color": "^7.0.0" + }, + "engines": { + "node": ">=14.18" + }, + "funding": { + "url": "https://github.com/chalk/supports-hyperlinks?sponsor=1" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/tinybench": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", @@ -1212,9 +1669,9 @@ "license": "MIT" }, "node_modules/tinyexec": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", - "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.1.tgz", + "integrity": "sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA==", "dev": true, "license": "MIT", "engines": { @@ -1222,14 +1679,14 @@ } }, "node_modules/tinyglobby": { - "version": "0.2.15", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", - "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", "dev": true, "license": "MIT", "dependencies": { "fdir": "^6.5.0", - "picomatch": "^4.0.3" + "picomatch": "^4.0.4" }, "engines": { "node": ">=12.0.0" @@ -1239,9 +1696,9 @@ } }, "node_modules/tinyrainbow": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.0.3.tgz", - "integrity": "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.2.0.tgz", + "integrity": "sha512-LgO3D9yZJjApUiuUfl9iFAwrtaX4+lok3wJIqttGoKCHlWUqHqbQpnxCf82L8FjgKsh4iGo78hqJwgL8F6To2A==", "dev": true, "license": "MIT", "engines": { @@ -1262,19 +1719,46 @@ "node": ">=14.17" } }, + "node_modules/undici-types": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", + "dev": true, + "license": "MIT" + }, + "node_modules/unicode-emoji-modifier-base": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unicode-emoji-modifier-base/-/unicode-emoji-modifier-base-1.0.0.tgz", + "integrity": "sha512-yLSH4py7oFH3oG/9K+XWrz1pSi3dfUrWEnInbxMfArOfc1+33BlGPQtLsOYwvdMy11AwUBetYuaRxSPqgkq+8g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/validate-npm-package-name": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-5.0.1.tgz", + "integrity": "sha512-OljLrQ9SQdOUqTaQxqL5dEfZWrXExyyWsozYlAWFawPVNuD83igl7uJD2RTkNMbniIYgt8l81eCJGIdQF7avLQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, "node_modules/vite": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.0.tgz", - "integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==", + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.3.4.tgz", + "integrity": "sha512-IftLE8phJhdNRIGNEV4JCCvfNScoZQVfcXaN8TQropWGJtOHcu+iAB9pHcuGvmEb/kddw0LBe/jS8ztdC6xdiQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { - "esbuild": "^0.27.0", - "fdir": "^6.5.0", - "picomatch": "^4.0.3", - "postcss": "^8.5.6", - "rollup": "^4.43.0", - "tinyglobby": "^0.2.15" + "lightningcss": "^1.33.0", + "picomatch": "^4.0.7", + "postcss": "^8.5.29", + "rolldown": "~1.2.12", + "tinyglobby": "^0.2.17" }, "bin": { "vite": "bin/vite.js" @@ -1290,9 +1774,10 @@ }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.7.1", + "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", - "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", @@ -1305,13 +1790,16 @@ "@types/node": { "optional": true }, - "jiti": { + "@vitejs/devtools": { "optional": true }, - "less": { + "esbuild": { + "optional": true + }, + "jiti": { "optional": true }, - "lightningcss": { + "less": { "optional": true }, "sass": { @@ -1338,31 +1826,31 @@ } }, "node_modules/vitest": { - "version": "4.0.16", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.16.tgz", - "integrity": "sha512-E4t7DJ9pESL6E3I8nFjPa4xGUd3PmiWDLsDztS2qXSJWfHtbQnwAWylaBvSNY48I3vr8PTqIZlyK8TE3V3CA4Q==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.11.tgz", + "integrity": "sha512-fhACrNXUidIbGSBr5FlbuBkO7VWC1ZyLl0DO4CU2DrQoAPxX84Ysxs+HeGQpii5lZWV1Q4gBZTTu49mF+A6Edw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/expect": "4.0.16", - "@vitest/mocker": "4.0.16", - "@vitest/pretty-format": "4.0.16", - "@vitest/runner": "4.0.16", - "@vitest/snapshot": "4.0.16", - "@vitest/spy": "4.0.16", - "@vitest/utils": "4.0.16", - "es-module-lexer": "^1.7.0", - "expect-type": "^1.2.2", + "@vitest/expect": "4.1.11", + "@vitest/mocker": "4.1.11", + "@vitest/pretty-format": "4.1.11", + "@vitest/runner": "4.1.11", + "@vitest/snapshot": "4.1.11", + "@vitest/spy": "4.1.11", + "@vitest/utils": "4.1.11", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", - "std-env": "^3.10.0", + "std-env": "^4.0.0-rc.1", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", - "tinyrainbow": "^3.0.3", - "vite": "^6.0.0 || ^7.0.0", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", "why-is-node-running": "^2.3.0" }, "bin": { @@ -1378,12 +1866,15 @@ "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", - "@vitest/browser-playwright": "4.0.16", - "@vitest/browser-preview": "4.0.16", - "@vitest/browser-webdriverio": "4.0.16", - "@vitest/ui": "4.0.16", + "@vitest/browser-playwright": "4.1.11", + "@vitest/browser-preview": "4.1.11", + "@vitest/browser-webdriverio": "4.1.11", + "@vitest/coverage-istanbul": "4.1.11", + "@vitest/coverage-v8": "4.1.11", + "@vitest/ui": "4.1.11", "happy-dom": "*", - "jsdom": "*" + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "@edge-runtime/vm": { @@ -1404,6 +1895,12 @@ "@vitest/browser-webdriverio": { "optional": true }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, "@vitest/ui": { "optional": true }, @@ -1412,6 +1909,9 @@ }, "jsdom": { "optional": true + }, + "vite": { + "optional": false } } }, @@ -1431,6 +1931,63 @@ "engines": { "node": ">=8" } + }, + "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==", + "dev": true, + "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/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "16.2.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.2.tgz", + "integrity": "sha512-Nt9ZJjXTv5R8MHbqby/wXQ6Gi0Bb3TcYZkR1bzuL4yB2OxWPkXknz513gEF0GoA6tn00UpbPvERW8rzCuWCA6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } } } } diff --git a/package.json b/package.json index c4e5e01..82b548d 100644 --- a/package.json +++ b/package.json @@ -1,39 +1,56 @@ { "name": "@openjung/core", "version": "1.0.1", - "description": "Core OEJTS (Open Extended Jungian Type Scales) MBTI personality test questions and scoring logic", + "description": "Questions, scoring and i18n for the Open Extended Jungian Type Scales (OEJTS) personality test. Zero dependencies, TypeScript, 44 locales.", "type": "module", "main": "./dist/index.js", "module": "./dist/index.js", "types": "./dist/index.d.ts", "exports": { ".": { + "types": "./dist/index.d.ts", "import": "./dist/index.js", - "types": "./dist/index.d.ts" - } + "default": "./dist/index.js" + }, + "./package.json": "./package.json" }, + "sideEffects": false, "files": [ "dist", "src", - "TRANSLATIONS.md" + "!src/**/*.test.ts", + "TRANSLATIONS.md", + "LICENSE-DATA" ], + "engines": { + "node": ">=18" + }, "scripts": { - "build": "tsc", - "dev": "tsc --watch", + "build": "tsc -p tsconfig.build.json", + "clean": "rm -rf dist", + "dev": "tsc -p tsconfig.build.json --watch", + "typecheck": "tsc --noEmit", + "format": "prettier --write .", + "format:check": "prettier --check .", "test": "vitest run", "test:watch": "vitest", "test:coverage": "vitest run --coverage", - "prepublishOnly": "npm run build" + "check:package": "publint && attw --pack . --profile esm-only", + "check": "npm run typecheck && npm run format:check && npm test && npm run build && npm run check:package", + "prepublishOnly": "npm run clean && npm run check" }, "keywords": [ "mbti", "oejts", "openjung", "personality", + "personality-test", "psychology", - "test", + "psychometrics", "jung", - "jungian" + "jungian", + "i18n", + "typescript" ], "author": "ya-luotao ", "license": "MIT", @@ -45,8 +62,16 @@ "url": "https://github.com/openjung/core/issues" }, "homepage": "https://openjung.org", + "publishConfig": { + "access": "public", + "provenance": true + }, "devDependencies": { - "typescript": "^5.7.0", + "@arethetypeswrong/cli": "^0.18.2", + "@types/node": "^24.0.0", + "prettier": "^3.6.0", + "publint": "^0.3.12", + "typescript": "^5.9.0", "vitest": "^4.0.16" } } diff --git a/src/index.ts b/src/index.ts index 1531ea4..a752b4a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,13 +9,14 @@ export type { BilingualText, QuestionPair, DimensionQuestions, + MultilingualText, // Test Quality Metrics types ConfidenceLevel, DimensionConfidence, TestConfidence, ConsistencyResult, TestConsistency, -} from './types'; +} from './types.js'; // Questions data export { @@ -29,7 +30,7 @@ export { quickTestQuestions, QUICK_TEST_TOTAL, QUICK_TEST_PER_DIMENSION, -} from './questions'; +} from './questions.js'; // Scoring functions export { @@ -62,7 +63,16 @@ export { checkDimensionConsistency, checkTestConsistency, getConfidenceLabel, -} from './scoring'; +} from './scoring.js'; + +// Locales +export type { Locale } from './locales.js'; +export { + SUPPORTED_LOCALES, + PURRJUNG_LOCALES, + DEFAULT_LOCALE, + getLocalizedText, +} from './locales.js'; // PurrJung Cat Test - Questions data export { @@ -71,7 +81,7 @@ export { sortedPurrjungQuestions, PURRJUNG_TOTAL_QUESTIONS, PURRJUNG_QUESTIONS_PER_DIMENSION, -} from './purrjungQuestions'; +} from './purrjungQuestions.js'; // PurrJung Cat Test - Scoring functions export { @@ -84,4 +94,4 @@ export { generatePurrjungResult, isPurrjungTestComplete, getPurrjungDimensionQuestionIds, -} from './purrjungScoring'; +} from './purrjungScoring.js'; diff --git a/src/purrjungQuestions.ts b/src/purrjungQuestions.ts index f615a42..58d5335 100644 --- a/src/purrjungQuestions.ts +++ b/src/purrjungQuestions.ts @@ -1,4 +1,4 @@ -import type { QuestionPair, DimensionQuestions } from './types'; +import type { QuestionPair, DimensionQuestions } from './types.js'; /** * PurrJung Cat Personality Test - 16 Questions @@ -196,7 +196,7 @@ export const purrjungQuestions: QuestionPair[] = [ id: 8, dimension: 'SN', title: { - en: 'Your cat\'s preferred resting spots are...', + en: "Your cat's preferred resting spots are...", zh: '你的猫喜欢的休息地点是...', ja: 'あなたの猫の好みの休憩場所は...', ko: '당신의 고양이가 선호하는 휴식 장소는...', @@ -273,7 +273,7 @@ export const purrjungQuestions: QuestionPair[] = [ id: 11, dimension: 'TF', title: { - en: 'Your cat\'s response to verbal praise or encouragement is...', + en: "Your cat's response to verbal praise or encouragement is...", zh: '你的猫对言语表扬或鼓励的反应是...', ja: '言葉による称賛や励ましに対するあなたの猫の反応は...', ko: '칭찬이나 격려에 대한 당신의 고양이의 반응은...', @@ -298,7 +298,7 @@ export const purrjungQuestions: QuestionPair[] = [ id: 12, dimension: 'TF', title: { - en: 'When you call your cat\'s name, it typically...', + en: "When you call your cat's name, it typically...", zh: '当你叫你的猫的名字时,它通常...', ja: '猫の名前を呼ぶと、通常...', ko: '고양이 이름을 부르면, 보통...', @@ -325,7 +325,7 @@ export const purrjungQuestions: QuestionPair[] = [ id: 13, dimension: 'JP', title: { - en: 'Your cat\'s daily activity pattern is...', + en: "Your cat's daily activity pattern is...", zh: '你的猫的日常活动模式是...', ja: 'あなたの猫の毎日の活動パターンは...', ko: '당신의 고양이의 일상 활동 패턴은...', @@ -375,7 +375,7 @@ export const purrjungQuestions: QuestionPair[] = [ id: 15, dimension: 'JP', title: { - en: 'Your cat\'s energy bursts (zoomies) occur...', + en: "Your cat's energy bursts (zoomies) occur...", zh: '你的猫的能量爆发(疯跑)发生在...', ja: 'あなたの猫のエネルギー爆発(ズーミーズ)は...', ko: '당신의 고양이의 에너지 폭발(줌미)은...', @@ -400,7 +400,7 @@ export const purrjungQuestions: QuestionPair[] = [ id: 16, dimension: 'JP', title: { - en: 'When it\'s time for grooming or nail trimming, your cat...', + en: "When it's time for grooming or nail trimming, your cat...", zh: '当需要梳毛或剪指甲时,你的猫...', ja: 'グルーミングや爪切りの時間になると、あなたの猫は...', ko: '그루밍이나 발톱 다듬기 시간이 되면, 당신의 고양이는...', diff --git a/src/questions.test.ts b/src/questions.test.ts index 663741d..d12b89c 100644 --- a/src/questions.test.ts +++ b/src/questions.test.ts @@ -6,10 +6,10 @@ import { dimensionQuestions, TOTAL_QUESTIONS, QUESTIONS_PER_DIMENSION, -} from './questions'; -import { questionTranslations } from './questionTranslations'; -import { generateResult } from './scoring'; -import type { Dimension } from './types'; +} from './questions.js'; +import { questionTranslations } from './questionTranslations.js'; +import { generateResult } from './scoring.js'; +import type { Dimension } from './types.js'; // Independent release contract, not derived from the data being checked. const originalLanguages = ['en', 'zh', 'ja', 'ko', 'zh-tw']; diff --git a/src/questions.ts b/src/questions.ts index 1c6dec0..3e27f9a 100644 --- a/src/questions.ts +++ b/src/questions.ts @@ -1,5 +1,5 @@ -import type { QuestionPair, DimensionQuestions } from './types'; -import { questionTranslations } from './questionTranslations'; +import type { QuestionPair, DimensionQuestions } from './types.js'; +import { questionTranslations } from './questionTranslations.js'; /** * OEJTS 1.2 - 32 Questions @@ -15,232 +15,802 @@ export const questions: QuestionPair[] = [ { id: 1, dimension: 'JP', - title: { en: 'How do you track tasks?', zh: '你如何记录任务?', ja: 'タスクをどう管理する?', ko: '할 일을 어떻게 기록하나요?', 'zh-tw': '你如何記錄任務?' }, - leftTrait: { en: 'Makes lists', zh: '制定清单', ja: 'リストを作る', ko: '목록을 만든다', 'zh-tw': '制定清單' }, - rightTrait: { en: 'Relies on memory', zh: '依靠记忆', ja: '記憶に頼る', ko: '기억에 의존한다', 'zh-tw': '依靠記憶' }, + title: { + en: 'How do you track tasks?', + zh: '你如何记录任务?', + ja: 'タスクをどう管理する?', + ko: '할 일을 어떻게 기록하나요?', + 'zh-tw': '你如何記錄任務?', + }, + leftTrait: { + en: 'Makes lists', + zh: '制定清单', + ja: 'リストを作る', + ko: '목록을 만든다', + 'zh-tw': '制定清單', + }, + rightTrait: { + en: 'Relies on memory', + zh: '依靠记忆', + ja: '記憶に頼る', + ko: '기억에 의존한다', + 'zh-tw': '依靠記憶', + }, }, { id: 5, dimension: 'JP', - title: { en: 'How do you organize your space?', zh: '你如何整理空间?', ja: '身の回りの片づけは?', ko: '공간을 어떻게 정리하나요?', 'zh-tw': '你如何整理空間?' }, - leftTrait: { en: 'Keeps a clean room', zh: '保持房间整洁', ja: '部屋をきれいに保つ', ko: '방을 깨끗이 유지한다', 'zh-tw': '保持房間整潔' }, - rightTrait: { en: 'Just puts stuff wherever', zh: '随手放置东西', ja: '物はその辺に置きがち', ko: '물건을 아무데나 둔다', 'zh-tw': '隨手放置東西' }, + title: { + en: 'How do you organize your space?', + zh: '你如何整理空间?', + ja: '身の回りの片づけは?', + ko: '공간을 어떻게 정리하나요?', + 'zh-tw': '你如何整理空間?', + }, + leftTrait: { + en: 'Keeps a clean room', + zh: '保持房间整洁', + ja: '部屋をきれいに保つ', + ko: '방을 깨끗이 유지한다', + 'zh-tw': '保持房間整潔', + }, + rightTrait: { + en: 'Just puts stuff wherever', + zh: '随手放置东西', + ja: '物はその辺に置きがち', + ko: '물건을 아무데나 둔다', + 'zh-tw': '隨手放置東西', + }, }, { id: 9, dimension: 'JP', - title: { en: 'How would you describe your lifestyle?', zh: '你如何形容自己的生活方式?', ja: 'あなたの生活スタイルは?', ko: '당신의 생활 방식은?', 'zh-tw': '你如何形容自己的生活方式?' }, - leftTrait: { en: 'Organized', zh: '有条理', ja: '整理整頓されている', ko: '체계적이다', 'zh-tw': '有條理' }, - rightTrait: { en: 'Chaotic', zh: '随性混乱', ja: '成り行きまかせ', ko: '혼란스럽다', 'zh-tw': '隨性混亂' }, + title: { + en: 'How would you describe your lifestyle?', + zh: '你如何形容自己的生活方式?', + ja: 'あなたの生活スタイルは?', + ko: '당신의 생활 방식은?', + 'zh-tw': '你如何形容自己的生活方式?', + }, + leftTrait: { + en: 'Organized', + zh: '有条理', + ja: '整理整頓されている', + ko: '체계적이다', + 'zh-tw': '有條理', + }, + rightTrait: { + en: 'Chaotic', + zh: '随性混乱', + ja: '成り行きまかせ', + ko: '혼란스럽다', + 'zh-tw': '隨性混亂', + }, }, { id: 13, dimension: 'JP', - title: { en: 'When do you make plans?', zh: '你何时制定计划?', ja: 'いつ計画を立てる?', ko: '언제 계획을 세우나요?', 'zh-tw': '你何時制定計劃?' }, - leftTrait: { en: 'Plans far ahead', zh: '提前规划', ja: 'かなり前から計画する', ko: '미리 계획한다', 'zh-tw': '提前規劃' }, - rightTrait: { en: 'Plans at the last minute', zh: '最后一刻才计划', ja: '直前に計画する', ko: '마지막에 계획한다', 'zh-tw': '最後一刻才計劃' }, + title: { + en: 'When do you make plans?', + zh: '你何时制定计划?', + ja: 'いつ計画を立てる?', + ko: '언제 계획을 세우나요?', + 'zh-tw': '你何時制定計劃?', + }, + leftTrait: { + en: 'Plans far ahead', + zh: '提前规划', + ja: 'かなり前から計画する', + ko: '미리 계획한다', + 'zh-tw': '提前規劃', + }, + rightTrait: { + en: 'Plans at the last minute', + zh: '最后一刻才计划', + ja: '直前に計画する', + ko: '마지막에 계획한다', + 'zh-tw': '最後一刻才計劃', + }, }, { id: 17, dimension: 'JP', - title: { en: 'How do you approach decisions?', zh: '你如何做决定?', ja: '決めるときは?', ko: '결정을 어떻게 내리나요?', 'zh-tw': '你如何做決定?' }, - leftTrait: { en: 'Commits', zh: '做出承诺', ja: 'きっぱり決める', ko: '약속한다', 'zh-tw': '做出承諾' }, - rightTrait: { en: 'Keeps options open', zh: '保留选择', ja: '選択肢を残しておく', ko: '선택지를 열어둔다', 'zh-tw': '保留選擇' }, + title: { + en: 'How do you approach decisions?', + zh: '你如何做决定?', + ja: '決めるときは?', + ko: '결정을 어떻게 내리나요?', + 'zh-tw': '你如何做決定?', + }, + leftTrait: { + en: 'Commits', + zh: '做出承诺', + ja: 'きっぱり決める', + ko: '약속한다', + 'zh-tw': '做出承諾', + }, + rightTrait: { + en: 'Keeps options open', + zh: '保留选择', + ja: '選択肢を残しておく', + ko: '선택지를 열어둔다', + 'zh-tw': '保留選擇', + }, }, { id: 21, dimension: 'JP', - title: { en: 'When do you complete tasks?', zh: '你何时完成任务?', ja: 'やるべきことは?', ko: '언제 일을 끝내나요?', 'zh-tw': '你何時完成任務?' }, - leftTrait: { en: 'Gets work done right away', zh: '立即完成工作', ja: 'すぐに片づける', ko: '일을 바로 끝낸다', 'zh-tw': '立即完成工作' }, - rightTrait: { en: 'Procrastinates', zh: '拖延', ja: '先延ばしにする', ko: '미룬다', 'zh-tw': '拖延' }, + title: { + en: 'When do you complete tasks?', + zh: '你何时完成任务?', + ja: 'やるべきことは?', + ko: '언제 일을 끝내나요?', + 'zh-tw': '你何時完成任務?', + }, + leftTrait: { + en: 'Gets work done right away', + zh: '立即完成工作', + ja: 'すぐに片づける', + ko: '일을 바로 끝낸다', + 'zh-tw': '立即完成工作', + }, + rightTrait: { + en: 'Procrastinates', + zh: '拖延', + ja: '先延ばしにする', + ko: '미룬다', + 'zh-tw': '拖延', + }, }, { id: 25, dimension: 'JP', - title: { en: 'How do you handle new situations?', zh: '你如何应对新情况?', ja: '初めての状況では?', ko: '새로운 상황에 어떻게 대처하나요?', 'zh-tw': '你如何應對新情況?' }, - leftTrait: { en: 'Prepares', zh: '提前准备', ja: '準備する', ko: '준비한다', 'zh-tw': '提前準備' }, - rightTrait: { en: 'Improvises', zh: '即兴发挥', ja: 'その場で対応する', ko: '즉흥적으로 한다', 'zh-tw': '即興發揮' }, + title: { + en: 'How do you handle new situations?', + zh: '你如何应对新情况?', + ja: '初めての状況では?', + ko: '새로운 상황에 어떻게 대처하나요?', + 'zh-tw': '你如何應對新情況?', + }, + leftTrait: { + en: 'Prepares', + zh: '提前准备', + ja: '準備する', + ko: '준비한다', + 'zh-tw': '提前準備', + }, + rightTrait: { + en: 'Improvises', + zh: '即兴发挥', + ja: 'その場で対応する', + ko: '즉흥적으로 한다', + 'zh-tw': '即興發揮', + }, }, { id: 29, dimension: 'JP', - title: { en: 'What drives you more?', zh: '什么更能驱动你?', ja: 'どちらに力を注ぐ?', ko: '무엇이 당신을 움직이나요?', 'zh-tw': '什麼更能驅動你?' }, - leftTrait: { en: 'Works hard', zh: '努力工作', ja: 'とことん頑張る', ko: '열심히 일한다', 'zh-tw': '努力工作' }, - rightTrait: { en: 'Plays hard', zh: '尽情玩乐', ja: '思い切り遊ぶ', ko: '열심히 논다', 'zh-tw': '盡情玩樂' }, + title: { + en: 'What drives you more?', + zh: '什么更能驱动你?', + ja: 'どちらに力を注ぐ?', + ko: '무엇이 당신을 움직이나요?', + 'zh-tw': '什麼更能驅動你?', + }, + leftTrait: { + en: 'Works hard', + zh: '努力工作', + ja: 'とことん頑張る', + ko: '열심히 일한다', + 'zh-tw': '努力工作', + }, + rightTrait: { + en: 'Plays hard', + zh: '尽情玩乐', + ja: '思い切り遊ぶ', + ko: '열심히 논다', + 'zh-tw': '盡情玩樂', + }, }, // TF dimension (Questions 2, 6, 10, 14, 18, 22, 26, 30) { id: 2, dimension: 'TF', - title: { en: 'How do you approach new information?', zh: '你如何看待新信息?', ja: '新しい情報に対しては?', ko: '새로운 정보를 어떻게 받아들이나요?', 'zh-tw': '你如何看待新資訊?' }, - leftTrait: { en: 'Wants to believe', zh: '愿意相信', ja: 'まず信じたい', ko: '믿고 싶어한다', 'zh-tw': '願意相信' }, - rightTrait: { en: 'Skeptical', zh: '持怀疑态度', ja: 'まず疑ってみる', ko: '회의적이다', 'zh-tw': '持懷疑態度' }, + title: { + en: 'How do you approach new information?', + zh: '你如何看待新信息?', + ja: '新しい情報に対しては?', + ko: '새로운 정보를 어떻게 받아들이나요?', + 'zh-tw': '你如何看待新資訊?', + }, + leftTrait: { + en: 'Wants to believe', + zh: '愿意相信', + ja: 'まず信じたい', + ko: '믿고 싶어한다', + 'zh-tw': '願意相信', + }, + rightTrait: { + en: 'Skeptical', + zh: '持怀疑态度', + ja: 'まず疑ってみる', + ko: '회의적이다', + 'zh-tw': '持懷疑態度', + }, }, { id: 6, dimension: 'TF', - title: { en: 'How do you view logical thinking?', zh: '你如何看待逻辑思维?', ja: '「ロボットみたい」と言われたら?', ko: '논리적 사고를 어떻게 생각하나요?', 'zh-tw': '你如何看待邏輯思維?' }, - leftTrait: { en: 'Thinks "robotic" is an insult', zh: '认为"像机器人"是侮辱', ja: '悪口だと感じる', ko: '"로봇 같다"는 말을 모욕으로 여긴다', 'zh-tw': '認為「像機器人」是侮辱' }, - rightTrait: { en: 'Strives to have a mechanical mind', zh: '追求机械般的思维', ja: 'むしろそうありたい', ko: '기계적인 사고를 추구한다', 'zh-tw': '追求機械般的思維' }, + title: { + en: 'How do you view logical thinking?', + zh: '你如何看待逻辑思维?', + ja: '「ロボットみたい」と言われたら?', + ko: '논리적 사고를 어떻게 생각하나요?', + 'zh-tw': '你如何看待邏輯思維?', + }, + leftTrait: { + en: 'Thinks "robotic" is an insult', + zh: '认为"像机器人"是侮辱', + ja: '悪口だと感じる', + ko: '"로봇 같다"는 말을 모욕으로 여긴다', + 'zh-tw': '認為「像機器人」是侮辱', + }, + rightTrait: { + en: 'Strives to have a mechanical mind', + zh: '追求机械般的思维', + ja: 'むしろそうありたい', + ko: '기계적인 사고를 추구한다', + 'zh-tw': '追求機械般的思維', + }, }, { id: 10, dimension: 'TF', - title: { en: 'How do you handle criticism?', zh: '你如何应对批评?', ja: '批判されると?', ko: '비판을 어떻게 받아들이나요?', 'zh-tw': '你如何應對批評?' }, - leftTrait: { en: 'Easily hurt', zh: '容易受伤', ja: '傷つきやすい', ko: '쉽게 상처받는다', 'zh-tw': '容易受傷' }, - rightTrait: { en: 'Thick-skinned', zh: '脸皮厚', ja: '気にしない', ko: '둔감하다', 'zh-tw': '臉皮厚' }, + title: { + en: 'How do you handle criticism?', + zh: '你如何应对批评?', + ja: '批判されると?', + ko: '비판을 어떻게 받아들이나요?', + 'zh-tw': '你如何應對批評?', + }, + leftTrait: { + en: 'Easily hurt', + zh: '容易受伤', + ja: '傷つきやすい', + ko: '쉽게 상처받는다', + 'zh-tw': '容易受傷', + }, + rightTrait: { + en: 'Thick-skinned', + zh: '脸皮厚', + ja: '気にしない', + ko: '둔감하다', + 'zh-tw': '臉皮厚', + }, }, { id: 14, dimension: 'TF', - title: { en: 'What do you seek from others?', zh: '你希望从他人那里得到什么?', ja: '他者から何を求める?', ko: '다른 사람에게서 무엇을 원하나요?', 'zh-tw': '你希望從他人那裡得到什麼?' }, - leftTrait: { en: "Wants people's love", zh: '渴望他人的爱', ja: '人々の愛を求める', ko: '사람들의 사랑을 원한다', 'zh-tw': '渴望他人的愛' }, - rightTrait: { en: "Wants people's respect", zh: '渴望他人的尊重', ja: '人々の尊敬を求める', ko: '사람들의 존경을 원한다', 'zh-tw': '渴望他人的尊重' }, + title: { + en: 'What do you seek from others?', + zh: '你希望从他人那里得到什么?', + ja: '他者から何を求める?', + ko: '다른 사람에게서 무엇을 원하나요?', + 'zh-tw': '你希望從他人那裡得到什麼?', + }, + leftTrait: { + en: "Wants people's love", + zh: '渴望他人的爱', + ja: '人々の愛を求める', + ko: '사람들의 사랑을 원한다', + 'zh-tw': '渴望他人的愛', + }, + rightTrait: { + en: "Wants people's respect", + zh: '渴望他人的尊重', + ja: '人々の尊敬を求める', + ko: '사람들의 존경을 원한다', + 'zh-tw': '渴望他人的尊重', + }, }, { id: 18, dimension: 'TF', - title: { en: 'What would you like to be good at?', zh: '你想擅长什么?', ja: '得意になりたいのは?', ko: '무엇을 잘하고 싶나요?', 'zh-tw': '你想擅長什麼?' }, - leftTrait: { en: 'Wants to be good at fixing people', zh: '想擅长帮助他人', ja: '人の悩みを解きほぐすこと', ko: '사람을 돕는 것을 잘하고 싶다', 'zh-tw': '想擅長幫助他人' }, - rightTrait: { en: 'Wants to be good at fixing things', zh: '想擅长修理事物', ja: '壊れた物を直すこと', ko: '물건을 고치는 것을 잘하고 싶다', 'zh-tw': '想擅長修理事物' }, + title: { + en: 'What would you like to be good at?', + zh: '你想擅长什么?', + ja: '得意になりたいのは?', + ko: '무엇을 잘하고 싶나요?', + 'zh-tw': '你想擅長什麼?', + }, + leftTrait: { + en: 'Wants to be good at fixing people', + zh: '想擅长帮助他人', + ja: '人の悩みを解きほぐすこと', + ko: '사람을 돕는 것을 잘하고 싶다', + 'zh-tw': '想擅長幫助他人', + }, + rightTrait: { + en: 'Wants to be good at fixing things', + zh: '想擅长修理事物', + ja: '壊れた物を直すこと', + ko: '물건을 고치는 것을 잘하고 싶다', + 'zh-tw': '想擅長修理事物', + }, }, { id: 22, dimension: 'TF', - title: { en: 'How do you make decisions?', zh: '你如何做决定?', ja: '決めるときの決め手は?', ko: '어떻게 결정을 내리나요?', 'zh-tw': '你如何做決定?' }, - leftTrait: { en: 'Follows the heart', zh: '跟随内心', ja: '気持ち', ko: '마음을 따른다', 'zh-tw': '跟隨內心' }, - rightTrait: { en: 'Follows the head', zh: '跟随理性', ja: '理屈', ko: '이성을 따른다', 'zh-tw': '跟隨理性' }, + title: { + en: 'How do you make decisions?', + zh: '你如何做决定?', + ja: '決めるときの決め手は?', + ko: '어떻게 결정을 내리나요?', + 'zh-tw': '你如何做決定?', + }, + leftTrait: { + en: 'Follows the heart', + zh: '跟随内心', + ja: '気持ち', + ko: '마음을 따른다', + 'zh-tw': '跟隨內心', + }, + rightTrait: { + en: 'Follows the head', + zh: '跟随理性', + ja: '理屈', + ko: '이성을 따른다', + 'zh-tw': '跟隨理性', + }, }, { id: 26, dimension: 'TF', - title: { en: 'What is morality based on?', zh: '道德的基础是什么?', ja: '善悪の判断のよりどころは?', ko: '도덕의 기반은 무엇인가요?', 'zh-tw': '道德的基礎是什麼?' }, - leftTrait: { en: 'Bases morality on compassion', zh: '道德基于同情', ja: '思いやり', ko: '도덕은 동정심에 기반한다', 'zh-tw': '道德基於同情' }, - rightTrait: { en: 'Bases morality on justice', zh: '道德基于正义', ja: '公正さ', ko: '도덕은 정의에 기반한다', 'zh-tw': '道德基於正義' }, + title: { + en: 'What is morality based on?', + zh: '道德的基础是什么?', + ja: '善悪の判断のよりどころは?', + ko: '도덕의 기반은 무엇인가요?', + 'zh-tw': '道德的基礎是什麼?', + }, + leftTrait: { + en: 'Bases morality on compassion', + zh: '道德基于同情', + ja: '思いやり', + ko: '도덕은 동정심에 기반한다', + 'zh-tw': '道德基於同情', + }, + rightTrait: { + en: 'Bases morality on justice', + zh: '道德基于正义', + ja: '公正さ', + ko: '도덕은 정의에 기반한다', + 'zh-tw': '道德基於正義', + }, }, { id: 30, dimension: 'TF', - title: { en: 'How do you relate to emotions?', zh: '你如何看待情感?', ja: '感情については?', ko: '감정을 어떻게 대하나요?', 'zh-tw': '你如何看待情感?' }, - leftTrait: { en: 'Values emotions', zh: '重视情感', ja: '感情を大切にする', ko: '감정을 중시한다', 'zh-tw': '重視情感' }, - rightTrait: { en: 'Uncomfortable with emotions', zh: '对情感感到不自在', ja: '感情を扱うのは苦手', ko: '감정이 불편하다', 'zh-tw': '對情感感到不自在' }, + title: { + en: 'How do you relate to emotions?', + zh: '你如何看待情感?', + ja: '感情については?', + ko: '감정을 어떻게 대하나요?', + 'zh-tw': '你如何看待情感?', + }, + leftTrait: { + en: 'Values emotions', + zh: '重视情感', + ja: '感情を大切にする', + ko: '감정을 중시한다', + 'zh-tw': '重視情感', + }, + rightTrait: { + en: 'Uncomfortable with emotions', + zh: '对情感感到不自在', + ja: '感情を扱うのは苦手', + ko: '감정이 불편하다', + 'zh-tw': '對情感感到不自在', + }, }, // EI dimension (Questions 3, 7, 11, 15, 19, 23, 27, 31) { id: 3, dimension: 'EI', - title: { en: 'How do you feel about being alone?', zh: '你对独处有什么感受?', ja: '一人の時間は?', ko: '혼자 있는 것을 어떻게 느끼나요?', 'zh-tw': '你對獨處有什麼感受?' }, - leftTrait: { en: 'Bored by time alone', zh: '独处时感到无聊', ja: '退屈に感じる', ko: '혼자 있으면 지루하다', 'zh-tw': '獨處時感到無聊' }, - rightTrait: { en: 'Needs time alone', zh: '需要独处时间', ja: 'なくてはならない', ko: '혼자만의 시간이 필요하다', 'zh-tw': '需要獨處時間' }, + title: { + en: 'How do you feel about being alone?', + zh: '你对独处有什么感受?', + ja: '一人の時間は?', + ko: '혼자 있는 것을 어떻게 느끼나요?', + 'zh-tw': '你對獨處有什麼感受?', + }, + leftTrait: { + en: 'Bored by time alone', + zh: '独处时感到无聊', + ja: '退屈に感じる', + ko: '혼자 있으면 지루하다', + 'zh-tw': '獨處時感到無聊', + }, + rightTrait: { + en: 'Needs time alone', + zh: '需要独处时间', + ja: 'なくてはならない', + ko: '혼자만의 시간이 필요하다', + 'zh-tw': '需要獨處時間', + }, }, { id: 7, dimension: 'EI', - title: { en: 'What is your energy level like?', zh: '你的能量状态如何?', ja: '普段のテンションは?', ko: '당신의 에너지 수준은?', 'zh-tw': '你的能量狀態如何?' }, - leftTrait: { en: 'Energetic', zh: '精力充沛', ja: 'エネルギッシュ', ko: '활기차다', 'zh-tw': '精力充沛' }, + title: { + en: 'What is your energy level like?', + zh: '你的能量状态如何?', + ja: '普段のテンションは?', + ko: '당신의 에너지 수준은?', + 'zh-tw': '你的能量狀態如何?', + }, + leftTrait: { + en: 'Energetic', + zh: '精力充沛', + ja: 'エネルギッシュ', + ko: '활기차다', + 'zh-tw': '精力充沛', + }, rightTrait: { en: 'Mellow', zh: '平静温和', ja: '穏やか', ko: '차분하다', 'zh-tw': '平靜溫和' }, }, { id: 11, dimension: 'EI', - title: { en: 'How do you work best?', zh: '你在什么环境下工作最好?', ja: '力を発揮しやすいのは?', ko: '어떤 환경에서 가장 잘 일하나요?', 'zh-tw': '你在什麼環境下工作最好?' }, - leftTrait: { en: 'Works best in groups', zh: '在团队中表现最佳', ja: 'みんなで取り組むとき', ko: '팀에서 일할 때 최고다', 'zh-tw': '在團隊中表現最佳' }, - rightTrait: { en: 'Works best alone', zh: '独自工作表现最佳', ja: '一人で取り組むとき', ko: '혼자 일할 때 최고다', 'zh-tw': '獨自工作表現最佳' }, + title: { + en: 'How do you work best?', + zh: '你在什么环境下工作最好?', + ja: '力を発揮しやすいのは?', + ko: '어떤 환경에서 가장 잘 일하나요?', + 'zh-tw': '你在什麼環境下工作最好?', + }, + leftTrait: { + en: 'Works best in groups', + zh: '在团队中表现最佳', + ja: 'みんなで取り組むとき', + ko: '팀에서 일할 때 최고다', + 'zh-tw': '在團隊中表現最佳', + }, + rightTrait: { + en: 'Works best alone', + zh: '独自工作表现最佳', + ja: '一人で取り組むとき', + ko: '혼자 일할 때 최고다', + 'zh-tw': '獨自工作表現最佳', + }, }, { id: 15, dimension: 'EI', - title: { en: 'How do parties make you feel?', zh: '聚会让你感觉如何?', ja: '大人数の集まりでは?', ko: '파티에서 어떤 기분이 드나요?', 'zh-tw': '聚會讓你感覺如何?' }, - leftTrait: { en: 'Gets fired up by parties', zh: '因聚会而兴奋', ja: '元気が出る', ko: '파티에서 흥분한다', 'zh-tw': '因聚會而興奮' }, - rightTrait: { en: 'Gets worn out by parties', zh: '因聚会而疲惫', ja: '疲れてしまう', ko: '파티에서 지친다', 'zh-tw': '因聚會而疲憊' }, + title: { + en: 'How do parties make you feel?', + zh: '聚会让你感觉如何?', + ja: '大人数の集まりでは?', + ko: '파티에서 어떤 기분이 드나요?', + 'zh-tw': '聚會讓你感覺如何?', + }, + leftTrait: { + en: 'Gets fired up by parties', + zh: '因聚会而兴奋', + ja: '元気が出る', + ko: '파티에서 흥분한다', + 'zh-tw': '因聚會而興奮', + }, + rightTrait: { + en: 'Gets worn out by parties', + zh: '因聚会而疲惫', + ja: '疲れてしまう', + ko: '파티에서 지친다', + 'zh-tw': '因聚會而疲憊', + }, }, { id: 19, dimension: 'EI', - title: { en: 'In conversations, do you...', zh: '在交谈中,你更倾向于...', ja: '会話では?', ko: '대화할 때 당신은...', 'zh-tw': '在交談中,你更傾向於...' }, - leftTrait: { en: 'Talks more', zh: '话比较多', ja: 'よく話す', ko: '말을 많이 한다', 'zh-tw': '話比較多' }, - rightTrait: { en: 'Listens more', zh: '更善于倾听', ja: 'よく聞く', ko: '듣는 편이다', 'zh-tw': '更善於傾聽' }, + title: { + en: 'In conversations, do you...', + zh: '在交谈中,你更倾向于...', + ja: '会話では?', + ko: '대화할 때 당신은...', + 'zh-tw': '在交談中,你更傾向於...', + }, + leftTrait: { + en: 'Talks more', + zh: '话比较多', + ja: 'よく話す', + ko: '말을 많이 한다', + 'zh-tw': '話比較多', + }, + rightTrait: { + en: 'Listens more', + zh: '更善于倾听', + ja: 'よく聞く', + ko: '듣는 편이다', + 'zh-tw': '更善於傾聽', + }, }, { id: 23, dimension: 'EI', - title: { en: 'What do you prefer on weekends?', zh: '周末你更喜欢做什么?', ja: '週末は?', ko: '주말에 무엇을 하고 싶나요?', 'zh-tw': '週末你更喜歡做什麼?' }, - leftTrait: { en: 'Goes out on the town', zh: '喜欢外出活动', ja: '外に出かける', ko: '외출을 즐긴다', 'zh-tw': '喜歡外出活動' }, - rightTrait: { en: 'Stays at home', zh: '喜欢待在家里', ja: '家で過ごす', ko: '집에 있는 것을 좋아한다', 'zh-tw': '喜歡待在家裡' }, + title: { + en: 'What do you prefer on weekends?', + zh: '周末你更喜欢做什么?', + ja: '週末は?', + ko: '주말에 무엇을 하고 싶나요?', + 'zh-tw': '週末你更喜歡做什麼?', + }, + leftTrait: { + en: 'Goes out on the town', + zh: '喜欢外出活动', + ja: '外に出かける', + ko: '외출을 즐긴다', + 'zh-tw': '喜歡外出活動', + }, + rightTrait: { + en: 'Stays at home', + zh: '喜欢待在家里', + ja: '家で過ごす', + ko: '집에 있는 것을 좋아한다', + 'zh-tw': '喜歡待在家裡', + }, }, { id: 27, dimension: 'EI', - title: { en: 'How loud is your natural voice?', zh: '你的自然音量如何?', ja: '声の大きさは?', ko: '당신의 자연스러운 목소리는?', 'zh-tw': '你的自然音量如何?' }, - leftTrait: { en: 'Yelling comes naturally', zh: '自然而然大声说话', ja: '大きな声が自然に出る', ko: '큰 소리가 자연스럽다', 'zh-tw': '自然而然大聲說話' }, - rightTrait: { en: 'Finds it difficult to yell loudly', zh: '很难大声喊叫', ja: '大声を出すのが苦手', ko: '큰 소리를 내기 어렵다', 'zh-tw': '很難大聲喊叫' }, + title: { + en: 'How loud is your natural voice?', + zh: '你的自然音量如何?', + ja: '声の大きさは?', + ko: '당신의 자연스러운 목소리는?', + 'zh-tw': '你的自然音量如何?', + }, + leftTrait: { + en: 'Yelling comes naturally', + zh: '自然而然大声说话', + ja: '大きな声が自然に出る', + ko: '큰 소리가 자연스럽다', + 'zh-tw': '自然而然大聲說話', + }, + rightTrait: { + en: 'Finds it difficult to yell loudly', + zh: '很难大声喊叫', + ja: '大声を出すのが苦手', + ko: '큰 소리를 내기 어렵다', + 'zh-tw': '很難大聲喊叫', + }, }, { id: 31, dimension: 'EI', - title: { en: 'How do you feel about being in the spotlight?', zh: '你对成为焦点有什么感受?', ja: '注目されるのは?', ko: '주목받는 것을 어떻게 느끼나요?', 'zh-tw': '你對成為焦點有什麼感受?' }, - leftTrait: { en: 'Likes to perform', zh: '喜欢表演', ja: '人前に立つのが好き', ko: '공연하는 것을 좋아한다', 'zh-tw': '喜歡表演' }, - rightTrait: { en: 'Avoids public speaking', zh: '避免公开演讲', ja: 'できれば避けたい', ko: '대중 앞에서 말하기를 피한다', 'zh-tw': '避免公開演講' }, + title: { + en: 'How do you feel about being in the spotlight?', + zh: '你对成为焦点有什么感受?', + ja: '注目されるのは?', + ko: '주목받는 것을 어떻게 느끼나요?', + 'zh-tw': '你對成為焦點有什麼感受?', + }, + leftTrait: { + en: 'Likes to perform', + zh: '喜欢表演', + ja: '人前に立つのが好き', + ko: '공연하는 것을 좋아한다', + 'zh-tw': '喜歡表演', + }, + rightTrait: { + en: 'Avoids public speaking', + zh: '避免公开演讲', + ja: 'できれば避けたい', + ko: '대중 앞에서 말하기를 피한다', + 'zh-tw': '避免公開演講', + }, }, // SN dimension (Questions 4, 8, 12, 16, 20, 24, 28, 32) { id: 4, dimension: 'SN', - title: { en: 'How do you view the status quo?', zh: '你如何看待现状?', ja: '今の状況については?', ko: '현 상황을 어떻게 보나요?', 'zh-tw': '你如何看待現狀?' }, - leftTrait: { en: 'Accepts things as they are', zh: '接受事物的本来面目', ja: 'あるがままを受け入れる', ko: '있는 그대로 받아들인다', 'zh-tw': '接受事物的本來面目' }, - rightTrait: { en: 'Unsatisfied with the way things are', zh: '对现状不满足', ja: '今のままでは物足りない', ko: '현 상황에 만족하지 않는다', 'zh-tw': '對現狀不滿足' }, + title: { + en: 'How do you view the status quo?', + zh: '你如何看待现状?', + ja: '今の状況については?', + ko: '현 상황을 어떻게 보나요?', + 'zh-tw': '你如何看待現狀?', + }, + leftTrait: { + en: 'Accepts things as they are', + zh: '接受事物的本来面目', + ja: 'あるがままを受け入れる', + ko: '있는 그대로 받아들인다', + 'zh-tw': '接受事物的本來面目', + }, + rightTrait: { + en: 'Unsatisfied with the way things are', + zh: '对现状不满足', + ja: '今のままでは物足りない', + ko: '현 상황에 만족하지 않는다', + 'zh-tw': '對現狀不滿足', + }, }, { id: 8, dimension: 'SN', - title: { en: 'What type of test do you prefer?', zh: '你喜欢什么类型的考试?', ja: '好きな試験形式は?', ko: '어떤 유형의 시험을 선호하나요?', 'zh-tw': '你喜歡什麼類型的考試?' }, - leftTrait: { en: 'Prefers multiple choice test', zh: '喜欢选择题', ja: '選択問題', ko: '객관식을 선호한다', 'zh-tw': '喜歡選擇題' }, - rightTrait: { en: 'Prefers essay answers', zh: '喜欢论述题', ja: '記述問題', ko: '서술형을 선호한다', 'zh-tw': '喜歡論述題' }, + title: { + en: 'What type of test do you prefer?', + zh: '你喜欢什么类型的考试?', + ja: '好きな試験形式は?', + ko: '어떤 유형의 시험을 선호하나요?', + 'zh-tw': '你喜歡什麼類型的考試?', + }, + leftTrait: { + en: 'Prefers multiple choice test', + zh: '喜欢选择题', + ja: '選択問題', + ko: '객관식을 선호한다', + 'zh-tw': '喜歡選擇題', + }, + rightTrait: { + en: 'Prefers essay answers', + zh: '喜欢论述题', + ja: '記述問題', + ko: '서술형을 선호한다', + 'zh-tw': '喜歡論述題', + }, }, { id: 12, dimension: 'SN', - title: { en: 'Where is your focus in time?', zh: '你的时间焦点在哪里?', ja: '意識が向くのは?', ko: '시간의 초점은 어디에 있나요?', 'zh-tw': '你的時間焦點在哪裡?' }, - leftTrait: { en: 'Focused on the past', zh: '关注过去', ja: '過去のこと', ko: '과거에 집중한다', 'zh-tw': '關注過去' }, - rightTrait: { en: 'Focused on the future', zh: '关注未来', ja: '未来のこと', ko: '미래에 집중한다', 'zh-tw': '關注未來' }, + title: { + en: 'Where is your focus in time?', + zh: '你的时间焦点在哪里?', + ja: '意識が向くのは?', + ko: '시간의 초점은 어디에 있나요?', + 'zh-tw': '你的時間焦點在哪裡?', + }, + leftTrait: { + en: 'Focused on the past', + zh: '关注过去', + ja: '過去のこと', + ko: '과거에 집중한다', + 'zh-tw': '關注過去', + }, + rightTrait: { + en: 'Focused on the future', + zh: '关注未来', + ja: '未来のこと', + ko: '미래에 집중한다', + 'zh-tw': '關注未來', + }, }, { id: 16, dimension: 'SN', - title: { en: 'In a group, do you tend to...', zh: '在群体中,你倾向于...', ja: 'グループの中では?', ko: '그룹에서 당신은...', 'zh-tw': '在群體中,你傾向於...' }, - leftTrait: { en: 'Fits in', zh: '融入群体', ja: '周りに溶け込む', ko: '어울린다', 'zh-tw': '融入群體' }, - rightTrait: { en: 'Stands out', zh: '与众不同', ja: '目立つほう', ko: '눈에 띈다', 'zh-tw': '與眾不同' }, + title: { + en: 'In a group, do you tend to...', + zh: '在群体中,你倾向于...', + ja: 'グループの中では?', + ko: '그룹에서 당신은...', + 'zh-tw': '在群體中,你傾向於...', + }, + leftTrait: { + en: 'Fits in', + zh: '融入群体', + ja: '周りに溶け込む', + ko: '어울린다', + 'zh-tw': '融入群體', + }, + rightTrait: { + en: 'Stands out', + zh: '与众不同', + ja: '目立つほう', + ko: '눈에 띈다', + 'zh-tw': '與眾不同', + }, }, { id: 20, dimension: 'SN', - title: { en: 'When telling a story, do you...', zh: '讲故事时,你会...', ja: '出来事を話すときは?', ko: '이야기할 때 당신은...', 'zh-tw': '講故事時,你會...' }, - leftTrait: { en: 'Describes what happened', zh: '描述发生了什么', ja: '何が起きたかを話す', ko: '무슨 일이 있었는지 설명한다', 'zh-tw': '描述發生了什麼' }, - rightTrait: { en: 'Describes what it meant', zh: '描述这意味着什么', ja: 'それにどんな意味があったかを話す', ko: '그것이 무엇을 의미하는지 설명한다', 'zh-tw': '描述這意味著什麼' }, + title: { + en: 'When telling a story, do you...', + zh: '讲故事时,你会...', + ja: '出来事を話すときは?', + ko: '이야기할 때 당신은...', + 'zh-tw': '講故事時,你會...', + }, + leftTrait: { + en: 'Describes what happened', + zh: '描述发生了什么', + ja: '何が起きたかを話す', + ko: '무슨 일이 있었는지 설명한다', + 'zh-tw': '描述發生了什麼', + }, + rightTrait: { + en: 'Describes what it meant', + zh: '描述这意味着什么', + ja: 'それにどんな意味があったかを話す', + ko: '그것이 무엇을 의미하는지 설명한다', + 'zh-tw': '描述這意味著什麼', + }, }, { id: 24, dimension: 'SN', - title: { en: 'When learning something new, what do you want?', zh: '学习新事物时,你想要什么?', ja: '新しいことを学ぶときは?', ko: '새로운 것을 배울 때 무엇을 원하나요?', 'zh-tw': '學習新事物時,你想要什麼?' }, - leftTrait: { en: 'Wants the details', zh: '想要细节', ja: 'まず細かい点を知りたい', ko: '세부사항을 원한다', 'zh-tw': '想要細節' }, - rightTrait: { en: 'Wants the big picture', zh: '想要大局观', ja: 'まず全体像をつかみたい', ko: '큰 그림을 원한다', 'zh-tw': '想要大局觀' }, + title: { + en: 'When learning something new, what do you want?', + zh: '学习新事物时,你想要什么?', + ja: '新しいことを学ぶときは?', + ko: '새로운 것을 배울 때 무엇을 원하나요?', + 'zh-tw': '學習新事物時,你想要什麼?', + }, + leftTrait: { + en: 'Wants the details', + zh: '想要细节', + ja: 'まず細かい点を知りたい', + ko: '세부사항을 원한다', + 'zh-tw': '想要細節', + }, + rightTrait: { + en: 'Wants the big picture', + zh: '想要大局观', + ja: 'まず全体像をつかみたい', + ko: '큰 그림을 원한다', + 'zh-tw': '想要大局觀', + }, }, { id: 28, dimension: 'SN', - title: { en: 'How do you approach knowledge?', zh: '你如何获取知识?', ja: '知識の身につけ方は?', ko: '지식에 어떻게 접근하나요?', 'zh-tw': '你如何獲取知識?' }, - leftTrait: { en: 'Empirical', zh: '经验主义', ja: '経験や実例から', ko: '경험주의적이다', 'zh-tw': '經驗主義' }, - rightTrait: { en: 'Theoretical', zh: '理论主义', ja: '理論や原理から', ko: '이론적이다', 'zh-tw': '理論主義' }, + title: { + en: 'How do you approach knowledge?', + zh: '你如何获取知识?', + ja: '知識の身につけ方は?', + ko: '지식에 어떻게 접근하나요?', + 'zh-tw': '你如何獲取知識?', + }, + leftTrait: { + en: 'Empirical', + zh: '经验主义', + ja: '経験や実例から', + ko: '경험주의적이다', + 'zh-tw': '經驗主義', + }, + rightTrait: { + en: 'Theoretical', + zh: '理论主义', + ja: '理論や原理から', + ko: '이론적이다', + 'zh-tw': '理論主義', + }, }, { id: 32, dimension: 'SN', - title: { en: 'What questions interest you most?', zh: '什么问题最让你感兴趣?', ja: '気になるのは?', ko: '어떤 질문에 가장 관심이 있나요?', 'zh-tw': '什麼問題最讓你感興趣?' }, - leftTrait: { en: 'Likes to know "who/what/when"', zh: '想知道"谁/什么/何时"', ja: '「誰が・何を・いつ」', ko: '"누가/무엇을/언제"를 알고 싶다', 'zh-tw': '想知道「誰/什麼/何時」' }, - rightTrait: { en: 'Likes to know "why"', zh: '想知道"为什么"', ja: '「なぜ」', ko: '"왜"를 알고 싶다', 'zh-tw': '想知道「為什麼」' }, + title: { + en: 'What questions interest you most?', + zh: '什么问题最让你感兴趣?', + ja: '気になるのは?', + ko: '어떤 질문에 가장 관심이 있나요?', + 'zh-tw': '什麼問題最讓你感興趣?', + }, + leftTrait: { + en: 'Likes to know "who/what/when"', + zh: '想知道"谁/什么/何时"', + ja: '「誰が・何を・いつ」', + ko: '"누가/무엇을/언제"를 알고 싶다', + 'zh-tw': '想知道「誰/什麼/何時」', + }, + rightTrait: { + en: 'Likes to know "why"', + zh: '想知道"为什么"', + ja: '「なぜ」', + ko: '"왜"를 알고 싶다', + 'zh-tw': '想知道「為什麼」', + }, }, ]; @@ -276,15 +846,17 @@ export const QUESTIONS_PER_DIMENSION = 8; * Quick Test Mode - 8 most discriminating questions (2 per dimension) * Selected for clarity and strong differentiation between types */ -export const quickTestQuestionIds: Record = { - EI: [3, 15], // "Needs time alone" & "Gets worn out by parties" - clear E/I indicators - SN: [24, 32], // "Details vs big picture" & "who/what/when vs why" - clear S/N indicators - TF: [22, 14], // "Heart vs head" & "love vs respect" - clear T/F indicators - JP: [9, 13], // "Organized vs chaotic" & "plans ahead vs last minute" - clear J/P indicators +export const quickTestQuestionIds: DimensionQuestions = { + EI: [3, 15], // "Needs time alone" & "Gets worn out by parties" - clear E/I indicators + SN: [24, 32], // "Details vs big picture" & "who/what/when vs why" - clear S/N indicators + TF: [22, 14], // "Heart vs head" & "love vs respect" - clear T/F indicators + JP: [9, 13], // "Organized vs chaotic" & "plans ahead vs last minute" - clear J/P indicators } as const; // Flat list of quick test question IDs (sorted by ID for consistent presentation) -export const quickTestQuestions = Object.values(quickTestQuestionIds).flat().sort((a, b) => a - b); +export const quickTestQuestions = Object.values(quickTestQuestionIds) + .flat() + .sort((a, b) => a - b); // Quick test totals export const QUICK_TEST_TOTAL = 8; diff --git a/src/quickScoring.test.ts b/src/quickScoring.test.ts index 365ecd5..57c9eb0 100644 --- a/src/quickScoring.test.ts +++ b/src/quickScoring.test.ts @@ -5,17 +5,19 @@ import { calculateQuickPercentages, generateQuickResult, isQuickTestComplete, -} from './scoring'; -import type { TestAnswers, DimensionScores } from './types'; -import { quickTestQuestionIds, QUICK_TEST_TOTAL } from './questions'; +} from './scoring.js'; +import type { TestAnswers, DimensionScores } from './types.js'; +import { quickTestQuestionIds, QUICK_TEST_TOTAL } from './questions.js'; describe('calculateQuickScores', () => { it('returns minimum scores (2) when all answers are 1', () => { const answers: TestAnswers = {}; // Quick test uses questions: EI[3,15], SN[24,32], TF[22,14], JP[9,13] - Object.values(quickTestQuestionIds).flat().forEach((id) => { - answers[id] = 1; - }); + Object.values(quickTestQuestionIds) + .flat() + .forEach((id) => { + answers[id] = 1; + }); const scores = calculateQuickScores(answers); expect(scores.EI).toBe(2); expect(scores.SN).toBe(2); @@ -25,9 +27,11 @@ describe('calculateQuickScores', () => { it('returns maximum scores (10) when all answers are 5', () => { const answers: TestAnswers = {}; - Object.values(quickTestQuestionIds).flat().forEach((id) => { - answers[id] = 5; - }); + Object.values(quickTestQuestionIds) + .flat() + .forEach((id) => { + answers[id] = 5; + }); const scores = calculateQuickScores(answers); expect(scores.EI).toBe(10); expect(scores.SN).toBe(10); @@ -37,9 +41,11 @@ describe('calculateQuickScores', () => { it('returns neutral scores (6) when all answers are 3', () => { const answers: TestAnswers = {}; - Object.values(quickTestQuestionIds).flat().forEach((id) => { - answers[id] = 3; - }); + Object.values(quickTestQuestionIds) + .flat() + .forEach((id) => { + answers[id] = 3; + }); const scores = calculateQuickScores(answers); expect(scores.EI).toBe(6); expect(scores.SN).toBe(6); @@ -77,10 +83,10 @@ describe('calculateQuickScores', () => { }); const scores = calculateQuickScores(answers); - expect(scores.EI).toBe(2); // 2 * 1 = 2 + expect(scores.EI).toBe(2); // 2 * 1 = 2 expect(scores.SN).toBe(10); // 2 * 5 = 10 - expect(scores.TF).toBe(4); // 2 * 2 = 4 - expect(scores.JP).toBe(8); // 2 * 4 = 8 + expect(scores.TF).toBe(4); // 2 * 2 = 4 + expect(scores.JP).toBe(8); // 2 * 4 = 8 }); it('only uses quick test question IDs', () => { @@ -90,9 +96,11 @@ describe('calculateQuickScores', () => { answers[i] = 5; } // Override quick test questions with 1 - Object.values(quickTestQuestionIds).flat().forEach((id) => { - answers[id] = 1; - }); + Object.values(quickTestQuestionIds) + .flat() + .forEach((id) => { + answers[id] = 1; + }); const scores = calculateQuickScores(answers); // Should only use quick test questions (answered 1) @@ -214,9 +222,11 @@ describe('calculateQuickPercentages', () => { describe('generateQuickResult', () => { it('generates complete result with all components', () => { const answers: TestAnswers = {}; - Object.values(quickTestQuestionIds).flat().forEach((id) => { - answers[id] = 3; - }); + Object.values(quickTestQuestionIds) + .flat() + .forEach((id) => { + answers[id] = 3; + }); const result = generateQuickResult(answers); expect(result).toHaveProperty('type'); expect(result).toHaveProperty('scores'); @@ -225,9 +235,11 @@ describe('generateQuickResult', () => { it('type matches scores correctly', () => { const answers: TestAnswers = {}; - Object.values(quickTestQuestionIds).flat().forEach((id) => { - answers[id] = 5; - }); + Object.values(quickTestQuestionIds) + .flat() + .forEach((id) => { + answers[id] = 5; + }); const result = generateQuickResult(answers); expect(result.type).toBe('INTP'); expect(result.scores.EI).toBe(10); @@ -238,9 +250,11 @@ describe('generateQuickResult', () => { it('percentages match scores correctly', () => { const answers: TestAnswers = {}; - Object.values(quickTestQuestionIds).flat().forEach((id) => { - answers[id] = 1; - }); + Object.values(quickTestQuestionIds) + .flat() + .forEach((id) => { + answers[id] = 1; + }); const result = generateQuickResult(answers); expect(result.percentages.E).toBe(100); expect(result.percentages.I).toBe(0); @@ -249,33 +263,49 @@ describe('generateQuickResult', () => { it('handles real-world ENFP answers', () => { // ENFP: E (low EI), N (high SN), F (low TF), P (high JP) const answers: TestAnswers = {}; - quickTestQuestionIds.EI.forEach((id) => { answers[id] = 2; }); // Low = E - quickTestQuestionIds.SN.forEach((id) => { answers[id] = 4; }); // High = N - quickTestQuestionIds.TF.forEach((id) => { answers[id] = 2; }); // Low = F - quickTestQuestionIds.JP.forEach((id) => { answers[id] = 4; }); // High = P + quickTestQuestionIds.EI.forEach((id) => { + answers[id] = 2; + }); // Low = E + quickTestQuestionIds.SN.forEach((id) => { + answers[id] = 4; + }); // High = N + quickTestQuestionIds.TF.forEach((id) => { + answers[id] = 2; + }); // Low = F + quickTestQuestionIds.JP.forEach((id) => { + answers[id] = 4; + }); // High = P const result = generateQuickResult(answers); expect(result.type).toBe('ENFP'); - expect(result.scores.EI).toBe(4); // E preference - expect(result.scores.SN).toBe(8); // N preference - expect(result.scores.TF).toBe(4); // F preference - expect(result.scores.JP).toBe(8); // P preference + expect(result.scores.EI).toBe(4); // E preference + expect(result.scores.SN).toBe(8); // N preference + expect(result.scores.TF).toBe(4); // F preference + expect(result.scores.JP).toBe(8); // P preference }); it('handles real-world ISTJ answers', () => { // ISTJ: I (high EI), S (low SN), T (high TF), J (low JP) const answers: TestAnswers = {}; - quickTestQuestionIds.EI.forEach((id) => { answers[id] = 4; }); // High = I - quickTestQuestionIds.SN.forEach((id) => { answers[id] = 2; }); // Low = S - quickTestQuestionIds.TF.forEach((id) => { answers[id] = 4; }); // High = T - quickTestQuestionIds.JP.forEach((id) => { answers[id] = 2; }); // Low = J + quickTestQuestionIds.EI.forEach((id) => { + answers[id] = 4; + }); // High = I + quickTestQuestionIds.SN.forEach((id) => { + answers[id] = 2; + }); // Low = S + quickTestQuestionIds.TF.forEach((id) => { + answers[id] = 4; + }); // High = T + quickTestQuestionIds.JP.forEach((id) => { + answers[id] = 2; + }); // Low = J const result = generateQuickResult(answers); expect(result.type).toBe('ISTJ'); - expect(result.scores.EI).toBe(8); // I preference - expect(result.scores.SN).toBe(4); // S preference - expect(result.scores.TF).toBe(8); // T preference - expect(result.scores.JP).toBe(4); // J preference + expect(result.scores.EI).toBe(8); // I preference + expect(result.scores.SN).toBe(4); // S preference + expect(result.scores.TF).toBe(8); // T preference + expect(result.scores.JP).toBe(4); // J preference }); }); @@ -291,9 +321,11 @@ describe('isQuickTestComplete', () => { it('returns true for exactly 8 answers', () => { const answers: TestAnswers = {}; - Object.values(quickTestQuestionIds).flat().forEach((id) => { - answers[id] = 3; - }); + Object.values(quickTestQuestionIds) + .flat() + .forEach((id) => { + answers[id] = 3; + }); expect(isQuickTestComplete(answers)).toBe(true); expect(Object.keys(answers).length).toBe(QUICK_TEST_TOTAL); }); diff --git a/src/scoring.test.ts b/src/scoring.test.ts index 44935b0..d628d25 100644 --- a/src/scoring.test.ts +++ b/src/scoring.test.ts @@ -23,9 +23,9 @@ import { checkDimensionConsistency, checkTestConsistency, getConfidenceLabel, -} from './scoring'; -import type { TestAnswers, DimensionScores, Dimension, ConfidenceLevel } from './types'; -import { dimensionQuestions, TOTAL_QUESTIONS } from './questions'; +} from './scoring.js'; +import type { TestAnswers, DimensionScores, Dimension } from './types.js'; +import { dimensionQuestions, TOTAL_QUESTIONS } from './questions.js'; describe('calculateScores', () => { it('returns minimum scores (8) when all answers are 1', () => { diff --git a/tsconfig.build.json b/tsconfig.build.json new file mode 100644 index 0000000..3276af0 --- /dev/null +++ b/tsconfig.build.json @@ -0,0 +1,7 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "noEmit": false + }, + "exclude": ["node_modules", "dist", "src/**/*.test.ts"] +} diff --git a/tsconfig.json b/tsconfig.json index 2323cff..6613dc8 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,14 +1,21 @@ { "compilerOptions": { - "target": "ES2020", - "module": "ESNext", - "moduleResolution": "bundler", + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "lib": ["ES2022"], + "types": ["node"], "declaration": true, "declarationMap": true, "sourceMap": true, "outDir": "./dist", "rootDir": "./src", "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "verbatimModuleSyntax": true, + "isolatedModules": true, "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true From 7d0f40e14e42a6546a1906ef0a54dc39a90ab28a Mon Sep 17 00:00:00 2001 From: ya-luotao Date: Sat, 10 Oct 2026 06:11:20 +0800 Subject: [PATCH 2/5] refactor: share one scoring engine across all tests; add locale helpers scoring.ts and purrjungScoring.ts carried three copies of the same sum / threshold / percentage logic that differed only in question IDs, score range and pole letters. Move that into an internal scale.ts and express each test as a ScaleSpec. Results are unchanged; the existing 212 tests pass without modification. Add SUPPORTED_LOCALES, PURRJUNG_LOCALES, DEFAULT_LOCALE, the Locale type and getLocalizedText() so consumers stop hand-maintaining the locale list. Rename BilingualText to MultilingualText and keep the old name as a deprecated alias. Type quickTestQuestionIds as DimensionQuestions. New tests: locale coverage on every question, PurrJung scoring (it had none), and a pinned list of public runtime exports so removals are loud. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01FGXjqn5CZbnVLNtHjkp45t --- src/index.test.ts | 83 ++++++++++ src/locales.test.ts | 70 +++++++++ src/locales.ts | 73 +++++++++ src/purrjungScoring.test.ts | 130 ++++++++++++++++ src/purrjungScoring.ts | 72 ++++----- src/scale.ts | 112 ++++++++++++++ src/scoring.ts | 296 +++++++++++++++--------------------- src/types.ts | 17 ++- 8 files changed, 627 insertions(+), 226 deletions(-) create mode 100644 src/index.test.ts create mode 100644 src/locales.test.ts create mode 100644 src/locales.ts create mode 100644 src/purrjungScoring.test.ts create mode 100644 src/scale.ts diff --git a/src/index.test.ts b/src/index.test.ts new file mode 100644 index 0000000..3511aba --- /dev/null +++ b/src/index.test.ts @@ -0,0 +1,83 @@ +import { describe, it, expect } from 'vitest'; +import * as api from './index.js'; + +/** + * Public API contract. Every name here is re-exported by downstream apps + * (openjung.org re-exports most of them verbatim), so removing or renaming one + * is a breaking change. Add new names freely; remove only with a major bump. + */ +const PUBLIC_EXPORTS = [ + // Question bank + 'questions', + 'sortedQuestions', + 'dimensionQuestions', + 'TOTAL_QUESTIONS', + 'QUESTIONS_PER_DIMENSION', + // Quick test + 'quickTestQuestionIds', + 'quickTestQuestions', + 'QUICK_TEST_TOTAL', + 'QUICK_TEST_PER_DIMENSION', + // Full test scoring + 'calculateScores', + 'determineType', + 'calculatePercentages', + 'generateResult', + 'isTestComplete', + // Quick test scoring + 'calculateQuickScores', + 'determineQuickType', + 'calculateQuickPercentages', + 'generateQuickResult', + 'isQuickTestComplete', + // Single dimension + 'DIMENSION_QUESTIONS_COUNT', + 'DIMENSION_SCORE_MIN', + 'DIMENSION_SCORE_MAX', + 'DIMENSION_THRESHOLD', + 'calculateDimensionScore', + 'determineDimensionPreference', + 'calculateDimensionPercentages', + 'generateDimensionResult', + 'isDimensionTestComplete', + 'getDimensionQuestionIds', + // Quality metrics + 'getConfidenceLevel', + 'calculateDimensionConfidence', + 'calculateTestConfidence', + 'checkDimensionConsistency', + 'checkTestConsistency', + 'getConfidenceLabel', + // Locales + 'SUPPORTED_LOCALES', + 'PURRJUNG_LOCALES', + 'DEFAULT_LOCALE', + 'getLocalizedText', + // PurrJung + 'purrjungQuestions', + 'purrjungDimensionQuestions', + 'sortedPurrjungQuestions', + 'PURRJUNG_TOTAL_QUESTIONS', + 'PURRJUNG_QUESTIONS_PER_DIMENSION', + 'PURRJUNG_SCORE_MIN', + 'PURRJUNG_SCORE_MAX', + 'PURRJUNG_THRESHOLD', + 'calculatePurrjungScores', + 'determinePurrjungType', + 'calculatePurrjungPercentages', + 'generatePurrjungResult', + 'isPurrjungTestComplete', + 'getPurrjungDimensionQuestionIds', +].sort(); + +describe('public API surface', () => { + it('exports exactly the documented runtime names', () => { + expect(Object.keys(api).sort()).toEqual(PUBLIC_EXPORTS); + }); + + it('exports no undefined values', () => { + for (const name of PUBLIC_EXPORTS) { + expect(api[name as keyof typeof api], name).toBeDefined(); + } + }); +}); diff --git a/src/locales.test.ts b/src/locales.test.ts new file mode 100644 index 0000000..c530732 --- /dev/null +++ b/src/locales.test.ts @@ -0,0 +1,70 @@ +import { describe, it, expect } from 'vitest'; +import { + SUPPORTED_LOCALES, + PURRJUNG_LOCALES, + DEFAULT_LOCALE, + getLocalizedText, +} from './locales.js'; +import { questions } from './questions.js'; +import { purrjungQuestions } from './purrjungQuestions.js'; +import { questionTranslations } from './questionTranslations.js'; + +describe('SUPPORTED_LOCALES', () => { + it('lists 44 unique locales starting with the five reviewed ones', () => { + expect(SUPPORTED_LOCALES).toHaveLength(44); + expect(new Set(SUPPORTED_LOCALES).size).toBe(44); + expect(SUPPORTED_LOCALES.slice(0, 5)).toEqual(['en', 'zh', 'ja', 'ko', 'zh-tw']); + }); + + it('matches the locales in the translation table', () => { + const added = SUPPORTED_LOCALES.slice(5); + expect([...added].sort()).toEqual(Object.keys(questionTranslations).sort()); + }); + + it('is present on every field of every OEJTS question', () => { + for (const question of questions) { + for (const locale of SUPPORTED_LOCALES) { + expect(question.title?.[locale], `q${question.id} title ${locale}`).toBeTruthy(); + expect(question.leftTrait[locale], `q${question.id} left ${locale}`).toBeTruthy(); + expect(question.rightTrait[locale], `q${question.id} right ${locale}`).toBeTruthy(); + } + } + }); + + it('starts with the default locale', () => { + expect(SUPPORTED_LOCALES[0]).toBe(DEFAULT_LOCALE); + expect(DEFAULT_LOCALE).toBe('en'); + }); +}); + +describe('PURRJUNG_LOCALES', () => { + it('is present on every field of every PurrJung question', () => { + expect(PURRJUNG_LOCALES).toEqual(['en', 'zh', 'ja', 'ko', 'zh-tw']); + for (const question of purrjungQuestions) { + for (const locale of PURRJUNG_LOCALES) { + expect(question.title?.[locale], `q${question.id} title ${locale}`).toBeTruthy(); + expect(question.leftTrait[locale], `q${question.id} left ${locale}`).toBeTruthy(); + expect(question.rightTrait[locale], `q${question.id} right ${locale}`).toBeTruthy(); + } + } + }); +}); + +describe('getLocalizedText', () => { + const text = { en: 'Makes lists', de: 'Macht Listen' }; + + it('returns the requested locale when present', () => { + expect(getLocalizedText(text, 'de')).toBe('Macht Listen'); + }); + + it('falls back to English for a missing locale', () => { + expect(getLocalizedText(text, 'fr')).toBe('Makes lists'); + }); + + it('returns the real translation for every question and locale, never the fallback', () => { + const q1 = questions.find((q) => q.id === 1)!; + for (const locale of SUPPORTED_LOCALES) { + expect(getLocalizedText(q1.leftTrait, locale)).toBe(q1.leftTrait[locale]); + } + }); +}); diff --git a/src/locales.ts b/src/locales.ts new file mode 100644 index 0000000..f01294a --- /dev/null +++ b/src/locales.ts @@ -0,0 +1,73 @@ +import type { MultilingualText } from './types.js'; + +/** + * Locales shipped with the OEJTS question bank (`questions`), in the order they were added. + * + * The first five were written and reviewed by people. The remaining 39 are AI-assisted + * drafts awaiting native-speaker review; see TRANSLATIONS.md for provenance and caveats. + */ +export const SUPPORTED_LOCALES = [ + 'en', + 'zh', + 'ja', + 'ko', + 'zh-tw', + 'ms', + 'de', + 'fr', + 'es', + 'ar', + 'he', + 'ru', + 'pt-br', + 'id', + 'vi', + 'th', + 'tr', + 'it', + 'pl', + 'nl', + 'hi', + 'bn', + 'fil', + 'uk', + 'sw', + 'cs', + 'ro', + 'hu', + 'sk', + 'el', + 'sv', + 'no', + 'da', + 'fi', + 'my', + 'km', + 'lo', + 'si', + 'ta', + 'am', + 'ha', + 'yo', + 'zu', + 'ig', +] as const; + +/** A locale code present in every OEJTS question. */ +export type Locale = (typeof SUPPORTED_LOCALES)[number]; + +/** Locales shipped with the PurrJung cat test (`purrjungQuestions`). */ +export const PURRJUNG_LOCALES = ['en', 'zh', 'ja', 'ko', 'zh-tw'] as const; + +/** The locale every text object is guaranteed to contain. */ +export const DEFAULT_LOCALE = 'en' as const; + +/** + * Read a localized string, falling back to English when the locale is missing. + * + * @param text - A multilingual text object such as a question's `title` or `leftTrait` + * @param locale - The locale to read, e.g. `'de'` or `'zh-tw'` + */ +export function getLocalizedText(text: MultilingualText, locale: string): string { + return text[locale] ?? text[DEFAULT_LOCALE]; +} diff --git a/src/purrjungScoring.test.ts b/src/purrjungScoring.test.ts new file mode 100644 index 0000000..a17cb21 --- /dev/null +++ b/src/purrjungScoring.test.ts @@ -0,0 +1,130 @@ +import { describe, it, expect } from 'vitest'; +import { + PURRJUNG_SCORE_MIN, + PURRJUNG_SCORE_MAX, + PURRJUNG_THRESHOLD, + calculatePurrjungScores, + determinePurrjungType, + calculatePurrjungPercentages, + generatePurrjungResult, + isPurrjungTestComplete, + getPurrjungDimensionQuestionIds, +} from './purrjungScoring.js'; +import { + purrjungQuestions, + purrjungDimensionQuestions, + sortedPurrjungQuestions, + PURRJUNG_TOTAL_QUESTIONS, + PURRJUNG_QUESTIONS_PER_DIMENSION, +} from './purrjungQuestions.js'; +import type { Dimension, TestAnswers } from './types.js'; + +const DIMENSIONS: Dimension[] = ['EI', 'SN', 'TF', 'JP']; + +function allAnswers(value: number): TestAnswers { + const answers: TestAnswers = {}; + for (let i = 1; i <= PURRJUNG_TOTAL_QUESTIONS; i++) answers[i] = value; + return answers; +} + +describe('PurrJung question bank', () => { + it('has 16 questions, 4 per dimension, with unique sequential IDs', () => { + expect(purrjungQuestions).toHaveLength(PURRJUNG_TOTAL_QUESTIONS); + expect(PURRJUNG_TOTAL_QUESTIONS).toBe(16); + expect(PURRJUNG_QUESTIONS_PER_DIMENSION).toBe(4); + expect(sortedPurrjungQuestions.map((q) => q.id)).toEqual( + Array.from({ length: 16 }, (_, i) => i + 1) + ); + for (const dim of DIMENSIONS) { + expect(purrjungDimensionQuestions[dim]).toHaveLength(4); + expect(getPurrjungDimensionQuestionIds(dim)).toBe(purrjungDimensionQuestions[dim]); + } + }); + + it('assigns every question to the dimension that lists its ID', () => { + for (const question of purrjungQuestions) { + expect(purrjungDimensionQuestions[question.dimension]).toContain(question.id); + } + }); +}); + +describe('calculatePurrjungScores', () => { + it('returns 4 per dimension when every answer is 1', () => { + expect(calculatePurrjungScores(allAnswers(1))).toEqual({ EI: 4, SN: 4, TF: 4, JP: 4 }); + }); + + it('returns 20 per dimension when every answer is 5', () => { + expect(calculatePurrjungScores(allAnswers(5))).toEqual({ EI: 20, SN: 20, TF: 20, JP: 20 }); + }); + + it('treats missing answers as neutral (3)', () => { + expect(calculatePurrjungScores({})).toEqual({ EI: 12, SN: 12, TF: 12, JP: 12 }); + }); + + it('matches the exported range constants', () => { + expect(PURRJUNG_SCORE_MIN).toBe(4); + expect(PURRJUNG_SCORE_MAX).toBe(20); + expect(PURRJUNG_THRESHOLD).toBe(12); + }); +}); + +describe('determinePurrjungType', () => { + it('resolves low scores to E, S, T, J (TF direction is inverted versus the human test)', () => { + expect(determinePurrjungType({ EI: 4, SN: 4, TF: 4, JP: 4 })).toBe('ESTJ'); + }); + + it('resolves high scores to I, N, F, P', () => { + expect(determinePurrjungType({ EI: 20, SN: 20, TF: 20, JP: 20 })).toBe('INFP'); + }); + + it('resolves the exact midpoint to the left pole', () => { + expect(determinePurrjungType({ EI: 12, SN: 12, TF: 12, JP: 12 })).toBe('ESTJ'); + expect(determinePurrjungType({ EI: 13, SN: 13, TF: 13, JP: 13 })).toBe('INFP'); + }); +}); + +describe('calculatePurrjungPercentages', () => { + it('maps the minimum score to 100% left', () => { + const p = calculatePurrjungPercentages({ EI: 4, SN: 4, TF: 4, JP: 4 }); + expect(p).toEqual({ E: 100, I: 0, S: 100, N: 0, T: 100, F: 0, J: 100, P: 0 }); + }); + + it('maps the maximum score to 100% right', () => { + const p = calculatePurrjungPercentages({ EI: 20, SN: 20, TF: 20, JP: 20 }); + expect(p).toEqual({ E: 0, I: 100, S: 0, N: 100, T: 0, F: 100, J: 0, P: 100 }); + }); + + it('maps the midpoint to 50/50 and always sums each pair to 100', () => { + const p = calculatePurrjungPercentages({ EI: 12, SN: 12, TF: 12, JP: 12 }); + expect(p.E).toBe(50); + expect(p.T).toBe(50); + for (let score = 4; score <= 20; score++) { + const q = calculatePurrjungPercentages({ EI: score, SN: score, TF: score, JP: score }); + expect(q.E + q.I).toBe(100); + expect(q.S + q.N).toBe(100); + expect(q.T + q.F).toBe(100); + expect(q.J + q.P).toBe(100); + } + }); +}); + +describe('generatePurrjungResult', () => { + it('combines scores, type and percentages', () => { + const answers: TestAnswers = { ...allAnswers(3), 1: 1, 2: 1, 3: 1, 4: 1, 13: 5, 14: 5 }; + const result = generatePurrjungResult(answers); + expect(result.scores).toEqual({ EI: 4, SN: 12, TF: 12, JP: 16 }); + expect(result.type).toBe('ESTP'); + expect(result.percentages.E).toBe(100); + expect(result.percentages.P).toBe(75); + }); +}); + +describe('isPurrjungTestComplete', () => { + it('is true for exactly 16 answers and false otherwise', () => { + expect(isPurrjungTestComplete({})).toBe(false); + expect(isPurrjungTestComplete(allAnswers(3))).toBe(true); + const fifteen = allAnswers(3); + delete fifteen[16]; + expect(isPurrjungTestComplete(fifteen)).toBe(false); + }); +}); diff --git a/src/purrjungScoring.ts b/src/purrjungScoring.ts index fcae465..fd5e244 100644 --- a/src/purrjungScoring.ts +++ b/src/purrjungScoring.ts @@ -1,5 +1,12 @@ -import { purrjungDimensionQuestions, PURRJUNG_TOTAL_QUESTIONS, PURRJUNG_QUESTIONS_PER_DIMENSION } from './purrjungQuestions'; -import type { TestAnswers, DimensionScores, DimensionPercentages, TestResult } from './types'; +import { purrjungDimensionQuestions, PURRJUNG_TOTAL_QUESTIONS } from './purrjungQuestions.js'; +import { scoresToPercentages, scoresToType, sumDimensions, type DimensionPoles } from './scale.js'; +import type { + TestAnswers, + DimensionScores, + DimensionPercentages, + TestResult, + Dimension, +} from './types.js'; /** * PurrJung Cat Personality Test Scoring @@ -7,40 +14,41 @@ import type { TestAnswers, DimensionScores, DimensionPercentages, TestResult } f * Each dimension has 4 questions scored 1-5 * Total range per dimension: 4-20 * - * Dimension scoring direction (same as human OEJTS): + * Dimension scoring direction: * - EI: Low (4-12) = Social (E), High (13-20) = Solitary (I) * - SN: Low (4-12) = Routine (S), High (13-20) = Novelty (N) * - TF: Low (4-12) = Independent (T), High (13-20) = Bonded (F) * - JP: Low (4-12) = Structured (J), High (13-20) = Spontaneous (P) + * + * Note that TF runs T → F here, the opposite of the human OEJTS test (F → T). */ // Score range constants -export const PURRJUNG_SCORE_MIN = 4; // 4 questions × 1 = 4 +export const PURRJUNG_SCORE_MIN = 4; // 4 questions × 1 = 4 export const PURRJUNG_SCORE_MAX = 20; // 4 questions × 5 = 20 export const PURRJUNG_THRESHOLD = 12; // Midpoint of 4-20 range +/** Pole letters: `[low score, high score]` per dimension. */ +const PURRJUNG_POLES: DimensionPoles = { + EI: ['E', 'I'], // Social ↔ Solitary + SN: ['S', 'N'], // Routine ↔ Novelty + TF: ['T', 'F'], // Independent ↔ Bonded + JP: ['J', 'P'], // Structured ↔ Spontaneous +}; + /** * Calculate dimension scores from PurrJung test answers * Each dimension has 4 questions scored 1-5 * Total range per dimension: 4-20 + * Unanswered questions count as neutral (3). */ export function calculatePurrjungScores(answers: TestAnswers): DimensionScores { - const scores: DimensionScores = { EI: 0, SN: 0, TF: 0, JP: 0 }; - - for (const [dimension, questionIds] of Object.entries(purrjungDimensionQuestions)) { - scores[dimension as keyof DimensionScores] = questionIds.reduce( - (sum, qId) => sum + (answers[qId] ?? 3), // Default to neutral (3) if missing - 0 - ); - } - - return scores; + return sumDimensions(answers, purrjungDimensionQuestions); } /** * Determine cat personality type from dimension scores * - * Scoring direction matches human OEJTS: * - EI: Low = E (Social), High = I (Solitary) * - SN: Low = S (Routine), High = N (Novelty) * - TF: Low = T (Independent), High = F (Bonded) @@ -49,12 +57,7 @@ export function calculatePurrjungScores(answers: TestAnswers): DimensionScores { * Threshold: 12 (midpoint of 4-20 range) */ export function determinePurrjungType(scores: DimensionScores): string { - const e_i = scores.EI > PURRJUNG_THRESHOLD ? 'I' : 'E'; - const s_n = scores.SN > PURRJUNG_THRESHOLD ? 'N' : 'S'; - const t_f = scores.TF > PURRJUNG_THRESHOLD ? 'F' : 'T'; - const j_p = scores.JP > PURRJUNG_THRESHOLD ? 'P' : 'J'; - - return `${e_i}${s_n}${t_f}${j_p}`; + return scoresToType(scores, PURRJUNG_THRESHOLD, PURRJUNG_POLES); } /** @@ -62,28 +65,7 @@ export function determinePurrjungType(scores: DimensionScores): string { * Converts raw scores (4-20) to percentages (0-100) */ export function calculatePurrjungPercentages(scores: DimensionScores): DimensionPercentages { - const toRightPercentage = (score: number): number => { - // Convert 4-20 range to 0-100 - // Score 4 = 0% right, Score 20 = 100% right - const normalized = ((score - PURRJUNG_SCORE_MIN) / (PURRJUNG_SCORE_MAX - PURRJUNG_SCORE_MIN)) * 100; - return Math.round(normalized); - }; - - const eiRight = toRightPercentage(scores.EI); // I (Solitary) percentage - const snRight = toRightPercentage(scores.SN); // N (Novelty) percentage - const tfRight = toRightPercentage(scores.TF); // F (Bonded) percentage - const jpRight = toRightPercentage(scores.JP); // P (Spontaneous) percentage - - return { - E: 100 - eiRight, // Social - I: eiRight, // Solitary - S: 100 - snRight, // Routine - N: snRight, // Novelty - T: 100 - tfRight, // Independent - F: tfRight, // Bonded - J: 100 - jpRight, // Structured - P: jpRight, // Spontaneous - }; + return scoresToPercentages(scores, PURRJUNG_SCORE_MIN, PURRJUNG_SCORE_MAX, PURRJUNG_POLES); } /** @@ -98,7 +80,7 @@ export function generatePurrjungResult(answers: TestAnswers): TestResult { } /** - * Validate if all PurrJung test questions are answered + * Check whether exactly 16 answers have been recorded (count check only). */ export function isPurrjungTestComplete(answers: TestAnswers): boolean { return Object.keys(answers).length === PURRJUNG_TOTAL_QUESTIONS; @@ -107,6 +89,6 @@ export function isPurrjungTestComplete(answers: TestAnswers): boolean { /** * Get the question IDs for a specific dimension */ -export function getPurrjungDimensionQuestionIds(dimension: keyof DimensionScores): readonly number[] { +export function getPurrjungDimensionQuestionIds(dimension: Dimension): readonly number[] { return purrjungDimensionQuestions[dimension]; } diff --git a/src/scale.ts b/src/scale.ts new file mode 100644 index 0000000..eaabcdb --- /dev/null +++ b/src/scale.ts @@ -0,0 +1,112 @@ +/** + * Shared scoring engine for every bipolar Likert scale in this package. + * + * The full OEJTS test, the quick test, single-dimension tests and the PurrJung + * cat test all follow the same pattern: a set of question IDs per dimension, + * answers from 1 (left trait) to 5 (right trait), a score range derived from + * the question count, and a midpoint threshold that decides the pole letter. + * Only the question IDs, the range and the pole letters differ. + * + * This module is internal. The public API lives in `scoring.ts` and + * `purrjungScoring.ts`. + */ +import type { + Dimension, + DimensionPercentages, + DimensionQuestions, + DimensionScores, + TestAnswers, +} from './types.js'; + +/** The four Jungian dimensions, in canonical type-string order. */ +export const DIMENSIONS: readonly Dimension[] = ['EI', 'SN', 'TF', 'JP']; + +/** Answer value used in place of an unanswered question. */ +export const NEUTRAL_ANSWER = 3; + +/** Pole letters for each dimension: `[left, right]` = `[low score, high score]`. */ +export type DimensionPoles = Readonly>; + +/** Everything the engine needs to score one kind of test. */ +export interface ScaleSpec { + /** Question IDs grouped by dimension. */ + readonly questionIds: DimensionQuestions; + /** Lowest possible dimension score (question count × 1). */ + readonly min: number; + /** Highest possible dimension score (question count × 5). */ + readonly max: number; + /** Scores strictly above this value resolve to the right pole. */ + readonly threshold: number; + /** Pole letters per dimension. */ + readonly poles: DimensionPoles; +} + +/** Sum the answers for a list of question IDs, treating missing answers as neutral. */ +export function sumAnswers(answers: TestAnswers, questionIds: readonly number[]): number { + let sum = 0; + for (const id of questionIds) { + sum += answers[id] ?? NEUTRAL_ANSWER; + } + return sum; +} + +/** Sum answers for every dimension. */ +export function sumDimensions( + answers: TestAnswers, + questionIds: DimensionQuestions +): DimensionScores { + return { + EI: sumAnswers(answers, questionIds.EI), + SN: sumAnswers(answers, questionIds.SN), + TF: sumAnswers(answers, questionIds.TF), + JP: sumAnswers(answers, questionIds.JP), + }; +} + +/** Pick the pole letter for one dimension score. */ +export function poleForScore( + score: number, + threshold: number, + poles: readonly [left: string, right: string] +): string { + return score > threshold ? poles[1] : poles[0]; +} + +/** Build the four-letter type string from dimension scores. */ +export function scoresToType( + scores: DimensionScores, + threshold: number, + poles: DimensionPoles +): string { + return DIMENSIONS.map((dim) => poleForScore(scores[dim], threshold, poles[dim])).join(''); +} + +/** + * Map a raw score onto 0–100, where `min` → 0 and `max` → 100. + * The result is the share of the right pole; the left pole is `100 - result`. + */ +export function toRightPercentage(score: number, min: number, max: number): number { + return Math.round(((score - min) / (max - min)) * 100); +} + +/** Split every dimension score into a percentage for each of its two poles. */ +export function scoresToPercentages( + scores: DimensionScores, + min: number, + max: number, + poles: DimensionPoles +): DimensionPercentages { + const percentages: Record = {}; + for (const dim of DIMENSIONS) { + const right = toRightPercentage(scores[dim], min, max); + const [leftPole, rightPole] = poles[dim]; + percentages[leftPole] = 100 - right; + percentages[rightPole] = right; + } + return percentages as unknown as DimensionPercentages; +} + +/** True when every question in the list has an answer. */ +export function hasAllAnswers(answers: TestAnswers, questionIds: readonly number[]): boolean { + return questionIds.every((id) => answers[id] !== undefined); +} diff --git a/src/scoring.ts b/src/scoring.ts index f6821d6..772d9f4 100644 --- a/src/scoring.ts +++ b/src/scoring.ts @@ -1,4 +1,21 @@ -import { dimensionQuestions, TOTAL_QUESTIONS, quickTestQuestionIds, QUICK_TEST_TOTAL, QUICK_TEST_PER_DIMENSION } from './questions'; +import { + dimensionQuestions, + TOTAL_QUESTIONS, + quickTestQuestionIds, + QUICK_TEST_TOTAL, +} from './questions.js'; +import { + DIMENSIONS, + NEUTRAL_ANSWER, + hasAllAnswers, + poleForScore, + scoresToPercentages, + scoresToType, + sumAnswers, + sumDimensions, + toRightPercentage, + type DimensionPoles, +} from './scale.js'; import type { TestAnswers, DimensionScores, @@ -11,24 +28,45 @@ import type { TestConfidence, ConsistencyResult, TestConsistency, -} from './types'; +} from './types.js'; + +/** + * Pole letters for the human OEJTS test. + * Low scores (left traits) map to the first letter, high scores (right traits) to the second. + * + * - EI: E ↔ I + * - SN: S ↔ N + * - TF: F ↔ T (note the order: low = Feeling, high = Thinking) + * - JP: J ↔ P + */ +const OEJTS_POLES: DimensionPoles = { + EI: ['E', 'I'], + SN: ['S', 'N'], + TF: ['F', 'T'], + JP: ['J', 'P'], +}; + +// ============================================ +// Full Test (32 questions, 8 per dimension) +// ============================================ + +/** Number of questions per dimension */ +export const DIMENSION_QUESTIONS_COUNT = 8; + +/** Score range for one dimension of the full test: 8-40 */ +export const DIMENSION_SCORE_MIN = 8; +export const DIMENSION_SCORE_MAX = 40; +/** Midpoint of the 8-40 range; scores above it resolve to the right pole */ +export const DIMENSION_THRESHOLD = 24; /** * Calculate dimension scores from test answers * Each dimension has 8 questions scored 1-5 * Total range per dimension: 8-40 + * Unanswered questions count as neutral (3). */ export function calculateScores(answers: TestAnswers): DimensionScores { - const scores: DimensionScores = { EI: 0, SN: 0, TF: 0, JP: 0 }; - - for (const [dimension, questionIds] of Object.entries(dimensionQuestions)) { - scores[dimension as keyof DimensionScores] = questionIds.reduce( - (sum, qId) => sum + (answers[qId] ?? 3), // Default to neutral (3) if missing - 0 - ); - } - - return scores; + return sumDimensions(answers, dimensionQuestions); } /** @@ -45,14 +83,7 @@ export function calculateScores(answers: TestAnswers): DimensionScores { * Left traits are J-oriented, right traits are P-oriented */ export function determineType(scores: DimensionScores): string { - const threshold = 24; - - const e_i = scores.EI > threshold ? 'I' : 'E'; - const s_n = scores.SN > threshold ? 'N' : 'S'; - const t_f = scores.TF > threshold ? 'T' : 'F'; - const j_p = scores.JP > threshold ? 'P' : 'J'; - - return `${e_i}${s_n}${t_f}${j_p}`; + return scoresToType(scores, DIMENSION_THRESHOLD, OEJTS_POLES); } /** @@ -60,28 +91,7 @@ export function determineType(scores: DimensionScores): string { * Converts raw scores (8-40) to percentages (0-100) */ export function calculatePercentages(scores: DimensionScores): DimensionPercentages { - const toRightPercentage = (score: number): number => { - // Convert 8-40 range to 0-100 - // Score 8 = 0% right, Score 40 = 100% right - const normalized = ((score - 8) / 32) * 100; - return Math.round(normalized); - }; - - const eiRight = toRightPercentage(scores.EI); // I percentage - const snRight = toRightPercentage(scores.SN); // N percentage - const tfRight = toRightPercentage(scores.TF); // T percentage - const jpRight = toRightPercentage(scores.JP); // P percentage - - return { - E: 100 - eiRight, - I: eiRight, - S: 100 - snRight, - N: snRight, - F: 100 - tfRight, - T: tfRight, - J: 100 - jpRight, - P: jpRight, - }; + return scoresToPercentages(scores, DIMENSION_SCORE_MIN, DIMENSION_SCORE_MAX, OEJTS_POLES); } /** @@ -96,32 +106,35 @@ export function generateResult(answers: TestAnswers): TestResult { } /** - * Validate if all questions are answered + * Check whether the expected number of questions has been answered. + * + * This is a count check only: it does not verify which IDs are present or that + * values fall within 1-5. Use `isDimensionTestComplete` for ID-level checks. */ -export function isTestComplete(answers: TestAnswers, totalQuestions: number = TOTAL_QUESTIONS): boolean { +export function isTestComplete( + answers: TestAnswers, + totalQuestions: number = TOTAL_QUESTIONS +): boolean { return Object.keys(answers).length === totalQuestions; } // ============================================ -// Quick Test Mode Functions (8 questions) +// Quick Test Mode (8 questions, 2 per dimension) // ============================================ +/** Score range for one dimension of the quick test: 2-10 */ +const QUICK_SCORE_MIN = 2; +const QUICK_SCORE_MAX = 10; +/** Midpoint of the 2-10 range */ +const QUICK_THRESHOLD = 6; + /** * Calculate dimension scores from quick test answers * Each dimension has 2 questions scored 1-5 * Total range per dimension: 2-10 */ export function calculateQuickScores(answers: TestAnswers): DimensionScores { - const scores: DimensionScores = { EI: 0, SN: 0, TF: 0, JP: 0 }; - - for (const [dimension, questionIds] of Object.entries(quickTestQuestionIds)) { - scores[dimension as keyof DimensionScores] = questionIds.reduce( - (sum, qId) => sum + (answers[qId] ?? 3), // Default to neutral (3) if missing - 0 - ); - } - - return scores; + return sumDimensions(answers, quickTestQuestionIds); } /** @@ -129,14 +142,7 @@ export function calculateQuickScores(answers: TestAnswers): DimensionScores { * Threshold adjusted for 2-10 range (midpoint = 6) */ export function determineQuickType(scores: DimensionScores): string { - const threshold = 6; // Midpoint of 2-10 range - - const e_i = scores.EI > threshold ? 'I' : 'E'; - const s_n = scores.SN > threshold ? 'N' : 'S'; - const t_f = scores.TF > threshold ? 'T' : 'F'; - const j_p = scores.JP > threshold ? 'P' : 'J'; - - return `${e_i}${s_n}${t_f}${j_p}`; + return scoresToType(scores, QUICK_THRESHOLD, OEJTS_POLES); } /** @@ -144,28 +150,7 @@ export function determineQuickType(scores: DimensionScores): string { * Converts raw scores (2-10) to percentages (0-100) */ export function calculateQuickPercentages(scores: DimensionScores): DimensionPercentages { - const toRightPercentage = (score: number): number => { - // Convert 2-10 range to 0-100 - // Score 2 = 0% right, Score 10 = 100% right - const normalized = ((score - 2) / 8) * 100; - return Math.round(normalized); - }; - - const eiRight = toRightPercentage(scores.EI); // I percentage - const snRight = toRightPercentage(scores.SN); // N percentage - const tfRight = toRightPercentage(scores.TF); // T percentage - const jpRight = toRightPercentage(scores.JP); // P percentage - - return { - E: 100 - eiRight, - I: eiRight, - S: 100 - snRight, - N: snRight, - F: 100 - tfRight, - T: tfRight, - J: 100 - jpRight, - P: jpRight, - }; + return scoresToPercentages(scores, QUICK_SCORE_MIN, QUICK_SCORE_MAX, OEJTS_POLES); } /** @@ -180,35 +165,16 @@ export function generateQuickResult(answers: TestAnswers): TestResult { } /** - * Validate if all quick test questions are answered + * Check whether exactly 8 answers have been recorded (count check only). */ export function isQuickTestComplete(answers: TestAnswers): boolean { return Object.keys(answers).length === QUICK_TEST_TOTAL; } // ============================================ -// Single Dimension Test Functions (8 questions) +// Single Dimension Test (8 questions) // ============================================ -/** Number of questions per dimension */ -export const DIMENSION_QUESTIONS_COUNT = 8; - -/** Score range for single dimension: 8-40 */ -export const DIMENSION_SCORE_MIN = 8; -export const DIMENSION_SCORE_MAX = 40; -export const DIMENSION_THRESHOLD = 24; // Midpoint - -/** - * Dimension preference mapping - * Left preference (low score) -> Right preference (high score) - */ -const dimensionPreferences: Record = { - EI: { left: 'E', right: 'I' }, - SN: { left: 'S', right: 'N' }, - TF: { left: 'F', right: 'T' }, - JP: { left: 'J', right: 'P' }, -}; - /** * Calculate single dimension score from test answers * @param answers - TestAnswers with dimension questions answered @@ -216,11 +182,7 @@ const dimensionPreferences: Record = * @returns Score in 8-40 range */ export function calculateDimensionScore(answers: TestAnswers, dimension: Dimension): number { - const questionIds = dimensionQuestions[dimension]; - return questionIds.reduce( - (sum, qId) => sum + (answers[qId] ?? 3), // Default to neutral (3) if missing - 0 - ); + return sumAnswers(answers, dimensionQuestions[dimension]); } /** @@ -230,8 +192,7 @@ export function calculateDimensionScore(answers: TestAnswers, dimension: Dimensi * @returns Preference letter (E/I, S/N, F/T, J/P) */ export function determineDimensionPreference(score: number, dimension: Dimension): string { - const prefs = dimensionPreferences[dimension]; - return score > DIMENSION_THRESHOLD ? prefs.right : prefs.left; + return poleForScore(score, DIMENSION_THRESHOLD, OEJTS_POLES[dimension]); } /** @@ -240,12 +201,8 @@ export function determineDimensionPreference(score: number, dimension: Dimension * @returns Object with left and right percentages (total = 100) */ export function calculateDimensionPercentages(score: number): { left: number; right: number } { - // Convert 8-40 range to 0-100 (right percentage) - const rightPercent = Math.round(((score - DIMENSION_SCORE_MIN) / (DIMENSION_SCORE_MAX - DIMENSION_SCORE_MIN)) * 100); - return { - left: 100 - rightPercent, - right: rightPercent, - }; + const right = toRightPercentage(score, DIMENSION_SCORE_MIN, DIMENSION_SCORE_MAX); + return { left: 100 - right, right }; } /** @@ -254,7 +211,10 @@ export function calculateDimensionPercentages(score: number): { left: number; ri * @param dimension - Target dimension * @returns SingleDimensionResult with score, preference, and percentages */ -export function generateDimensionResult(answers: TestAnswers, dimension: Dimension): SingleDimensionResult { +export function generateDimensionResult( + answers: TestAnswers, + dimension: Dimension +): SingleDimensionResult { const score = calculateDimensionScore(answers, dimension); const preference = determineDimensionPreference(score, dimension); const { left, right } = calculateDimensionPercentages(score); @@ -275,8 +235,7 @@ export function generateDimensionResult(answers: TestAnswers, dimension: Dimensi * @returns True if all 8 questions for the dimension are answered */ export function isDimensionTestComplete(answers: TestAnswers, dimension: Dimension): boolean { - const questionIds = dimensionQuestions[dimension]; - return questionIds.every(qId => answers[qId] !== undefined); + return hasAllAnswers(answers, dimensionQuestions[dimension]); } /** @@ -289,7 +248,7 @@ export function getDimensionQuestionIds(dimension: Dimension): readonly number[] } // ============================================ -// Test Quality Metrics Functions +// Test Quality Metrics // ============================================ /** @@ -305,6 +264,9 @@ const CONFIDENCE_THRESHOLDS = { slight: 2, } as const; +/** Largest possible distance from the midpoint (24 → 8 or 24 → 40). */ +const MAX_DISTANCE = DIMENSION_SCORE_MAX - DIMENSION_THRESHOLD; + /** * Calculate confidence level from distance to threshold * @param distance - Absolute distance from threshold (0-16) @@ -323,18 +285,15 @@ export function getConfidenceLevel(distance: number): ConfidenceLevel { * @param dimension - The dimension being measured * @returns DimensionConfidence with level, distance, and percentage */ -export function calculateDimensionConfidence(score: number, dimension: Dimension): DimensionConfidence { +export function calculateDimensionConfidence( + score: number, + dimension: Dimension +): DimensionConfidence { const distance = Math.abs(score - DIMENSION_THRESHOLD); const level = getConfidenceLevel(distance); - // Max distance is 16 (from 24 to 8 or 24 to 40), convert to percentage - const percentage = Math.round((distance / 16) * 100); + const percentage = Math.round((distance / MAX_DISTANCE) * 100); - return { - dimension, - level, - distance, - percentage, - }; + return { dimension, level, distance, percentage }; } /** @@ -343,27 +302,16 @@ export function calculateDimensionConfidence(score: number, dimension: Dimension * @returns TestConfidence with per-dimension confidence and clarity index */ export function calculateTestConfidence(scores: DimensionScores): TestConfidence { - const dimensions: Dimension[] = ['EI', 'SN', 'TF', 'JP']; - const confidences: Record = {}; - - let totalDistance = 0; - for (const dim of dimensions) { - const confidence = calculateDimensionConfidence(scores[dim], dim); - confidences[dim] = confidence; - totalDistance += confidence.distance; - } + const EI = calculateDimensionConfidence(scores.EI, 'EI'); + const SN = calculateDimensionConfidence(scores.SN, 'SN'); + const TF = calculateDimensionConfidence(scores.TF, 'TF'); + const JP = calculateDimensionConfidence(scores.JP, 'JP'); - // Clarity index: average distance normalized to 0-100 - // Max total distance = 16 * 4 = 64 - const clarityIndex = Math.round((totalDistance / 64) * 100); + // Clarity index: total distance normalized to 0-100 (max = 16 × 4 = 64) + const totalDistance = EI.distance + SN.distance + TF.distance + JP.distance; + const clarityIndex = Math.round((totalDistance / (MAX_DISTANCE * DIMENSIONS.length)) * 100); - return { - EI: confidences.EI, - SN: confidences.SN, - TF: confidences.TF, - JP: confidences.JP, - clarityIndex, - }; + return { EI, SN, TF, JP, clarityIndex }; } /** @@ -373,13 +321,12 @@ export function calculateTestConfidence(scores: DimensionScores): TestConfidence * @returns Variance of the 8 answers (0 = all same, higher = more varied) */ function calculateAnswerVariance(answers: TestAnswers, dimension: Dimension): number { - const questionIds = dimensionQuestions[dimension]; - const values = questionIds.map(qId => answers[qId] ?? 3); + const values = dimensionQuestions[dimension].map((qId) => answers[qId] ?? NEUTRAL_ANSWER); if (values.length === 0) return 0; const mean = values.reduce((a, b) => a + b, 0) / values.length; - const squaredDiffs = values.map(v => Math.pow(v - mean, 2)); + const squaredDiffs = values.map((v) => (v - mean) ** 2); return squaredDiffs.reduce((a, b) => a + b, 0) / values.length; } @@ -388,21 +335,23 @@ const CONSISTENCY_THRESHOLD = 3; /** * Check consistency of answers within a dimension - * Flags question pairs where answers differ significantly + * Flags adjacent question pairs whose answers differ by 3 or more * @param answers - Test answers * @param dimension - Target dimension * @returns ConsistencyResult with variance and flagged pairs */ -export function checkDimensionConsistency(answers: TestAnswers, dimension: Dimension): ConsistencyResult { +export function checkDimensionConsistency( + answers: TestAnswers, + dimension: Dimension +): ConsistencyResult { const questionIds = dimensionQuestions[dimension]; const variance = calculateAnswerVariance(answers, dimension); const flaggedPairs: [number, number][] = []; - // Compare adjacent question pairs within dimension for (let i = 0; i < questionIds.length - 1; i++) { const q1 = questionIds[i]; const q2 = questionIds[i + 1]; - const diff = Math.abs((answers[q1] ?? 3) - (answers[q2] ?? 3)); + const diff = Math.abs((answers[q1] ?? NEUTRAL_ANSWER) - (answers[q2] ?? NEUTRAL_ANSWER)); if (diff >= CONSISTENCY_THRESHOLD) { flaggedPairs.push([q1, q2]); } @@ -422,26 +371,21 @@ export function checkDimensionConsistency(answers: TestAnswers, dimension: Dimen * @returns TestConsistency with per-dimension results and overall status */ export function checkTestConsistency(answers: TestAnswers): TestConsistency { - const dimensions: Dimension[] = ['EI', 'SN', 'TF', 'JP']; - const results: Record = {}; - const warnings: string[] = []; - - let allConsistent = true; - for (const dim of dimensions) { - const result = checkDimensionConsistency(answers, dim); - results[dim] = result; - if (!result.isConsistent) { - allConsistent = false; - warnings.push(`Inconsistent answers in ${dim} dimension`); - } - } + const EI = checkDimensionConsistency(answers, 'EI'); + const SN = checkDimensionConsistency(answers, 'SN'); + const TF = checkDimensionConsistency(answers, 'TF'); + const JP = checkDimensionConsistency(answers, 'JP'); + + const warnings = [EI, SN, TF, JP] + .filter((result) => !result.isConsistent) + .map((result) => `Inconsistent answers in ${result.dimension} dimension`); return { - EI: results.EI, - SN: results.SN, - TF: results.TF, - JP: results.JP, - overallConsistent: allConsistent, + EI, + SN, + TF, + JP, + overallConsistent: warnings.length === 0, warnings, }; } diff --git a/src/types.ts b/src/types.ts index 53d5c63..115d034 100644 --- a/src/types.ts +++ b/src/types.ts @@ -47,8 +47,12 @@ export interface SingleDimensionResult { rightPercent: number; // 0-100 } -// Multilingual text (en is required, others optional for fallback) -export interface BilingualText { +/** + * Multilingual text. `en` is always present; every other locale is optional so + * consumers can carry partial translations of their own content. + * Use `getLocalizedText()` to read a locale with English fallback. + */ +export interface MultilingualText { [locale: string]: string | undefined; en: string; zh?: string; @@ -57,13 +61,16 @@ export interface BilingualText { 'zh-tw'?: string; } +/** @deprecated Use {@link MultilingualText}. Kept as an alias for existing imports. */ +export type BilingualText = MultilingualText; + // Question pair (bipolar scale) export interface QuestionPair { id: number; dimension: Dimension; - title?: BilingualText; // Optional question prompt to guide users - leftTrait: BilingualText; - rightTrait: BilingualText; + title?: MultilingualText; // Optional question prompt to guide users + leftTrait: MultilingualText; + rightTrait: MultilingualText; } // Dimension to question IDs mapping From 157abf24aa3bd3240409a70722e886aa5362d266 Mon Sep 17 00:00:00 2001 From: ya-luotao Date: Sat, 10 Oct 2026 06:11:20 +0800 Subject: [PATCH 3/5] ci: add CI matrix, release-tag guard, Dependabot and issue/PR templates CI runs typecheck, Prettier, tests, build, a Node ESM import smoke test and publint/attw on Node 20, 22 and 24 for every push and PR. The publish workflow now triggers on a published release (not a created draft), refuses to publish when the tag does not match package.json, and no longer bumps versions or pushes to main from CI. prepublishOnly runs the full check. Issue forms: bug report and translation correction (the fields TRANSLATIONS.md asks reporters for). PR template carries the contract checklist. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01FGXjqn5CZbnVLNtHjkp45t --- .github/ISSUE_TEMPLATE/bug_report.yml | 31 ++++++++++ .github/ISSUE_TEMPLATE/config.yml | 8 +++ .../ISSUE_TEMPLATE/translation_correction.yml | 61 +++++++++++++++++++ .github/PULL_REQUEST_TEMPLATE.md | 16 +++++ .github/dependabot.yml | 14 +++++ .github/workflows/ci.yml | 56 +++++++++++++++++ .github/workflows/publish.yml | 58 ++++++++---------- 7 files changed, 212 insertions(+), 32 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.yml create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/ISSUE_TEMPLATE/translation_correction.yml create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/ci.yml diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..ac20c08 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,31 @@ +name: Bug report +description: A function returns the wrong result, a type is wrong, or the package fails to load. +labels: [bug] +body: + - type: textarea + id: what + attributes: + label: What happened? + description: What did you call, what did you get, and what did you expect? + placeholder: | + generateResult({ ... }) returned type "ENFP" but the EI score was 24, which should be E. + validations: + required: true + - type: textarea + id: repro + attributes: + label: Minimal reproduction + description: A snippet we can paste into a test. Please include the answers object. + render: ts + - type: input + id: version + attributes: + label: Package version + placeholder: '@openjung/core@1.0.1, or a commit SHA' + validations: + required: true + - type: input + id: runtime + attributes: + label: Runtime + placeholder: Node 24 / Vite 7 / Cloudflare Workers / React Native diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..b646e3c --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: true +contact_links: + - name: openjung.org website + url: https://openjung.org + about: Try the test, or report a problem with the site itself. + - name: OEJTS source instrument + url: https://openpsychometrics.org/tests/OEJTS/ + about: The original Open Extended Jungian Type Scales by Open Psychometrics. diff --git a/.github/ISSUE_TEMPLATE/translation_correction.yml b/.github/ISSUE_TEMPLATE/translation_correction.yml new file mode 100644 index 0000000..1707b9f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/translation_correction.yml @@ -0,0 +1,61 @@ +name: Translation correction +description: Propose better wording for a question in one locale. Please do not include your own answers or results. +labels: [translation] +body: + - type: markdown + attributes: + value: | + Thank you. 39 of the 44 locales are AI-assisted drafts that have not had native-speaker review, + so corrections are very welcome. See TRANSLATIONS.md for the known review priorities. + Changing the **direction** of a pair (which side scores 1 and which scores 5) is a scoring change, + not a translation fix; please open a bug report for that instead. + - type: input + id: locale + attributes: + label: Locale + description: The locale code as used in the package, e.g. `de`, `pt-br`, `zh-tw`. + validations: + required: true + - type: input + id: question + attributes: + label: Question ID + description: 1–32 for the OEJTS questions, or "PurrJung 1–16" for the cat test. + validations: + required: true + - type: dropdown + id: field + attributes: + label: Field + options: + - title + - leftTrait + - rightTrait + validations: + required: true + - type: textarea + id: current + attributes: + label: Current wording + validations: + required: true + - type: textarea + id: proposed + attributes: + label: Proposed wording + validations: + required: true + - type: textarea + id: reason + attributes: + label: Why is the new wording better? + description: Register, idiom, ambiguity, cultural fit, gendered address, reading level, etc. + validations: + required: true + - type: checkboxes + id: speaker + attributes: + label: Your relationship to this language + options: + - label: I am a native or fluent speaker of this locale. + - label: I compared the proposal against the English source text. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..be5b2ee --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,16 @@ +## What + + + +## Contracts + + + +- [ ] Question IDs, dimension membership and left/right direction are unchanged +- [ ] Every name in `src/index.test.ts` still exports (or this is a breaking change and the title says so) +- [ ] Added or changed locale strings keep the `[title, leftTrait, rightTrait]` order +- [ ] `npm run check` passes locally + +## Notes for review + + diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..55b1148 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,14 @@ +version: 2 +updates: + - package-ecosystem: npm + directory: / + schedule: + interval: weekly + groups: + dev-dependencies: + dependency-type: development + update-types: [minor, patch] + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..8f0829a --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,56 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + check: + name: Node ${{ matrix.node }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + node: [20, 22, 24] + steps: + - uses: actions/checkout@v5 + + - uses: actions/setup-node@v5 + with: + node-version: ${{ matrix.node }} + cache: npm + + - run: npm ci + + - name: Typecheck + run: npm run typecheck + + - name: Format + run: npm run format:check + + - name: Test + run: npm test + + - name: Build + run: npm run build + + - name: Smoke-test the built package under Node ESM + run: | + node --input-type=module -e " + const api = await import('@openjung/core'); + const answers = Object.fromEntries(Array.from({ length: 32 }, (_, i) => [i + 1, 3])); + const result = api.generateResult(answers); + if (result.type !== 'ESFJ') throw new Error('unexpected neutral type ' + result.type); + console.log('ok:', Object.keys(api).length, 'exports,', api.SUPPORTED_LOCALES.length, 'locales'); + " + + - name: Lint package exports + run: npm run check:package diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 3ce5ba7..3122e72 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1,49 +1,43 @@ name: Publish to npm +# Publishes the version in package.json when a GitHub release is published. +# The release tag must match the package version (v1.2.3 ↔ "version": "1.2.3"). +# Bump the version with `npm version ` before tagging; see CONTRIBUTING.md. + on: release: - types: [created] + types: [published] workflow_dispatch: - inputs: - version: - description: 'Version bump type (patch, minor, major) or specific version' - required: false - default: 'patch' + +permissions: + contents: read + id-token: write jobs: publish: runs-on: ubuntu-latest - permissions: - contents: write - id-token: write steps: - - name: Checkout - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - - name: Setup Node.js - uses: actions/setup-node@v4 + - uses: actions/setup-node@v5 with: - node-version: '20' - registry-url: 'https://registry.npmjs.org' - - - name: Install dependencies - run: npm ci + node-version: 24 + cache: npm + registry-url: https://registry.npmjs.org - - name: Run tests - run: npm test + - run: npm ci - - name: Build - run: npm run build - - - name: Bump version (manual trigger only) - if: github.event_name == 'workflow_dispatch' + - name: Check that the release tag matches package.json + if: github.event_name == 'release' run: | - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - npm version ${{ github.event.inputs.version }} -m "Release v%s" - git push --follow-tags - - - name: Publish to npm - run: npm publish --access public --provenance + expected="v$(node -p "require('./package.json').version")" + if [ "${{ github.event.release.tag_name }}" != "$expected" ]; then + echo "Release tag ${{ github.event.release.tag_name }} does not match package version $expected" + exit 1 + fi + + # prepublishOnly runs clean, typecheck, format check, tests, build and publint. + - name: Publish + run: npm publish env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} From b00e8e39b93aa3067ef68ca98df3a15fe4f3725b Mon Sep 17 00:00:00 2001 From: ya-luotao Date: Sat, 10 Oct 2026 06:11:20 +0800 Subject: [PATCH 4/5] docs: rewrite README; add CHANGELOG, CONTRIBUTING, LICENSE and LICENSE-DATA README now documents every public area (full, quick, single-dimension, quality metrics, locales, PurrJung) with a scoring-model table that makes the PurrJung TF inversion explicit, and carries CI/license badges. LICENSE is plain MIT so GitHub detects it; LICENSE-DATA restates the CC BY-NC-SA 4.0 terms on the questionnaire content. CONTRIBUTING lists the contracts that must not change silently and the release steps. CHANGELOG seeds Unreleased with everything since @openmbti/core@1.0.1. TRANSLATIONS.md and the skill docs are only reformatted by Prettier. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01FGXjqn5CZbnVLNtHjkp45t --- .claude/skills/openjung-api/SKILL.md | 35 +-- .../openjung-api/references/api_reference.md | 196 +++++++++------- CHANGELOG.md | 51 ++++ CONTRIBUTING.md | 58 +++++ LICENSE | 21 ++ LICENSE-DATA | 26 +++ README.md | 218 ++++++++++++------ TRANSLATIONS.md | 30 +-- 8 files changed, 444 insertions(+), 191 deletions(-) create mode 100644 CHANGELOG.md create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE create mode 100644 LICENSE-DATA diff --git a/.claude/skills/openjung-api/SKILL.md b/.claude/skills/openjung-api/SKILL.md index d2f6bc0..51756b3 100644 --- a/.claude/skills/openjung-api/SKILL.md +++ b/.claude/skills/openjung-api/SKILL.md @@ -19,17 +19,17 @@ OpenJung is an open-source personality test based on OEJTS (Open Extended Jungia ```javascript // 1. Fetch questions -const { questions } = await fetch('https://openjung.org/api/questions').then(r => r.json()); +const { questions } = await fetch('https://openjung.org/api/questions').then((r) => r.json()); // 2. Collect user answers (32 questions, values 1-5) -const answers = { "1": 3, "2": 5, /* ... all 32 */ "32": 4 }; +const answers = { 1: 3, 2: 5, /* ... all 32 */ 32: 4 }; // 3. Calculate result const { result } = await fetch('https://openjung.org/api/calculate', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ answers }) -}).then(r => r.json()); + body: JSON.stringify({ answers }), +}).then((r) => r.json()); console.log(result.type); // "ENFP" ``` @@ -45,6 +45,7 @@ GET /api/questions?locale=en ``` **Response:** + ```json { "totalQuestions": 32, @@ -73,6 +74,7 @@ POST /api/calculate ``` **Response:** + ```json { "result": { @@ -120,6 +122,7 @@ Get aggregate test statistics. **Scale**: 1-5 Likert (1 = strongly left trait, 5 = strongly right trait) **Dimensions** (8 questions each): + - **EI**: Extroversion (E) ↔ Introversion (I) - **SN**: Sensing (S) ↔ Intuition (N) - **TF**: Thinking (T) ↔ Feeling (F) @@ -128,6 +131,7 @@ Get aggregate test statistics. **Score Range**: 8-40 per dimension (8 questions × 1-5 scale) **Threshold**: 24 (midpoint) + - Score ≤ 24 → left preference (E, S, F, J) - Score > 24 → right preference (I, N, T, P) @@ -135,28 +139,29 @@ Get aggregate test statistics. ## Question Mapping -| Dimension | Question IDs | -|-----------|--------------| -| JP | 1, 5, 9, 13, 17, 21, 25, 29 | -| TF | 2, 6, 10, 14, 18, 22, 26, 30 | -| EI | 3, 7, 11, 15, 19, 23, 27, 31 | -| SN | 4, 8, 12, 16, 20, 24, 28, 32 | +| Dimension | Question IDs | +| --------- | ---------------------------- | +| JP | 1, 5, 9, 13, 17, 21, 25, 29 | +| TF | 2, 6, 10, 14, 18, 22, 26, 30 | +| EI | 3, 7, 11, 15, 19, 23, 27, 31 | +| SN | 4, 8, 12, 16, 20, 24, 28, 32 | ## Localization 44 languages supported. Pass `locale` parameter to endpoints: + - `en` (English), `zh` (Simplified Chinese), `ja` (Japanese), `ko` (Korean) - `es` (Spanish), `fr` (French), `de` (German), `pt` (Portuguese) - And 36 more... ## Error Handling -| Error Code | Description | -|------------|-------------| -| `INVALID_BODY` | Malformed JSON | -| `INVALID_PARAMS` | Missing/wrong answers format | +| Error Code | Description | +| -------------------- | ----------------------------- | +| `INVALID_BODY` | Malformed JSON | +| `INVALID_PARAMS` | Missing/wrong answers format | | `INCOMPLETE_ANSWERS` | Not all 32 questions answered | -| `CALCULATION_FAILED` | Server error | +| `CALCULATION_FAILED` | Server error | ## Resources diff --git a/.claude/skills/openjung-api/references/api_reference.md b/.claude/skills/openjung-api/references/api_reference.md index da48bbb..7b90a5f 100644 --- a/.claude/skills/openjung-api/references/api_reference.md +++ b/.claude/skills/openjung-api/references/api_reference.md @@ -21,23 +21,27 @@ interface TestAnswers { // Dimension raw scores interface DimensionScores { - EI: number; // 8-40 - SN: number; // 8-40 - TF: number; // 8-40 - JP: number; // 8-40 + EI: number; // 8-40 + SN: number; // 8-40 + TF: number; // 8-40 + JP: number; // 8-40 } // Percentage breakdown interface DimensionPercentages { - E: number; I: number; // sum to 100 - S: number; N: number; // sum to 100 - T: number; F: number; // sum to 100 - J: number; P: number; // sum to 100 + E: number; + I: number; // sum to 100 + S: number; + N: number; // sum to 100 + T: number; + F: number; // sum to 100 + J: number; + P: number; // sum to 100 } // Complete test result interface TestResult { - type: string; // "ENFP", "INTJ", etc. + type: string; // "ENFP", "INTJ", etc. scores: DimensionScores; percentages: DimensionPercentages; } @@ -60,9 +64,9 @@ interface ApiResult extends TestResult { interface Question { id: number; dimension: 'EI' | 'SN' | 'TF' | 'JP'; - title?: string; // Optional guidance text - leftTrait: string; // Left option (score 1) - rightTrait: string; // Right option (score 5) + title?: string; // Optional guidance text + leftTrait: string; // Left option (score 1) + rightTrait: string; // Right option (score 5) } // Confidence metrics @@ -71,8 +75,8 @@ type ConfidenceLevel = 'strong' | 'moderate' | 'slight' | 'balanced'; interface DimensionConfidence { dimension: 'EI' | 'SN' | 'TF' | 'JP'; level: ConfidenceLevel; - distance: number; // Distance from threshold (24) - percentage: number; // 0-100 confidence + distance: number; // Distance from threshold (24) + percentage: number; // 0-100 confidence } ``` @@ -85,15 +89,18 @@ interface DimensionConfidence { **Purpose**: Retrieve all 32 OEJTS test questions **Query Parameters:** -| Parameter | Type | Default | Description | -|-----------|------|---------|-------------| -| locale | string | "en" | Language code for localized text | + +| Parameter | Type | Default | Description | +| --------- | ------ | ------- | -------------------------------- | +| locale | string | "en" | Language code for localized text | **Headers:** + - `Access-Control-Allow-Origin: *` (CORS enabled) - `Cache-Control: public, max-age=86400` (24h cache) **Success Response (200):** + ```json { "totalQuestions": 32, @@ -121,6 +128,7 @@ interface DimensionConfidence { **Purpose**: Calculate personality type from submitted answers **Request Body:** + ```typescript { answers: Record; // Required: all 32 answers @@ -130,11 +138,13 @@ interface DimensionConfidence { ``` **Validation Rules:** + - All 32 questions must be answered - Answer values must be integers 1-5 - Answer keys must be string numbers "1" through "32" **Success Response (200):** + ```json { "result": { @@ -181,6 +191,7 @@ interface DimensionConfidence { ``` **Error Response (400):** + ```json { "error": "INCOMPLETE_ANSWERS", @@ -196,6 +207,7 @@ interface DimensionConfidence { **Purpose**: Record a completed test result (analytics, non-blocking) **Request Body:** + ```typescript { answers: Record; @@ -209,6 +221,7 @@ interface DimensionConfidence { ``` **Success Response (201):** + ```json { "success": true, @@ -223,9 +236,11 @@ interface DimensionConfidence { **Purpose**: Retrieve aggregate test statistics **Headers:** + - `Cache-Control: public, max-age=300` (5min cache) **Success Response (200):** + ```json { "totalTests": 15234, @@ -238,9 +253,7 @@ interface DimensionConfidence { { "locale": "en", "count": 8234 }, { "locale": "zh", "count": 4123 } ], - "recentTrend": [ - { "date": "2026-01-01", "count": 234 } - ], + "recentTrend": [{ "date": "2026-01-01", "count": 234 }], "lastUpdated": "2026-01-28T12:34:56Z" } ``` @@ -257,7 +270,7 @@ const dimensionQuestions = { EI: [3, 7, 11, 15, 19, 23, 27, 31], SN: [4, 8, 12, 16, 20, 24, 28, 32], TF: [2, 6, 10, 14, 18, 22, 26, 30], - JP: [1, 5, 9, 13, 17, 21, 25, 29] + JP: [1, 5, 9, 13, 17, 21, 25, 29], }; function calculateScores(answers: TestAnswers): DimensionScores { @@ -278,8 +291,8 @@ function determineType(scores: DimensionScores): string { return [ scores.EI <= THRESHOLD ? 'E' : 'I', scores.SN <= THRESHOLD ? 'S' : 'N', - scores.TF <= THRESHOLD ? 'F' : 'T', // Note: TF inverted - scores.JP <= THRESHOLD ? 'J' : 'P' + scores.TF <= THRESHOLD ? 'F' : 'T', // Note: TF inverted + scores.JP <= THRESHOLD ? 'J' : 'P', ].join(''); } ``` @@ -298,7 +311,7 @@ function calculatePercentages(scores: DimensionScores): DimensionPercentages { T: toPercent(scores.TF), F: 100 - toPercent(scores.TF), P: toPercent(scores.JP), - J: 100 - toPercent(scores.JP) + J: 100 - toPercent(scores.JP), }; } ``` @@ -308,18 +321,21 @@ function calculatePercentages(scores: DimensionScores): DimensionPercentages { ## Test Modes ### Full Test (32 Questions) + - Default mode - All 32 OEJTS questions - Most accurate results - Score range: 8-40 per dimension ### Quick Test (8 Questions) + - 2 questions per dimension - Selected questions: [3, 15, 24, 32, 22, 14, 9, 13] - Score range: 2-10 per dimension - Threshold: 6 ### Dimension Test (8 Questions) + - Single dimension focus - 8 questions for selected dimension only - Returns single dimension result @@ -328,40 +344,40 @@ function calculatePercentages(scores: DimensionScores): DimensionPercentages { ## All 32 Questions -| ID | Dim | Left Trait | Right Trait | -|----|-----|------------|-------------| -| 1 | JP | Makes lists | Relies on memory | -| 2 | TF | Skeptical | Wants to believe | -| 3 | EI | Bored by time alone | Needs time alone | -| 4 | SN | Accepts things as they are | Unsatisfied with the status quo | -| 5 | JP | Keeps a clean room | Tolerates a messy room | -| 6 | TF | Thinks "robotic" is an insult | Thinks "robotic" is a compliment | -| 7 | EI | Energized by interaction | Drained by interaction | -| 8 | SN | Prefers routine tasks | Prefers new challenges | -| 9 | JP | Plans social events ahead | Goes with the flow | -| 10 | TF | Sentimental | Unsentimental | -| 11 | EI | Comfortable in groups | Uncomfortable in groups | -| 12 | SN | Focuses on details | Focuses on big picture | -| 13 | JP | Prepares for meetings | Wings it | -| 14 | TF | Tenderhearted | Tough-minded | -| 15 | EI | Initiates conversations | Waits to be approached | -| 16 | SN | Works with facts | Works with ideas | -| 17 | JP | Finishes tasks early | Finishes at deadline | -| 18 | TF | Values harmony | Values truth | -| 19 | EI | Thinks out loud | Thinks inside head | -| 20 | SN | Trusts experience | Trusts intuition | -| 21 | JP | Follows schedules | Follows impulses | -| 22 | TF | Empathetic | Analytical | -| 23 | EI | Joins group activities | Prefers one-on-one | -| 24 | SN | Practical | Theoretical | -| 25 | JP | Organized workspace | Creative chaos | -| 26 | TF | Decides with heart | Decides with head | -| 27 | EI | Outgoing | Reserved | -| 28 | SN | Concrete | Abstract | -| 29 | JP | Punctual | Flexible with time | -| 30 | TF | Personal approach | Impersonal approach | -| 31 | EI | Party animal | Homebody | -| 32 | SN | Realistic | Imaginative | +| ID | Dim | Left Trait | Right Trait | +| --- | --- | ----------------------------- | -------------------------------- | +| 1 | JP | Makes lists | Relies on memory | +| 2 | TF | Skeptical | Wants to believe | +| 3 | EI | Bored by time alone | Needs time alone | +| 4 | SN | Accepts things as they are | Unsatisfied with the status quo | +| 5 | JP | Keeps a clean room | Tolerates a messy room | +| 6 | TF | Thinks "robotic" is an insult | Thinks "robotic" is a compliment | +| 7 | EI | Energized by interaction | Drained by interaction | +| 8 | SN | Prefers routine tasks | Prefers new challenges | +| 9 | JP | Plans social events ahead | Goes with the flow | +| 10 | TF | Sentimental | Unsentimental | +| 11 | EI | Comfortable in groups | Uncomfortable in groups | +| 12 | SN | Focuses on details | Focuses on big picture | +| 13 | JP | Prepares for meetings | Wings it | +| 14 | TF | Tenderhearted | Tough-minded | +| 15 | EI | Initiates conversations | Waits to be approached | +| 16 | SN | Works with facts | Works with ideas | +| 17 | JP | Finishes tasks early | Finishes at deadline | +| 18 | TF | Values harmony | Values truth | +| 19 | EI | Thinks out loud | Thinks inside head | +| 20 | SN | Trusts experience | Trusts intuition | +| 21 | JP | Follows schedules | Follows impulses | +| 22 | TF | Empathetic | Analytical | +| 23 | EI | Joins group activities | Prefers one-on-one | +| 24 | SN | Practical | Theoretical | +| 25 | JP | Organized workspace | Creative chaos | +| 26 | TF | Decides with heart | Decides with head | +| 27 | EI | Outgoing | Reserved | +| 28 | SN | Concrete | Abstract | +| 29 | JP | Punctual | Flexible with time | +| 30 | TF | Personal approach | Impersonal approach | +| 31 | EI | Party animal | Homebody | +| 32 | SN | Realistic | Imaginative | --- @@ -379,19 +395,19 @@ function PersonalityTest() { useEffect(() => { fetch('https://openjung.org/api/questions') - .then(r => r.json()) - .then(data => setQuestions(data.questions)); + .then((r) => r.json()) + .then((data) => setQuestions(data.questions)); }, []); const handleAnswer = (questionId, value) => { - setAnswers(prev => ({ ...prev, [questionId]: value })); + setAnswers((prev) => ({ ...prev, [questionId]: value })); }; const submit = async () => { const response = await fetch('https://openjung.org/api/calculate', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ answers }) + body: JSON.stringify({ answers }), }); const data = await response.json(); setResult(data.result); @@ -403,10 +419,12 @@ function PersonalityTest() { return (
- {questions.map(q => ( + {questions.map((q) => (
-

{q.leftTrait} ← → {q.rightTrait}

- {[1, 2, 3, 4, 5].map(v => ( +

+ {q.leftTrait} ← → {q.rightTrait} +

+ {[1, 2, 3, 4, 5].map((v) => (