diff --git a/.coderabbit.yaml b/.coderabbit.yaml index 344d2e6b..84493e93 100644 --- a/.coderabbit.yaml +++ b/.coderabbit.yaml @@ -1,8 +1,18 @@ # .coderabbit.yaml reviews: + auto_review: + enabled: true + auto_pause_after_reviewed_commits: 0 auto_title_instructions: "Generate a concise PR title that starts with a conventional commit type (feat, fix, chore, etc.) followed by a short imperative description of the change." path_filters: # Use "! " at the start of a pattern to EXCLUDE it from the review + - "!package-lock.json" # Ignore npm lockfile + - "!**/package-lock.json" # Ignore nested package lockfiles + - "!**/*.svg" # Ignore SVG graphics and logos + - "!**/public/*.html" # Ignore HTML bundles and demo scripts + - "!**/public/llms*.txt" # Ignore LLM context bundles + - "!**/llms*.txt" # Ignore root or nested LLM context files + - "!**/public/providers.v1.json" # Ignore static provider definitions - "!**/doc/api/**" # Ignore ONLY the generated TypeDoc API documentation - "!**/docs/**" # Ignore other generated documentation artifacts - "!**/.vitepress/cache/**" # Ignore VitePress cache diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml index 9a32cd7e..50c183c8 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -77,3 +77,11 @@ jobs: - name: Deploy to GitHub Pages id: deployment uses: actions/deploy-pages@v4 + + - name: Trigger tempo-workspace AI Context Deploy + uses: peter-evans/repository-dispatch@ff45666b9427631e3450c54a1bcbee4d9ff4d7c0 # v3.0.0 + with: + token: ${{ secrets.TEMPO_WORKSPACE_DISPATCH_TOKEN }} + repository: magmacomputing/tempo-workspace + event-type: sync-ai-context + diff --git a/.gitignore b/.gitignore index ffa3eee8..0c5c0cd4 100644 --- a/.gitignore +++ b/.gitignore @@ -51,6 +51,8 @@ Thumbs.db **/doc/api/ **/public/api/ **/doc/9-plugins/ +**/public/llms-full.txt +**/llms-full.txt # Secrets and credentials .env .env.* diff --git a/package-lock.json b/package-lock.json index 69b2f698..7e500b8f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,17 +1,18 @@ { "name": "tempo-monorepo", - "version": "3.11.0", + "version": "3.11.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "tempo-monorepo", - "version": "3.11.0", + "version": "3.11.1", "workspaces": [ "packages/*", "packages/plugins/*" ], "dependencies": { + "magic-string": "^1.1.1", "typescript-7": "npm:typescript@^7.0.2" }, "devDependencies": { @@ -35,24 +36,24 @@ "typescript": "^6.0.3", "typescript-7": "npm:typescript@^7.0.2", "unplugin-swc": "^1.5.9", - "vitest": "^4.1.7", - "webdriverio": "^9.29.0" + "vitest": "4.1.10", + "webdriverio": "^9.30.1" }, "engines": { "node": ">=20.0.0" } }, "node_modules/@algolia/abtesting": { - "version": "1.20.0", - "resolved": "https://registry.npmjs.org/@algolia/abtesting/-/abtesting-1.20.0.tgz", - "integrity": "sha512-5CqkS592H3+24b6H6CQ2RVpphdmAuIElZzv0Hngqo/ZEZpUZJ+KGLcueBhx33fv2wYBXyuuvskG5aQ7Ti+lR0g==", + "version": "1.22.0", + "resolved": "https://registry.npmjs.org/@algolia/abtesting/-/abtesting-1.22.0.tgz", + "integrity": "sha512-BFR6zNowNKcY7Ou7TaJc9QWexES4YKPbmf/OTFofpdsdhz4x6q0lbxp3duO0EHnyrN7rE4ba/TSXuY+BDGu4+g==", "dev": true, "license": "MIT", "dependencies": { - "@algolia/client-common": "5.54.0", - "@algolia/requester-browser-xhr": "5.54.0", - "@algolia/requester-fetch": "5.54.0", - "@algolia/requester-node-http": "5.54.0" + "@algolia/client-common": "5.56.0", + "@algolia/requester-browser-xhr": "5.56.0", + "@algolia/requester-fetch": "5.56.0", + "@algolia/requester-node-http": "5.56.0" }, "engines": { "node": ">= 14.0.0" @@ -108,41 +109,41 @@ } }, "node_modules/@algolia/client-abtesting": { - "version": "5.54.0", - "resolved": "https://registry.npmjs.org/@algolia/client-abtesting/-/client-abtesting-5.54.0.tgz", - "integrity": "sha512-IXnH1x3DBsPQA4/FRO0Pvkfy79tKy5Qr+ugAV9jdcGkpzHc476D0WV1MFJ+pZxtbts0xh2JqzUVmYEqA6LkOpA==", + "version": "5.56.0", + "resolved": "https://registry.npmjs.org/@algolia/client-abtesting/-/client-abtesting-5.56.0.tgz", + "integrity": "sha512-7r4Z3NC7yU1oAQVWJNA2HX7tX481F3pJvCGyLIXiTdBcthz4Q/o21jwcMYDFkuI92UWTNBQQmHYgwHo1zS5dzg==", "dev": true, "license": "MIT", "dependencies": { - "@algolia/client-common": "5.54.0", - "@algolia/requester-browser-xhr": "5.54.0", - "@algolia/requester-fetch": "5.54.0", - "@algolia/requester-node-http": "5.54.0" + "@algolia/client-common": "5.56.0", + "@algolia/requester-browser-xhr": "5.56.0", + "@algolia/requester-fetch": "5.56.0", + "@algolia/requester-node-http": "5.56.0" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/client-analytics": { - "version": "5.54.0", - "resolved": "https://registry.npmjs.org/@algolia/client-analytics/-/client-analytics-5.54.0.tgz", - "integrity": "sha512-pBpRqm1wpE0GnGy2rNLk9rjDn0Le4iywNRtnAWblLeqfjxpWKg8lWnk7nmSoTShFO31sz2jatXXzxK2lz8ipbA==", + "version": "5.56.0", + "resolved": "https://registry.npmjs.org/@algolia/client-analytics/-/client-analytics-5.56.0.tgz", + "integrity": "sha512-avmjXQSq+jadFO8Xl2em05/uQdQnEmHsJyOAdVbZkmVgpMfxL12aJwVVfGNwYr9nulcpuJN1X0lTaQ5wxuNGcA==", "dev": true, "license": "MIT", "dependencies": { - "@algolia/client-common": "5.54.0", - "@algolia/requester-browser-xhr": "5.54.0", - "@algolia/requester-fetch": "5.54.0", - "@algolia/requester-node-http": "5.54.0" + "@algolia/client-common": "5.56.0", + "@algolia/requester-browser-xhr": "5.56.0", + "@algolia/requester-fetch": "5.56.0", + "@algolia/requester-node-http": "5.56.0" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/client-common": { - "version": "5.54.0", - "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-5.54.0.tgz", - "integrity": "sha512-WbuwRUlFvSOsuxqTDjSSmgusuF5KFt+oFPzobvPDvodra6EWnVwUXjz0elkNSsnsIlZGtcXlX3LhxkO7rF90jw==", + "version": "5.56.0", + "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-5.56.0.tgz", + "integrity": "sha512-v2TPStUhY//ripPjIVclZ8AWc7DEGooXULZGFlFu37zNatgHjw34oZZ+OSbbc/YHO+xZwPl62I1k8xH1m4S2eg==", "dev": true, "license": "MIT", "engines": { @@ -150,156 +151,180 @@ } }, "node_modules/@algolia/client-insights": { - "version": "5.54.0", - "resolved": "https://registry.npmjs.org/@algolia/client-insights/-/client-insights-5.54.0.tgz", - "integrity": "sha512-/HNLVi3kPI+JhO59WbglLjPM2c4uECU+x4gk1iADseKtE5eYqJ/RJ+FIwM8xzPFKhFJaw+8hVq4lkd0nn8HDDg==", + "version": "5.56.0", + "resolved": "https://registry.npmjs.org/@algolia/client-insights/-/client-insights-5.56.0.tgz", + "integrity": "sha512-P0ehROpM4Sem3Sqo5x2cKPgj67D3G3jy0rh1Amwkcvsfr6tkvIcdCmerieanqTF7NxUMPNFLkpIFeMO8Rpa50w==", "dev": true, "license": "MIT", "dependencies": { - "@algolia/client-common": "5.54.0", - "@algolia/requester-browser-xhr": "5.54.0", - "@algolia/requester-fetch": "5.54.0", - "@algolia/requester-node-http": "5.54.0" + "@algolia/client-common": "5.56.0", + "@algolia/requester-browser-xhr": "5.56.0", + "@algolia/requester-fetch": "5.56.0", + "@algolia/requester-node-http": "5.56.0" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/client-personalization": { - "version": "5.54.0", - "resolved": "https://registry.npmjs.org/@algolia/client-personalization/-/client-personalization-5.54.0.tgz", - "integrity": "sha512-6TolyyDRumIKeLGBGSFAZsSIJ8hrm6NFCGR1jO7pQTiOtSWgAIxcFE5/JRpZ3g+unG4OkNOFy+I0dUEquIDbig==", + "version": "5.56.0", + "resolved": "https://registry.npmjs.org/@algolia/client-personalization/-/client-personalization-5.56.0.tgz", + "integrity": "sha512-SXK3Vn3WVxyzbm31oePZBJkp1wpOyuWdd4B/Pv7n0aXDxmeSWhC1R1FC1517mMrFAIaPH4Rt0x6RUe7ZNjz8FA==", "dev": true, "license": "MIT", "dependencies": { - "@algolia/client-common": "5.54.0", - "@algolia/requester-browser-xhr": "5.54.0", - "@algolia/requester-fetch": "5.54.0", - "@algolia/requester-node-http": "5.54.0" + "@algolia/client-common": "5.56.0", + "@algolia/requester-browser-xhr": "5.56.0", + "@algolia/requester-fetch": "5.56.0", + "@algolia/requester-node-http": "5.56.0" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/client-query-suggestions": { - "version": "5.54.0", - "resolved": "https://registry.npmjs.org/@algolia/client-query-suggestions/-/client-query-suggestions-5.54.0.tgz", - "integrity": "sha512-AxW2MLBhjBtGX5kIZrVLO2SP2vRkZxJw5qHGxnQJfLcYhA04mFNP0fWRtHshlcVnDk9M8L9lwfr2lGpf3Er+hw==", + "version": "5.56.0", + "resolved": "https://registry.npmjs.org/@algolia/client-query-suggestions/-/client-query-suggestions-5.56.0.tgz", + "integrity": "sha512-5+ZdX8garFnmycnZgKhtXHePEaLj5zqDxI/0lkhhluzCcvTn0/PvvTirTg8hHYetQHvn7GDyeAiqTAieMvMW4A==", "dev": true, "license": "MIT", "dependencies": { - "@algolia/client-common": "5.54.0", - "@algolia/requester-browser-xhr": "5.54.0", - "@algolia/requester-fetch": "5.54.0", - "@algolia/requester-node-http": "5.54.0" + "@algolia/client-common": "5.56.0", + "@algolia/requester-browser-xhr": "5.56.0", + "@algolia/requester-fetch": "5.56.0", + "@algolia/requester-node-http": "5.56.0" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/client-search": { - "version": "5.54.0", - "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-5.54.0.tgz", - "integrity": "sha512-ngdgVGp05lJzUyA+sUzr0MDZ7AMtANcJpwIzq4ZsfpZL5B3S7A4XYfMcU2sECZc3bx3ysOhYcdbbaTjc3ve0WQ==", + "version": "5.56.0", + "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-5.56.0.tgz", + "integrity": "sha512-+mKUdYvqOi0BcvpAEyCEw49vSBptufIcfibtHz2bdr1pI789M46Yt0uQEk/sxtK3teh71OQvVFHaTDzShUWewQ==", "dev": true, "license": "MIT", "dependencies": { - "@algolia/client-common": "5.54.0", - "@algolia/requester-browser-xhr": "5.54.0", - "@algolia/requester-fetch": "5.54.0", - "@algolia/requester-node-http": "5.54.0" + "@algolia/client-common": "5.56.0", + "@algolia/requester-browser-xhr": "5.56.0", + "@algolia/requester-fetch": "5.56.0", + "@algolia/requester-node-http": "5.56.0" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/ingestion": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/@algolia/ingestion/-/ingestion-1.54.0.tgz", - "integrity": "sha512-hee59Z7FgZ6/13FYL05ANPwRJY1pfvIlrwC8eBZYdiRFXTJVvf94IyTWOxLqRVpbseDP6eQQTW+PT7/DxTZIng==", + "version": "1.56.0", + "resolved": "https://registry.npmjs.org/@algolia/ingestion/-/ingestion-1.56.0.tgz", + "integrity": "sha512-9g/zj+AZx5moFcdFIrYQoVrueXivjUcc3MQHtCYT8WhIuk1lUh1AyEhvJCS0XBZld09cLvd1AZ3BvDBpVpX2UA==", "dev": true, "license": "MIT", "dependencies": { - "@algolia/client-common": "5.54.0", - "@algolia/requester-browser-xhr": "5.54.0", - "@algolia/requester-fetch": "5.54.0", - "@algolia/requester-node-http": "5.54.0" + "@algolia/client-common": "5.56.0", + "@algolia/requester-browser-xhr": "5.56.0", + "@algolia/requester-fetch": "5.56.0", + "@algolia/requester-node-http": "5.56.0" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/monitoring": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/@algolia/monitoring/-/monitoring-1.54.0.tgz", - "integrity": "sha512-diCjZVbIO7Pzw98tEKqLWIAgmQBI3Zt1sHsXyAPNGZgn32derpIXTnjjpJbZl+uAhSSznd6SfxFGdC9uYdN1TA==", + "version": "1.56.0", + "resolved": "https://registry.npmjs.org/@algolia/monitoring/-/monitoring-1.56.0.tgz", + "integrity": "sha512-Qf3Sr6f9A9uxCZUf3MXS0d2b877uYzEB5yxqpVGXAhcJnBCQjrRRon0KvefpGkxy+BshrIJs96OUoMtGqXTFDA==", "dev": true, "license": "MIT", "dependencies": { - "@algolia/client-common": "5.54.0", - "@algolia/requester-browser-xhr": "5.54.0", - "@algolia/requester-fetch": "5.54.0", - "@algolia/requester-node-http": "5.54.0" + "@algolia/client-common": "5.56.0", + "@algolia/requester-browser-xhr": "5.56.0", + "@algolia/requester-fetch": "5.56.0", + "@algolia/requester-node-http": "5.56.0" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/recommend": { - "version": "5.54.0", - "resolved": "https://registry.npmjs.org/@algolia/recommend/-/recommend-5.54.0.tgz", - "integrity": "sha512-6zeslypRAGWDgVJEJYAPuqmyquHvnw4MQwG+XXdrw5dTNDjXYIcCJdQQcCY06xPF9tUvmzy/1vHiH9QxhgwuOQ==", + "version": "5.56.0", + "resolved": "https://registry.npmjs.org/@algolia/recommend/-/recommend-5.56.0.tgz", + "integrity": "sha512-GXWG1rWc5wu8hY4N33Y3b6ernY6sAdAvmKWN/zHAiACOx40WnpG0TVX5YazCAr/9gOYGInSiM2A0y2jy2xbiDA==", "dev": true, "license": "MIT", "dependencies": { - "@algolia/client-common": "5.54.0", - "@algolia/requester-browser-xhr": "5.54.0", - "@algolia/requester-fetch": "5.54.0", - "@algolia/requester-node-http": "5.54.0" + "@algolia/client-common": "5.56.0", + "@algolia/requester-browser-xhr": "5.56.0", + "@algolia/requester-fetch": "5.56.0", + "@algolia/requester-node-http": "5.56.0" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/requester-browser-xhr": { - "version": "5.54.0", - "resolved": "https://registry.npmjs.org/@algolia/requester-browser-xhr/-/requester-browser-xhr-5.54.0.tgz", - "integrity": "sha512-iHZax214LPXd7XizQ4BNnTsegl8f3IeKm8JcrmSNZ/5x1rZ5xLkbG/anltAWtLpoRNnfpr4Z80YdYeVVPpx6wQ==", + "version": "5.56.0", + "resolved": "https://registry.npmjs.org/@algolia/requester-browser-xhr/-/requester-browser-xhr-5.56.0.tgz", + "integrity": "sha512-7t24cBxaInS3mZb7ddEaZT/tp6q+/aR4YttsQVyP1/i+LmwPR34atO35KjaLFCcRVrlP7sYOAqkCfg6lIRB+ew==", "dev": true, "license": "MIT", "dependencies": { - "@algolia/client-common": "5.54.0" + "@algolia/client-common": "5.56.0" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/requester-fetch": { - "version": "5.54.0", - "resolved": "https://registry.npmjs.org/@algolia/requester-fetch/-/requester-fetch-5.54.0.tgz", - "integrity": "sha512-YKtuG5YwPxZ+kkfd4HmUO7Z9aICPUSMlHslzKTmtMMhxGnetxEqGj9T/v2r/PdcuOUC5oW0CHe8akJk8cpS3gQ==", + "version": "5.56.0", + "resolved": "https://registry.npmjs.org/@algolia/requester-fetch/-/requester-fetch-5.56.0.tgz", + "integrity": "sha512-R7ePHgVYmDFjZpvrsVAfbDz/d4RxKAYZ5/vgLfIsCVRZRryjWl/3INOxpOICzitehQ5FjNtNjcLQTrmHPTcHBQ==", "dev": true, "license": "MIT", "dependencies": { - "@algolia/client-common": "5.54.0" + "@algolia/client-common": "5.56.0" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/requester-node-http": { - "version": "5.54.0", - "resolved": "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-5.54.0.tgz", - "integrity": "sha512-zyZDJ4WS5TnjZZ5pqywTBFO9olW7QMtY2kf2dbLnu+UTzfc9ri/HGf27jRN2NTbX9FcRPxSqPqzUhF/BZIx0VA==", + "version": "5.56.0", + "resolved": "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-5.56.0.tgz", + "integrity": "sha512-PIOUXlSnrqM0S+WOgDRb4RzotydJH7ZoT6tOyL7tAO7qJOfvX5wsEW8Pe+PMKMwvuI4/gIyK9cg2H7lJXqnc4Q==", "dev": true, "license": "MIT", "dependencies": { - "@algolia/client-common": "5.54.0" + "@algolia/client-common": "5.56.0" }, "engines": { "node": ">= 14.0.0" } }, + "node_modules/@antfu/install-pkg": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@antfu/install-pkg/-/install-pkg-1.1.0.tgz", + "integrity": "sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "package-manager-detector": "^1.3.0", + "tinyexec": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@antfu/install-pkg/node_modules/tinyexec": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.0.tgz", + "integrity": "sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/@babel/helper-string-parser": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", @@ -321,13 +346,13 @@ } }, "node_modules/@babel/parser": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", - "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz", + "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.29.7" + "@babel/types": "^7.29.8" }, "bin": { "parser": "bin/babel-parser.js" @@ -337,9 +362,9 @@ } }, "node_modules/@babel/types": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", - "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", "dev": true, "license": "MIT", "dependencies": { @@ -357,6 +382,20 @@ "dev": true, "license": "MIT" }, + "node_modules/@braintree/sanitize-url": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-7.1.2.tgz", + "integrity": "sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@chevrotain/types": { + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-11.1.2.tgz", + "integrity": "sha512-U+HFai5+zmJCkK86QsaJtoITlboZHBqrVketcO2ROv865xfCMSFpELQoz1GkX5GzME8pTa+3kbKrZHQtI0gdbw==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/@docsearch/css": { "version": "3.8.2", "resolved": "https://registry.npmjs.org/@docsearch/css/-/css-3.8.2.tgz", @@ -408,40 +447,6 @@ } } }, - "node_modules/@emnapi/core": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", - "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.2.1", - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/runtime": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", - "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/wasi-threads": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", - "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@epic-web/invariant": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@epic-web/invariant/-/invariant-1.0.0.tgz", @@ -450,9 +455,9 @@ "license": "MIT" }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", - "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", + "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==", "cpu": [ "ppc64" ], @@ -467,9 +472,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", - "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz", + "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==", "cpu": [ "arm" ], @@ -484,9 +489,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", - "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz", + "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==", "cpu": [ "arm64" ], @@ -501,9 +506,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", - "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz", + "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==", "cpu": [ "x64" ], @@ -518,9 +523,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", - "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz", + "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==", "cpu": [ "arm64" ], @@ -535,9 +540,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", - "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz", + "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==", "cpu": [ "x64" ], @@ -552,9 +557,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", - "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz", + "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==", "cpu": [ "arm64" ], @@ -569,9 +574,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", - "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz", + "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==", "cpu": [ "x64" ], @@ -586,9 +591,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", - "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz", + "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==", "cpu": [ "arm" ], @@ -603,9 +608,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", - "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", + "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==", "cpu": [ "arm64" ], @@ -620,9 +625,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", - "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz", + "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==", "cpu": [ "ia32" ], @@ -637,9 +642,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", - "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz", + "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==", "cpu": [ "loong64" ], @@ -654,9 +659,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", - "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz", + "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==", "cpu": [ "mips64el" ], @@ -671,9 +676,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", - "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz", + "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==", "cpu": [ "ppc64" ], @@ -688,9 +693,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", - "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz", + "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==", "cpu": [ "riscv64" ], @@ -705,9 +710,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", - "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", + "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==", "cpu": [ "s390x" ], @@ -722,9 +727,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", - "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz", + "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==", "cpu": [ "x64" ], @@ -739,9 +744,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", - "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz", + "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==", "cpu": [ "arm64" ], @@ -756,9 +761,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", - "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz", + "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==", "cpu": [ "x64" ], @@ -773,9 +778,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", - "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz", + "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==", "cpu": [ "arm64" ], @@ -790,9 +795,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", - "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", + "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==", "cpu": [ "x64" ], @@ -807,9 +812,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", - "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", + "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==", "cpu": [ "arm64" ], @@ -824,9 +829,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", - "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", + "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==", "cpu": [ "x64" ], @@ -841,9 +846,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", - "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz", + "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==", "cpu": [ "arm64" ], @@ -858,9 +863,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", - "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz", + "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==", "cpu": [ "ia32" ], @@ -875,9 +880,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", - "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz", + "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==", "cpu": [ "x64" ], @@ -906,9 +911,9 @@ } }, "node_modules/@iconify-json/simple-icons": { - "version": "1.2.86", - "resolved": "https://registry.npmjs.org/@iconify-json/simple-icons/-/simple-icons-1.2.86.tgz", - "integrity": "sha512-t3jck5qPQuK1qy+bRn9eCoDQhIB7XSazKz1Fjp8hcan3XOAsTI5Mq/s3F0ekOKSvMQqkVORYK6ns6o6T9f5EMA==", + "version": "1.2.93", + "resolved": "https://registry.npmjs.org/@iconify-json/simple-icons/-/simple-icons-1.2.93.tgz", + "integrity": "sha512-/XhANjfGYOuqvSR3TmUnkQkINvQ4GVjVuukvymRbxtVFBvIq/yiXJqCDycKcQPT401OYT9H2vIY6ihAlz1QIAw==", "dev": true, "license": "CC0-1.0", "dependencies": { @@ -922,6 +927,18 @@ "dev": true, "license": "MIT" }, + "node_modules/@iconify/utils": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@iconify/utils/-/utils-3.1.4.tgz", + "integrity": "sha512-b1S7B1k9ohZ+iNTi2ATxbRYG9fTrJmUT0rc46bvVnNxqNRGW7dyo/vRREwyniI5IRN2RSJHDcm+s3BjWrSAjHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@antfu/install-pkg": "^1.1.0", + "@iconify/types": "^2.0.0", + "import-meta-resolve": "^4.2.0" + } + }, "node_modules/@inversifyjs/common": { "version": "1.5.2", "resolved": "https://registry.npmjs.org/@inversifyjs/common/-/common-1.5.2.tgz", @@ -1003,9 +1020,9 @@ } }, "node_modules/@javascript-obfuscator/escodegen": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/@javascript-obfuscator/escodegen/-/escodegen-2.4.1.tgz", - "integrity": "sha512-YrEJJDr4cb+pIQKWzHFoDlDkQzatcrNB6OhAD6iTSwiKwzZUMVdobwbOuLpF4EiLxUj0qP28Xl1saTHYzIPCLg==", + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@javascript-obfuscator/escodegen/-/escodegen-2.4.2.tgz", + "integrity": "sha512-3N7EUdYxuldwvtOtZIlAxNwPW1qTUFwWt3PxNkik6hB1uSSXLXj9JMlSyUbTCXK3Osjn0Nu0s/aa4mbvkGouXQ==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -1021,6 +1038,17 @@ "source-map": "~0.6.1" } }, + "node_modules/@javascript-obfuscator/escodegen/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/@javascript-obfuscator/estraverse": { "version": "5.4.0", "resolved": "https://registry.npmjs.org/@javascript-obfuscator/estraverse/-/estraverse-5.4.0.tgz", @@ -1031,19 +1059,6 @@ "node": ">=4.0" } }, - "node_modules/@jest/schemas": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", - "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@sinclair/typebox": "^0.27.8" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", @@ -1091,7 +1106,6 @@ "version": "1.5.5", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true, "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { @@ -1168,29 +1182,65 @@ "@magmacomputing/tempo": "^3.6.1" } }, - "node_modules/@napi-rs/wasm-runtime": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.5.tgz", - "integrity": "sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q==", + "node_modules/@mermaid-js/mermaid-mindmap": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/@mermaid-js/mermaid-mindmap/-/mermaid-mindmap-9.3.0.tgz", + "integrity": "sha512-IhtYSVBBRYviH1Ehu8gk69pMDF8DSRqXBRDMWrEfHoaMruHeaP2DXA3PBnuwsMaCdPQhlUUcy/7DBLAEIXvCAw==", "dev": true, "license": "MIT", "optional": true, "dependencies": { - "@tybys/wasm-util": "^0.10.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" - }, - "peerDependencies": { - "@emnapi/core": "^1.7.1", - "@emnapi/runtime": "^1.7.1" + "@braintree/sanitize-url": "^6.0.0", + "cytoscape": "^3.23.0", + "cytoscape-cose-bilkent": "^4.1.0", + "cytoscape-fcose": "^2.1.0", + "d3": "^7.0.0", + "khroma": "^2.0.0", + "non-layered-tidy-tree-layout": "^2.0.2" + } + }, + "node_modules/@mermaid-js/mermaid-mindmap/node_modules/@braintree/sanitize-url": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-6.0.4.tgz", + "integrity": "sha512-s3jaWicZd0pkP0jf5ysyHUI/RE7MHos6qlToFcGWXVp+ykHOy77OUMrfbgJ9it2C5bow7OIQwYYaHjk9XlBQ2A==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/@mermaid-js/parser": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@mermaid-js/parser/-/parser-1.2.0.tgz", + "integrity": "sha512-oYPyv8A4As1yH5Bx+04iQEQxXuIQDe0GKCNSRgao6z8AM9jixXIfP0vsppRLvGf+nKIOb9/LdpWA4YuJiVvESA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@chevrotain/types": "~11.1.2" + } + }, + "node_modules/@napi-rs/lzma-linux-x64-gnu": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", + "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" } }, "node_modules/@nodable/entities": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-2.2.0.tgz", - "integrity": "sha512-9uGyhaQavEUMC8AIddIjau4NsnsXhou+j5sBAGojCM1oxmQpVKTWR/9JxABD6UAv12vpIms55fPZKFQEhG6uBg==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-3.0.0.tgz", + "integrity": "sha512-8L9xFeTYKhm49xfIypoe2W5wV1m/3Z58kT+7kR9A8OyFxcPduI4VmxaUMQyKYrRjUoLLSXv6EKKID5Tvj9cUVw==", "dev": true, "funding": [ { @@ -1200,16 +1250,6 @@ ], "license": "MIT" }, - "node_modules/@oxc-project/types": { - "version": "0.133.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.133.0.tgz", - "integrity": "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/Boshen" - } - }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -1248,49 +1288,140 @@ "spacetrim": "0.11.59" } }, - "node_modules/@puppeteer/browsers": { - "version": "2.13.2", - "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.13.2.tgz", - "integrity": "sha512-5EUZSUIc37H6aIXyWO0Z4y8NlF8NnjgmqeQgOGiswAU7pY0HOo16ho4+alIWmSfdZnjqBRawMsP3I5YqLSn6kw==", + "node_modules/@rollup/plugin-alias": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-alias/-/plugin-alias-6.0.0.tgz", + "integrity": "sha512-tPCzJOtS7uuVZd+xPhoy5W4vThe6KWXNmsFCNktaAh5RTqcLiSfT4huPQIXkgJ6YCOjJHvecOAzQxLFhPxKr+g==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "rollup": ">=4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-node-resolve": { + "version": "16.0.3", + "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-16.0.3.tgz", + "integrity": "sha512-lUYM3UBGuM93CnMPG1YocWu7X802BrNF3jW2zny5gQyLQgRFJhV1Sq0Zi74+dh/6NBx1DxFC4b4GXg9wUCG5Qg==", + "dev": true, + "license": "MIT", "dependencies": { - "debug": "^4.4.3", - "extract-zip": "^2.0.1", - "progress": "^2.0.3", - "proxy-agent": "^6.5.0", - "semver": "^7.7.4", - "tar-fs": "^3.1.1", - "yargs": "^17.7.2" + "@rollup/pluginutils": "^5.0.1", + "@types/resolve": "1.20.2", + "deepmerge": "^4.2.2", + "is-module": "^1.0.0", + "resolve": "^1.22.1" }, - "bin": { - "browsers": "lib/cjs/main-cli.js" + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.78.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-terser": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-terser/-/plugin-terser-1.0.0.tgz", + "integrity": "sha512-FnCxhTBx6bMOYQrar6C8h3scPt8/JwIzw3+AJ2K++6guogH5fYaIFia+zZuhqv0eo1RN7W1Pz630SyvLbDjhtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "serialize-javascript": "^7.0.3", + "smob": "^1.0.0", + "terser": "^5.17.4" }, "engines": { - "node": ">=18" + "node": ">=20.0.0" + }, + "peerDependencies": { + "rollup": "^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } } }, - "node_modules/@rolldown/binding-android-arm64": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.3.tgz", - "integrity": "sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==", + "node_modules/@rollup/plugin-typescript": { + "version": "12.3.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-typescript/-/plugin-typescript-12.3.0.tgz", + "integrity": "sha512-7DP0/p7y3t67+NabT9f8oTBFE6gGkto4SA6Np2oudYmZE/m1dt8RB0SjL1msMxFpLo631qjRCcBlAbq1ml/Big==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.1.0", + "resolve": "^1.22.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.14.0||^3.0.0||^4.0.0", + "tslib": "*", + "typescript": ">=3.7.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + }, + "tslib": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.4.0.tgz", + "integrity": "sha512-MfPp06CjRLfXQ3wY0R8vJDYBy/MvVcc9OulEfR0B8Iv9ko+GCNaRZ+EpJYFl27LhKsZK0o420sYCRHCjfCgeUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.4.tgz", + "integrity": "sha512-RrPokAb7dmbxFoeO3TloqHyOjgye8RkBhSqmp4aJMIex4c9r46ZstPnleDQOq1t46VOVjwIuwNogIqbodV1Vvg==", "cpu": [ - "arm64" + "arm" ], "dev": true, "license": "MIT", "optional": true, "os": [ "android" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } + ] }, - "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.3.tgz", - "integrity": "sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==", + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.4.tgz", + "integrity": "sha512-JKuJc+pnpks2pjy7L/N3v/cAkZxYlnmuZoD840ldbMI5KDbC4iO9NKwPKYdjYFCMAIIlBzYSFHxIJVYzRo2/8A==", "cpu": [ "arm64" ], @@ -1298,33 +1429,27 @@ "license": "MIT", "optional": true, "os": [ - "darwin" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } + "android" + ] }, - "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.3.tgz", - "integrity": "sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==", + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.4.tgz", + "integrity": "sha512-krw5uS2STmvJ02x0uTXHbqQNuz+9eZ1iw+qXk9dmW2gvV4jV7O2hEoOnuhFrpOPiel1mBFtqbxYZZtC46hXLOw==", "cpu": [ - "x64" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "darwin" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } + ] }, - "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.3.tgz", - "integrity": "sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==", + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.4.tgz", + "integrity": "sha512-wsTxtgApb4PrOsNJIm0FZ1h3WvCC+k9uxLJ4ad75hgoS4NiRes2SoJFlDAyMwiUY8IssDqGcHbXuN0sx1tfF1A==", "cpu": [ "x64" ], @@ -1332,33 +1457,75 @@ "license": "MIT", "optional": true, "os": [ - "freebsd" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } + "darwin" + ] }, - "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.3.tgz", - "integrity": "sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==", + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.4.tgz", + "integrity": "sha512-GUOnQlyZe3yAXhWOtOMsn5Qkrv5E5mZXa0thbARWi5Ei2szlVXJFQhddZ4HbAzh8q92w5twp+CQvs/eFanz9YQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.4.tgz", + "integrity": "sha512-/Y7f3QuxjzPKsjA/rfEDa3+0vXqyjmJ50Ln8dPpCmWkKTrUoWHG1cWhTqaAMLob2m2nESWuC7yGrREz019Ztqg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.4.tgz", + "integrity": "sha512-81wiiX3v7aqy+T+bT61TJ78yJjRquqFFTTbAPt08imfQQzkPIW8t6aJbkTagtCCrXMNc9D66+geqlK7ydLPNqA==", "cpu": [ "arm" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.4.tgz", + "integrity": "sha512-9kmDIvNZqdoHOBZgNtpTBeLWYO/LVipM3H/j62P8848/l/VPEQL6N3uxU9pvP1oZAsXyC2MEnFP3ovRjo7WYNQ==", + "cpu": [ + "arm" ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.3.tgz", - "integrity": "sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==", + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.4.tgz", + "integrity": "sha512-CcnXHWnXg69g+DX5VWL3FHts3qMRN2uVEHX+BZvGLdd07/gXkn3ePjYtO1LDJvxkGKVHMclKBRa1QUTH+6toYQ==", "cpu": [ "arm64" ], @@ -1370,15 +1537,12 @@ "optional": true, "os": [ "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } + ] }, - "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.3.tgz", - "integrity": "sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==", + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.4.tgz", + "integrity": "sha512-iFOibiHnTRuhrWLlRsOQFdZJJIa7S8OwkneJr4ocALP16u5yk6lWLINFwhHaEqBFMsKDUZofLkGos7+CPzGB3g==", "cpu": [ "arm64" ], @@ -1390,15 +1554,46 @@ "optional": true, "os": [ "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.4.tgz", + "integrity": "sha512-XnWYMI7euHlb5a871xPja+Gm7DRCFU+FGRrtS2sMq9N8FvqtpagUy6gD4YOemC5MRk9xbh8+jYMEJbigFQwsgA==", + "cpu": [ + "loong64" ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.3.tgz", - "integrity": "sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==", + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.4.tgz", + "integrity": "sha512-qGDAlO0U8xedCcsdRm9oaoQY8DAx/QT7uIxJWhCdx0ceIWX783UC9QSYkdpzAe29wNiVfp24+bZdQmn49o45SQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.4.tgz", + "integrity": "sha512-ru4H6ezD7ysA5EiEK6qkkaEb4modH8CTej6kUy/gQi20u3kB3G7Zn8snXXkeJSCOFKG/rbPPtM/+9Wgas1961w==", "cpu": [ "ppc64" ], @@ -1410,17 +1605,31 @@ "optional": true, "os": [ "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.4.tgz", + "integrity": "sha512-2W4MO5WQVJnbJaZdvDb9rhBDuFU1nKIepPFpJUBsTh2k1YY2g+ODViaWuyOAjQ5cOP7NvrvLzt3wvHOoiAvc7w==", + "cpu": [ + "ppc64" ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.3.tgz", - "integrity": "sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==", + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.4.tgz", + "integrity": "sha512-+fxjfuoAmVMCYV5QyjoIpu0cp5DOiOTeqYFk1AVaxGr+/ravWLX89XfQmptsoWcaVy/TGf2hexzbUOrCQIL1CQ==", "cpu": [ - "s390x" + "riscv64" ], "dev": true, "libc": [ @@ -1430,17 +1639,31 @@ "optional": true, "os": [ "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.4.tgz", + "integrity": "sha512-jTn8JfHGL4djjFxPuM06LmNUJDsst2jeVlsd9OmIH6zc5sC9K6rIuO4YajXatLUpBmBKl6b35ro1QZocLi+tcA==", + "cpu": [ + "riscv64" ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.3.tgz", - "integrity": "sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==", + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.4.tgz", + "integrity": "sha512-oCJCJL4pXsoDcP2QZ+JVlPTIRc6266zsIaeJJsWImmF7HO0W8nb6HuSgZlMWxJwaPf8ehbSw8yo0EUw925hKsA==", "cpu": [ - "x64" + "s390x" ], "dev": true, "libc": [ @@ -1450,15 +1673,29 @@ "optional": true, "os": [ "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.4.tgz", + "integrity": "sha512-W69hukhZ3KKNRCaMIEzKvcFye42hh0FE1+YoYaf5+Ikacuftoco6yO/xouz0hc5d5W/s3yBro5jRiuEE/Q5vUw==", + "cpu": [ + "x64" ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.3.tgz", - "integrity": "sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==", + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.4.tgz", + "integrity": "sha512-qiXbGG2jkjXhzXpsFZSR2Xpb8DN/UaxYsbb/STbuR/6fpaDgRmmaq1B/LmtF2wQFOFOSsK2jdE0RZ3a0zHn4QA==", "cpu": [ "x64" ], @@ -1470,15 +1707,26 @@ "optional": true, "os": [ "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.4.tgz", + "integrity": "sha512-nWeM//hxv8mIo6jD7Hu4o48DVmV9pbV6gsKaWU+4NFyqHoPKwrkRiZGLKUhOBk8qNmDmpwFtPKg80Bo/Tn4xiQ==", + "cpu": [ + "x64" ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] }, - "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.3.tgz", - "integrity": "sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==", + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.4.tgz", + "integrity": "sha512-s62SQ/vgsRSvMwDkOEfTqfgASF0f26ZNaQuTA6Aok5lrikf89yI2W0gFHvZb2Jpgc6N8JnOKZgCK2iciO3CsxQ==", "cpu": [ "arm64" ], @@ -1487,34 +1735,91 @@ "optional": true, "os": [ "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.4.tgz", + "integrity": "sha512-J6wGf8TVGbXJq+HH+ttTvrcfNKPbuZecV6KT1B8I18BC5IURUh5kl4Yl5OEP5eFIUoI5BWxCsyYMhFsDx8kekw==", + "cpu": [ + "arm64" ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] }, - "node_modules/@rolldown/binding-wasm32-wasi": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.3.tgz", - "integrity": "sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==", + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.4.tgz", + "integrity": "sha512-zmfrQd/0wu6oJs8Vq8KwY/YtsKSsLtKe/HwAP4Wqy8LhWjeT55fHRAkOhYQ12wI3ayS4Tt12d5CDRD7N96SAYQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.4.tgz", + "integrity": "sha512-qPzHqdj9rfUD+w79dtE07zi/kFwKyCJqplp5K5ygeLTp7jLpAoc16OAH39HSmRC9UpozaecsleI8uAdEj6v2yw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.4.tgz", + "integrity": "sha512-zD6NdeWEByGE9QF9vCrlJ5YQB4oq9q91kPZS37Jwj5hOkvR1lTBSpsKhKDw4IJtbQ35LsTS1HD9DZYGKIshU1Q==", "cpu": [ - "wasm32" + "x64" ], "dev": true, "license": "MIT", "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@se-oss/deasync": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@se-oss/deasync/-/deasync-1.0.1.tgz", + "integrity": "sha512-Ha7P/xCNxOuH72BNdLRWs4TT8rsMMrERnHtfKWBeTWu+UFW9OBTrRgfZJOlbAAQFR0l4Q30cpAn8CuR7PXWcPg==", + "dev": true, + "license": "MIT", "dependencies": { - "@emnapi/core": "1.10.0", - "@emnapi/runtime": "1.10.0", - "@napi-rs/wasm-runtime": "^1.1.4" + "type-fest": "^4.37.0" }, "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">= 10" + }, + "optionalDependencies": { + "@se-oss/deasync-darwin-arm64": "1.0.1", + "@se-oss/deasync-darwin-x64": "1.0.1", + "@se-oss/deasync-linux-arm64-gnu": "1.0.1", + "@se-oss/deasync-linux-arm64-musl": "1.0.1", + "@se-oss/deasync-linux-x64-gnu": "1.0.1", + "@se-oss/deasync-linux-x64-musl": "1.0.1", + "@se-oss/deasync-win32-arm64-msvc": "1.0.1", + "@se-oss/deasync-win32-x64-msvc": "1.0.1" } }, - "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.3.tgz", - "integrity": "sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==", + "node_modules/@se-oss/deasync-darwin-arm64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@se-oss/deasync-darwin-arm64/-/deasync-darwin-arm64-1.0.1.tgz", + "integrity": "sha512-0YWmIDEGQfW3GGopmZHhfA6mamsG0HFKZhmBzHVyFiMKkJts8kpQwGbGrWlK8eOAoPCihOsG6tCotYR3p7HZaQ==", "cpu": [ "arm64" ], @@ -1522,16 +1827,16 @@ "license": "MIT", "optional": true, "os": [ - "win32" + "darwin" ], "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">= 10" } }, - "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.3.tgz", - "integrity": "sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==", + "node_modules/@se-oss/deasync-darwin-x64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@se-oss/deasync-darwin-x64/-/deasync-darwin-x64-1.0.1.tgz", + "integrity": "sha512-r3FRTLIXqGqOb1DjTLW3YhO/Dd1vA2qRLP0Ym3Wmk3yMv6c/nm15zg6UVoXbgBu8cjbvcsI/OfbHPdErmjMWsw==", "cpu": [ "x64" ], @@ -1539,227 +1844,60 @@ "license": "MIT", "optional": true, "os": [ - "win32" + "darwin" ], "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">= 10" } }, - "node_modules/@rolldown/pluginutils": { + "node_modules/@se-oss/deasync-linux-arm64-gnu": { "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/@rollup/plugin-alias": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/@rollup/plugin-alias/-/plugin-alias-6.0.0.tgz", - "integrity": "sha512-tPCzJOtS7uuVZd+xPhoy5W4vThe6KWXNmsFCNktaAh5RTqcLiSfT4huPQIXkgJ6YCOjJHvecOAzQxLFhPxKr+g==", + "resolved": "https://registry.npmjs.org/@se-oss/deasync-linux-arm64-gnu/-/deasync-linux-arm64-gnu-1.0.1.tgz", + "integrity": "sha512-657uRew7fZAx663Li03ilLV2lN09Dqb/NxawlDu8kKmboK1BLitHJRS+taiT5oFZqyIDrU45tlQKfCrW0p0sYA==", + "cpu": [ + "arm64" + ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=20.19.0" - }, - "peerDependencies": { - "rollup": ">=4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } - } - }, - "node_modules/@rollup/plugin-node-resolve": { - "version": "16.0.3", - "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-16.0.3.tgz", - "integrity": "sha512-lUYM3UBGuM93CnMPG1YocWu7X802BrNF3jW2zny5gQyLQgRFJhV1Sq0Zi74+dh/6NBx1DxFC4b4GXg9wUCG5Qg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@rollup/pluginutils": "^5.0.1", - "@types/resolve": "1.20.2", - "deepmerge": "^4.2.2", - "is-module": "^1.0.0", - "resolve": "^1.22.1" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "rollup": "^2.78.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } - } - }, - "node_modules/@rollup/plugin-terser": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@rollup/plugin-terser/-/plugin-terser-1.0.0.tgz", - "integrity": "sha512-FnCxhTBx6bMOYQrar6C8h3scPt8/JwIzw3+AJ2K++6guogH5fYaIFia+zZuhqv0eo1RN7W1Pz630SyvLbDjhtQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "serialize-javascript": "^7.0.3", - "smob": "^1.0.0", - "terser": "^5.17.4" - }, - "engines": { - "node": ">=20.0.0" - }, - "peerDependencies": { - "rollup": "^2.0.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } - } - }, - "node_modules/@rollup/plugin-typescript": { - "version": "12.3.0", - "resolved": "https://registry.npmjs.org/@rollup/plugin-typescript/-/plugin-typescript-12.3.0.tgz", - "integrity": "sha512-7DP0/p7y3t67+NabT9f8oTBFE6gGkto4SA6Np2oudYmZE/m1dt8RB0SjL1msMxFpLo631qjRCcBlAbq1ml/Big==", - "dev": true, - "license": "MIT", - "dependencies": { - "@rollup/pluginutils": "^5.1.0", - "resolve": "^1.22.1" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "rollup": "^2.14.0||^3.0.0||^4.0.0", - "tslib": "*", - "typescript": ">=3.7.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - }, - "tslib": { - "optional": true - } - } - }, - "node_modules/@rollup/pluginutils": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.4.0.tgz", - "integrity": "sha512-MfPp06CjRLfXQ3wY0R8vJDYBy/MvVcc9OulEfR0B8Iv9ko+GCNaRZ+EpJYFl27LhKsZK0o420sYCRHCjfCgeUg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0", - "estree-walker": "^2.0.2", - "picomatch": "^4.0.2" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } + "node": ">= 10" } }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", - "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz", - "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz", - "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==", + "node_modules/@se-oss/deasync-linux-arm64-musl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@se-oss/deasync-linux-arm64-musl/-/deasync-linux-arm64-musl-1.0.1.tgz", + "integrity": "sha512-IE3fIQPIJtko4lx9sRam+Zz0P4xbpAPJgDCHaz6k9cP1yUvVI179B4IZRnFx0GyjyQpm0KhHoIGHJc4KUmA81Q==", "cpu": [ "arm64" ], "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz", - "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==", - "cpu": [ - "x64" + "libc": [ + "musl" ], - "dev": true, "license": "MIT", "optional": true, "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz", - "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==", - "cpu": [ - "arm64" + "linux" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] + "engines": { + "node": ">= 10" + } }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz", - "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==", + "node_modules/@se-oss/deasync-linux-x64-gnu": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@se-oss/deasync-linux-x64-gnu/-/deasync-linux-x64-gnu-1.0.1.tgz", + "integrity": "sha512-XQl7etZESGIjIraCyxfAey8ZTIJUB4dUFU3rPR/xLVn9bKpZGlJLIms0z3hoHX9mipO+Cqo53vK4IVm6A7U/ww==", "cpu": [ "x64" ], "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz", - "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==", - "cpu": [ - "arm" - ], - "dev": true, "libc": [ "glibc" ], @@ -1767,14 +1905,17 @@ "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": ">= 10" + } }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz", - "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==", + "node_modules/@se-oss/deasync-linux-x64-musl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@se-oss/deasync-linux-x64-musl/-/deasync-linux-x64-musl-1.0.1.tgz", + "integrity": "sha512-vWgFAZlqImqMV6jhCWV7C9wcCS1eb1ajhlKduBRPfyUxxkoObe+EqTG2BKJAuafxp3/KS1aUsIMJma9mhwFvow==", "cpu": [ - "arm" + "x64" ], "dev": true, "libc": [ @@ -1784,1130 +1925,1248 @@ "optional": true, "os": [ "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz", - "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "glibc" ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": ">= 10" + } }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz", - "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==", + "node_modules/@se-oss/deasync-win32-arm64-msvc": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@se-oss/deasync-win32-arm64-msvc/-/deasync-win32-arm64-msvc-1.0.1.tgz", + "integrity": "sha512-yk7lEE7Zd8GX7o6CuUbg3HnnmUhBx4tgfn5ff3eoq05CgBO6Z3ZtL4l+utAe1cxcFaXPhyvcgnHYyA4OF544tg==", "cpu": [ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ - "linux" - ] + "win32" + ], + "engines": { + "node": ">= 10" + } }, - "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz", - "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==", + "node_modules/@se-oss/deasync-win32-x64-msvc": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@se-oss/deasync-win32-x64-msvc/-/deasync-win32-x64-msvc-1.0.1.tgz", + "integrity": "sha512-ixizmuLGRPGyAesWUNWVzVOsvuunNb/qMqU8SmjfLR/vVgzdQEkSHFf+fkX9GXPN6FDv+DAz5uskTzhjUyCXFA==", "cpu": [ - "loong64" + "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz", - "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==", - "cpu": [ - "loong64" + "win32" ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@shikijs/core": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-2.5.0.tgz", + "integrity": "sha512-uu/8RExTKtavlpH7XqnVYBrfBkUc20ngXiX9NSrBhOVZYv/7XQRKUyhtkeflY5QsxC0GbJThCerruZfsUaSldg==", "dev": true, - "libc": [ - "musl" - ], "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz", - "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==", + "dependencies": { + "@shikijs/engine-javascript": "2.5.0", + "@shikijs/engine-oniguruma": "2.5.0", + "@shikijs/types": "2.5.0", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4", + "hast-util-to-html": "^9.0.4" + } + }, + "node_modules/@shikijs/core/node_modules/@shikijs/engine-oniguruma": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-2.5.0.tgz", + "integrity": "sha512-pGd1wRATzbo/uatrCIILlAdFVKdxImWJGQ5rFiB5VZi2ve5xj3Ax9jny8QvkaV93btQEwR/rSz5ERFpC5mKNIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/types": "2.5.0", + "@shikijs/vscode-textmate": "^10.0.2" + } + }, + "node_modules/@shikijs/core/node_modules/@shikijs/types": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-2.5.0.tgz", + "integrity": "sha512-ygl5yhxki9ZLNuNpPitBWvcy9fsSKKaRuO4BAlMyagszQidxcpLAr0qiW/q43DtSIDxO6hEbtYLiFZNXO/hdGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" + } + }, + "node_modules/@shikijs/engine-javascript": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-2.5.0.tgz", + "integrity": "sha512-VjnOpnQf8WuCEZtNUdjjwGUbtAVKuZkVQ/5cHy/tojVVRIRtlWMYVjyWhxOmIq05AlSOv72z7hRNRGVBgQOl0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/types": "2.5.0", + "@shikijs/vscode-textmate": "^10.0.2", + "oniguruma-to-es": "^3.1.0" + } + }, + "node_modules/@shikijs/engine-javascript/node_modules/@shikijs/types": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-2.5.0.tgz", + "integrity": "sha512-ygl5yhxki9ZLNuNpPitBWvcy9fsSKKaRuO4BAlMyagszQidxcpLAr0qiW/q43DtSIDxO6hEbtYLiFZNXO/hdGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" + } + }, + "node_modules/@shikijs/engine-oniguruma": { + "version": "3.23.0", + "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-3.23.0.tgz", + "integrity": "sha512-1nWINwKXxKKLqPibT5f4pAFLej9oZzQTsby8942OTlsJzOBZ0MWKiwzMsd+jhzu8YPCHAswGnnN1YtQfirL35g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.23.0", + "@shikijs/vscode-textmate": "^10.0.2" + } + }, + "node_modules/@shikijs/langs": { + "version": "3.23.0", + "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-3.23.0.tgz", + "integrity": "sha512-2Ep4W3Re5aB1/62RSYQInK9mM3HsLeB91cHqznAJMuylqjzNVAVCMnNWRHFtcNHXsoNRayP9z1qj4Sq3nMqYXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.23.0" + } + }, + "node_modules/@shikijs/themes": { + "version": "3.23.0", + "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-3.23.0.tgz", + "integrity": "sha512-5qySYa1ZgAT18HR/ypENL9cUSGOeI2x+4IvYJu4JgVJdizn6kG4ia5Q1jDEOi7gTbN4RbuYtmHh0W3eccOrjMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.23.0" + } + }, + "node_modules/@shikijs/transformers": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/transformers/-/transformers-2.5.0.tgz", + "integrity": "sha512-SI494W5X60CaUwgi8u4q4m4s3YAFSxln3tzNjOSYqq54wlVgz0/NbbXEb3mdLbqMBztcmS7bVTaEd2w0qMmfeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/core": "2.5.0", + "@shikijs/types": "2.5.0" + } + }, + "node_modules/@shikijs/transformers/node_modules/@shikijs/types": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-2.5.0.tgz", + "integrity": "sha512-ygl5yhxki9ZLNuNpPitBWvcy9fsSKKaRuO4BAlMyagszQidxcpLAr0qiW/q43DtSIDxO6hEbtYLiFZNXO/hdGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" + } + }, + "node_modules/@shikijs/types": { + "version": "3.23.0", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-3.23.0.tgz", + "integrity": "sha512-3JZ5HXOZfYjsYSk0yPwBrkupyYSLpAE26Qc0HLghhZNGTZg/SKxXIIgoxOpmmeQP0RRSDJTk1/vPfw9tbw+jSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" + } + }, + "node_modules/@shikijs/vscode-textmate": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz", + "integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==", + "dev": true, + "license": "MIT" + }, + "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/@swc/core": { + "version": "1.15.47", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.15.47.tgz", + "integrity": "sha512-FbsO5JcfOjfH38W/rohBRBweJeERsAuIP4f377lmkmxTcq9exjtx4SkRuZY5CdfhR2CBVwDIJegBpJDffwNsOg==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@swc/counter": "^0.1.3", + "@swc/types": "^0.1.27" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/swc" + }, + "optionalDependencies": { + "@swc/core-darwin-arm64": "1.15.47", + "@swc/core-darwin-x64": "1.15.47", + "@swc/core-linux-arm-gnueabihf": "1.15.47", + "@swc/core-linux-arm64-gnu": "1.15.47", + "@swc/core-linux-arm64-musl": "1.15.47", + "@swc/core-linux-ppc64-gnu": "1.15.47", + "@swc/core-linux-s390x-gnu": "1.15.47", + "@swc/core-linux-x64-gnu": "1.15.47", + "@swc/core-linux-x64-musl": "1.15.47", + "@swc/core-win32-arm64-msvc": "1.15.47", + "@swc/core-win32-ia32-msvc": "1.15.47", + "@swc/core-win32-x64-msvc": "1.15.47" + }, + "peerDependencies": { + "@swc/helpers": ">=0.5.17" + }, + "peerDependenciesMeta": { + "@swc/helpers": { + "optional": true + } + } + }, + "node_modules/@swc/core-darwin-arm64": { + "version": "1.15.47", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.15.47.tgz", + "integrity": "sha512-GsoMtan3ojGGMGFbl31mmRu5ctZ56re8grGE8mO/OHJ8O+JRkzod02fe7X6ZQ8JvamA3imkEkx/h3u+vsOgPgA==", "cpu": [ - "ppc64" + "arm64" ], "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", + "license": "Apache-2.0 AND MIT", "optional": true, "os": [ - "linux" - ] + "darwin" + ], + "peer": true, + "engines": { + "node": ">=10" + } }, - "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz", - "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==", + "node_modules/@swc/core-darwin-x64": { + "version": "1.15.47", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.15.47.tgz", + "integrity": "sha512-leTi7Rx3KF4zcC637iqWgk9SoV8VXAD8ppQYXsep63px5A/UftOcxLN1pmr8Z1si/YvX90ompP/rHgpYkgwXWg==", "cpu": [ - "ppc64" + "x64" ], "dev": true, - "libc": [ - "musl" + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" ], - "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm-gnueabihf": { + "version": "1.15.47", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.15.47.tgz", + "integrity": "sha512-hBqHuoWKKIsKmDBn9qVeWqj5GWZhtlcczVaqQmNRXsDfq+voR5CxKRfamA367QjJXtceYuliLFfEL8QsskRM2g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", "optional": true, "os": [ "linux" - ] + ], + "peer": true, + "engines": { + "node": ">=10" + } }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz", - "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==", + "node_modules/@swc/core-linux-arm64-gnu": { + "version": "1.15.47", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.15.47.tgz", + "integrity": "sha512-TBxvRz+B4K205TWHHZxWVxkC2RFNP/Mz3PNcECBos5PsKwxjg3QSJzdoebr0VCf0Bfh8HOPldKxAP/8XkFe9gA==", "cpu": [ - "riscv64" + "arm64" ], "dev": true, "libc": [ "glibc" ], - "license": "MIT", + "license": "Apache-2.0 AND MIT", "optional": true, "os": [ "linux" - ] + ], + "peer": true, + "engines": { + "node": ">=10" + } }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz", - "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==", + "node_modules/@swc/core-linux-arm64-musl": { + "version": "1.15.47", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.15.47.tgz", + "integrity": "sha512-3Yu3Uq/VgytqsPjTMbkPU1ExADytbdWbruJYhA584E9jrpE2Ki+R6VVPoZCeAVk1Cb7QxcRTgblw6bSa6a/R+w==", "cpu": [ - "riscv64" + "arm64" ], "dev": true, "libc": [ "musl" ], - "license": "MIT", + "license": "Apache-2.0 AND MIT", "optional": true, "os": [ "linux" - ] + ], + "peer": true, + "engines": { + "node": ">=10" + } }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz", - "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==", + "node_modules/@swc/core-linux-ppc64-gnu": { + "version": "1.15.47", + "resolved": "https://registry.npmjs.org/@swc/core-linux-ppc64-gnu/-/core-linux-ppc64-gnu-1.15.47.tgz", + "integrity": "sha512-wfdMi5IaOaNtmh2/6geRoxIdNfqylUZFdtzTKS655y1axWfIWyx7As74vv0wVdjeCIZ3WmCI9odDd4rUttXOSQ==", "cpu": [ - "s390x" + "ppc64" ], "dev": true, "libc": [ "glibc" ], - "license": "MIT", + "license": "Apache-2.0 AND MIT", "optional": true, "os": [ "linux" - ] + ], + "peer": true, + "engines": { + "node": ">=10" + } }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz", - "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==", + "node_modules/@swc/core-linux-s390x-gnu": { + "version": "1.15.47", + "resolved": "https://registry.npmjs.org/@swc/core-linux-s390x-gnu/-/core-linux-s390x-gnu-1.15.47.tgz", + "integrity": "sha512-3hHYBY0yx8Ez7GMRrkhXHQzMdR5IZA6Wq5Ee4svlgwvSECLpnAJ9+0AimEGUFDvuLwE7nV/2+PYe8+Nm4rvNcQ==", "cpu": [ - "x64" + "s390x" ], "dev": true, "libc": [ "glibc" ], - "license": "MIT", + "license": "Apache-2.0 AND MIT", "optional": true, "os": [ "linux" - ] + ], + "peer": true, + "engines": { + "node": ">=10" + } }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz", - "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==", + "node_modules/@swc/core-linux-x64-gnu": { + "version": "1.15.47", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.15.47.tgz", + "integrity": "sha512-TjfhjgP/jGCfFHYC3JQPhJA1HwErbIJ9JfREDc1KNkvY6P0LodCgKVIlQ5deeTbkG7ih3bF5PHJLuLpaZjdRyQ==", "cpu": [ "x64" ], "dev": true, "libc": [ - "musl" + "glibc" ], - "license": "MIT", + "license": "Apache-2.0 AND MIT", "optional": true, "os": [ "linux" - ] + ], + "peer": true, + "engines": { + "node": ">=10" + } }, - "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz", - "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==", + "node_modules/@swc/core-linux-x64-musl": { + "version": "1.15.47", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.15.47.tgz", + "integrity": "sha512-CQpS8Ge/avfjZd0UEwG/sds83Uu32deQXcV1Jo3jD0mmvQQqtYAjpsDZXugmheeAwmt+YIuoVtVHro8LMYHqsQ==", "cpu": [ "x64" ], "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ] - }, - "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz", - "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==", - "cpu": [ - "arm64" + "libc": [ + "musl" ], - "dev": true, - "license": "MIT", + "license": "Apache-2.0 AND MIT", "optional": true, "os": [ - "openharmony" - ] + "linux" + ], + "peer": true, + "engines": { + "node": ">=10" + } }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz", - "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==", + "node_modules/@swc/core-win32-arm64-msvc": { + "version": "1.15.47", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.15.47.tgz", + "integrity": "sha512-0W8IKHsUTYiT7G2RqtOoVWk+89yzZikIiDUb/sCK6BmQDBhN91hQSfyUtW12jhEWLzYgcfmisfsZrmZE+84U1A==", "cpu": [ "arm64" ], "dev": true, - "license": "MIT", + "license": "Apache-2.0 AND MIT", "optional": true, "os": [ "win32" - ] + ], + "peer": true, + "engines": { + "node": ">=10" + } }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz", - "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==", + "node_modules/@swc/core-win32-ia32-msvc": { + "version": "1.15.47", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.15.47.tgz", + "integrity": "sha512-ZIp49d2Z4/ka2jO9otOg4hDvTdPmp86kVOgS2M5FCPI7eKKZ1W0boxWn+8XeZrfERtFGW0AlMRm4JhlJa7l3NA==", "cpu": [ "ia32" ], "dev": true, - "license": "MIT", + "license": "Apache-2.0 AND MIT", "optional": true, "os": [ "win32" - ] + ], + "peer": true, + "engines": { + "node": ">=10" + } }, - "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz", - "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==", + "node_modules/@swc/core-win32-x64-msvc": { + "version": "1.15.47", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.15.47.tgz", + "integrity": "sha512-2h8Iek95vnixkBRCo+H8p09+Q5ll2NgSMFrWTy0iKt7+/t+8/T5mBpiT6c0ZxSS7wcWjwZ9sGZkK70tTSYHdDw==", "cpu": [ "x64" ], "dev": true, - "license": "MIT", + "license": "Apache-2.0 AND MIT", "optional": true, "os": [ "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz", - "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==", - "cpu": [ - "x64" ], + "peer": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/counter": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] + "license": "Apache-2.0", + "peer": true }, - "node_modules/@se-oss/deasync": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@se-oss/deasync/-/deasync-1.0.1.tgz", - "integrity": "sha512-Ha7P/xCNxOuH72BNdLRWs4TT8rsMMrERnHtfKWBeTWu+UFW9OBTrRgfZJOlbAAQFR0l4Q30cpAn8CuR7PXWcPg==", + "node_modules/@swc/types": { + "version": "0.1.28", + "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.28.tgz", + "integrity": "sha512-V6Mnml8v09QALx6K0elJ7o9K/MkVDtW3t6L+7Ou/JcWtb3xwId2AH4FeOceySd2JaO87IMw4+6vSZxLm34LPbw==", "dev": true, - "license": "MIT", + "license": "Apache-2.0", + "peer": true, "dependencies": { - "type-fest": "^4.37.0" - }, - "engines": { - "node": ">= 10" - }, - "optionalDependencies": { - "@se-oss/deasync-darwin-arm64": "1.0.1", - "@se-oss/deasync-darwin-x64": "1.0.1", - "@se-oss/deasync-linux-arm64-gnu": "1.0.1", - "@se-oss/deasync-linux-arm64-musl": "1.0.1", - "@se-oss/deasync-linux-x64-gnu": "1.0.1", - "@se-oss/deasync-linux-x64-musl": "1.0.1", - "@se-oss/deasync-win32-arm64-msvc": "1.0.1", - "@se-oss/deasync-win32-x64-msvc": "1.0.1" + "@swc/counter": "^0.1.3" } }, - "node_modules/@se-oss/deasync-darwin-arm64": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@se-oss/deasync-darwin-arm64/-/deasync-darwin-arm64-1.0.1.tgz", - "integrity": "sha512-0YWmIDEGQfW3GGopmZHhfA6mamsG0HFKZhmBzHVyFiMKkJts8kpQwGbGrWlK8eOAoPCihOsG6tCotYR3p7HZaQ==", - "cpu": [ - "arm64" - ], + "node_modules/@tootallnate/quickjs-emscripten": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", + "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } + "license": "MIT" }, - "node_modules/@se-oss/deasync-darwin-x64": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@se-oss/deasync-darwin-x64/-/deasync-darwin-x64-1.0.1.tgz", - "integrity": "sha512-r3FRTLIXqGqOb1DjTLW3YhO/Dd1vA2qRLP0Ym3Wmk3yMv6c/nm15zg6UVoXbgBu8cjbvcsI/OfbHPdErmjMWsw==", - "cpu": [ - "x64" - ], + "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": [ - "darwin" - ], - "engines": { - "node": ">= 10" + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" } }, - "node_modules/@se-oss/deasync-linux-arm64-gnu": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@se-oss/deasync-linux-arm64-gnu/-/deasync-linux-arm64-gnu-1.0.1.tgz", - "integrity": "sha512-657uRew7fZAx663Li03ilLV2lN09Dqb/NxawlDu8kKmboK1BLitHJRS+taiT5oFZqyIDrU45tlQKfCrW0p0sYA==", - "cpu": [ - "arm64" - ], + "node_modules/@types/d3": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz", + "integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-array": "*", + "@types/d3-axis": "*", + "@types/d3-brush": "*", + "@types/d3-chord": "*", + "@types/d3-color": "*", + "@types/d3-contour": "*", + "@types/d3-delaunay": "*", + "@types/d3-dispatch": "*", + "@types/d3-drag": "*", + "@types/d3-dsv": "*", + "@types/d3-ease": "*", + "@types/d3-fetch": "*", + "@types/d3-force": "*", + "@types/d3-format": "*", + "@types/d3-geo": "*", + "@types/d3-hierarchy": "*", + "@types/d3-interpolate": "*", + "@types/d3-path": "*", + "@types/d3-polygon": "*", + "@types/d3-quadtree": "*", + "@types/d3-random": "*", + "@types/d3-scale": "*", + "@types/d3-scale-chromatic": "*", + "@types/d3-selection": "*", + "@types/d3-shape": "*", + "@types/d3-time": "*", + "@types/d3-time-format": "*", + "@types/d3-timer": "*", + "@types/d3-transition": "*", + "@types/d3-zoom": "*" + } + }, + "node_modules/@types/d3-array": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } + "license": "MIT" }, - "node_modules/@se-oss/deasync-linux-arm64-musl": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@se-oss/deasync-linux-arm64-musl/-/deasync-linux-arm64-musl-1.0.1.tgz", - "integrity": "sha512-IE3fIQPIJtko4lx9sRam+Zz0P4xbpAPJgDCHaz6k9cP1yUvVI179B4IZRnFx0GyjyQpm0KhHoIGHJc4KUmA81Q==", - "cpu": [ - "arm64" - ], + "node_modules/@types/d3-axis": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.6.tgz", + "integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==", "dev": true, - "libc": [ - "musl" - ], "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" + "dependencies": { + "@types/d3-selection": "*" } }, - "node_modules/@se-oss/deasync-linux-x64-gnu": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@se-oss/deasync-linux-x64-gnu/-/deasync-linux-x64-gnu-1.0.1.tgz", - "integrity": "sha512-XQl7etZESGIjIraCyxfAey8ZTIJUB4dUFU3rPR/xLVn9bKpZGlJLIms0z3hoHX9mipO+Cqo53vK4IVm6A7U/ww==", - "cpu": [ - "x64" - ], + "node_modules/@types/d3-brush": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.6.tgz", + "integrity": "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==", "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" + "dependencies": { + "@types/d3-selection": "*" } }, - "node_modules/@se-oss/deasync-linux-x64-musl": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@se-oss/deasync-linux-x64-musl/-/deasync-linux-x64-musl-1.0.1.tgz", - "integrity": "sha512-vWgFAZlqImqMV6jhCWV7C9wcCS1eb1ajhlKduBRPfyUxxkoObe+EqTG2BKJAuafxp3/KS1aUsIMJma9mhwFvow==", - "cpu": [ - "x64" - ], + "node_modules/@types/d3-chord": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.6.tgz", + "integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==", "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } + "license": "MIT" }, - "node_modules/@se-oss/deasync-win32-arm64-msvc": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@se-oss/deasync-win32-arm64-msvc/-/deasync-win32-arm64-msvc-1.0.1.tgz", - "integrity": "sha512-yk7lEE7Zd8GX7o6CuUbg3HnnmUhBx4tgfn5ff3eoq05CgBO6Z3ZtL4l+utAe1cxcFaXPhyvcgnHYyA4OF544tg==", - "cpu": [ - "arm64" - ], + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-contour": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.6.tgz", + "integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" + "dependencies": { + "@types/d3-array": "*", + "@types/geojson": "*" } }, - "node_modules/@se-oss/deasync-win32-x64-msvc": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@se-oss/deasync-win32-x64-msvc/-/deasync-win32-x64-msvc-1.0.1.tgz", - "integrity": "sha512-ixizmuLGRPGyAesWUNWVzVOsvuunNb/qMqU8SmjfLR/vVgzdQEkSHFf+fkX9GXPN6FDv+DAz5uskTzhjUyCXFA==", - "cpu": [ - "x64" - ], + "node_modules/@types/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-dispatch": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.7.tgz", + "integrity": "sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-drag": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz", + "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" + "dependencies": { + "@types/d3-selection": "*" } }, - "node_modules/@se-oss/deasync/node_modules/type-fest": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", - "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "node_modules/@types/d3-dsv": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz", + "integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==", "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } + "license": "MIT" }, - "node_modules/@shikijs/core": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-2.5.0.tgz", - "integrity": "sha512-uu/8RExTKtavlpH7XqnVYBrfBkUc20ngXiX9NSrBhOVZYv/7XQRKUyhtkeflY5QsxC0GbJThCerruZfsUaSldg==", + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-fetch": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.7.tgz", + "integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==", "dev": true, "license": "MIT", "dependencies": { - "@shikijs/engine-javascript": "2.5.0", - "@shikijs/engine-oniguruma": "2.5.0", - "@shikijs/types": "2.5.0", - "@shikijs/vscode-textmate": "^10.0.2", - "@types/hast": "^3.0.4", - "hast-util-to-html": "^9.0.4" + "@types/d3-dsv": "*" } }, - "node_modules/@shikijs/core/node_modules/@shikijs/engine-oniguruma": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-2.5.0.tgz", - "integrity": "sha512-pGd1wRATzbo/uatrCIILlAdFVKdxImWJGQ5rFiB5VZi2ve5xj3Ax9jny8QvkaV93btQEwR/rSz5ERFpC5mKNIw==", + "node_modules/@types/d3-force": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz", + "integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-format": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz", + "integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-geo": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.1.tgz", + "integrity": "sha512-65Emv9fQiQQqphLlRkuQ5ypPsOmWPhtBGCMv61JDPEPMvsx+gzhGf74yw1a78xFKPj6zw4AgQICJoQv0vK9M2w==", "dev": true, "license": "MIT", "dependencies": { - "@shikijs/types": "2.5.0", - "@shikijs/vscode-textmate": "^10.0.2" + "@types/geojson": "*" } }, - "node_modules/@shikijs/core/node_modules/@shikijs/types": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-2.5.0.tgz", - "integrity": "sha512-ygl5yhxki9ZLNuNpPitBWvcy9fsSKKaRuO4BAlMyagszQidxcpLAr0qiW/q43DtSIDxO6hEbtYLiFZNXO/hdGw==", + "node_modules/@types/d3-hierarchy": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz", + "integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", "dev": true, "license": "MIT", "dependencies": { - "@shikijs/vscode-textmate": "^10.0.2", - "@types/hast": "^3.0.4" + "@types/d3-color": "*" } }, - "node_modules/@shikijs/engine-javascript": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-2.5.0.tgz", - "integrity": "sha512-VjnOpnQf8WuCEZtNUdjjwGUbtAVKuZkVQ/5cHy/tojVVRIRtlWMYVjyWhxOmIq05AlSOv72z7hRNRGVBgQOl0w==", + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-polygon": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.2.tgz", + "integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-quadtree": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz", + "integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-random": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.4.tgz", + "integrity": "sha512-UHYId5WTCx4L4YNel7NU00XUXXgvgpgZOvp10PuvsQENjMDXhh2RyFc0KBjO7B45ne4Ha1yVH7ii0vnzKkuzWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", "dev": true, "license": "MIT", "dependencies": { - "@shikijs/types": "2.5.0", - "@shikijs/vscode-textmate": "^10.0.2", - "oniguruma-to-es": "^3.1.0" + "@types/d3-time": "*" } }, - "node_modules/@shikijs/engine-javascript/node_modules/@shikijs/types": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-2.5.0.tgz", - "integrity": "sha512-ygl5yhxki9ZLNuNpPitBWvcy9fsSKKaRuO4BAlMyagszQidxcpLAr0qiW/q43DtSIDxO6hEbtYLiFZNXO/hdGw==", + "node_modules/@types/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-selection": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz", + "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-shape": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz", + "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==", "dev": true, "license": "MIT", "dependencies": { - "@shikijs/vscode-textmate": "^10.0.2", - "@types/hast": "^3.0.4" + "@types/d3-path": "*" } }, - "node_modules/@shikijs/engine-oniguruma": { - "version": "3.23.0", - "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-3.23.0.tgz", - "integrity": "sha512-1nWINwKXxKKLqPibT5f4pAFLej9oZzQTsby8942OTlsJzOBZ0MWKiwzMsd+jhzu8YPCHAswGnnN1YtQfirL35g==", + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-time-format": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.3.tgz", + "integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-transition": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz", + "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==", "dev": true, "license": "MIT", "dependencies": { - "@shikijs/types": "3.23.0", - "@shikijs/vscode-textmate": "^10.0.2" + "@types/d3-selection": "*" } }, - "node_modules/@shikijs/langs": { - "version": "3.23.0", - "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-3.23.0.tgz", - "integrity": "sha512-2Ep4W3Re5aB1/62RSYQInK9mM3HsLeB91cHqznAJMuylqjzNVAVCMnNWRHFtcNHXsoNRayP9z1qj4Sq3nMqYXg==", + "node_modules/@types/d3-zoom": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz", + "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==", "dev": true, "license": "MIT", "dependencies": { - "@shikijs/types": "3.23.0" + "@types/d3-interpolate": "*", + "@types/d3-selection": "*" } }, - "node_modules/@shikijs/themes": { - "version": "3.23.0", - "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-3.23.0.tgz", - "integrity": "sha512-5qySYa1ZgAT18HR/ypENL9cUSGOeI2x+4IvYJu4JgVJdizn6kG4ia5Q1jDEOi7gTbN4RbuYtmHh0W3eccOrjMA==", + "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" + }, + "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" + }, + "node_modules/@types/geojson": { + "version": "7946.0.16", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", + "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/google.maps": { + "version": "3.65.5", + "resolved": "https://registry.npmjs.org/@types/google.maps/-/google.maps-3.65.5.tgz", + "integrity": "sha512-oR9Hq/WaKGzIguZEwoCuyQ0ofDhCbefmS9RrjLUZ5scMziwTE6xA90drJYt2ZsyUCYmCqnxQcDpfGqeRE/Zgog==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/hammerjs": { + "version": "2.0.46", + "resolved": "https://registry.npmjs.org/@types/hammerjs/-/hammerjs-2.0.46.tgz", + "integrity": "sha512-ynRvcq6wvqexJ9brDMS4BnBLzmr0e14d6ZJTEShTBWKymQiHwlAyGu0ZPEFI2Fh1U53F7tN9ufClWM5KvqkKOw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/hast": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.5.tgz", + "integrity": "sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g==", "dev": true, "license": "MIT", "dependencies": { - "@shikijs/types": "3.23.0" + "@types/unist": "*" } }, - "node_modules/@shikijs/transformers": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@shikijs/transformers/-/transformers-2.5.0.tgz", - "integrity": "sha512-SI494W5X60CaUwgi8u4q4m4s3YAFSxln3tzNjOSYqq54wlVgz0/NbbXEb3mdLbqMBztcmS7bVTaEd2w0qMmfeg==", + "node_modules/@types/jquery": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-4.0.1.tgz", + "integrity": "sha512-9a59A/tycXgYuPABcp6/3spSShn0NT2UOM4EfHvMumjYi4lJWTsK5SZWjhx3yRm9IHGCeWXdV2YfNsrWrft/CA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/markdown-it": { + "version": "14.1.2", + "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz", + "integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==", "dev": true, "license": "MIT", "dependencies": { - "@shikijs/core": "2.5.0", - "@shikijs/types": "2.5.0" + "@types/linkify-it": "^5", + "@types/mdurl": "^2" } }, - "node_modules/@shikijs/transformers/node_modules/@shikijs/types": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-2.5.0.tgz", - "integrity": "sha512-ygl5yhxki9ZLNuNpPitBWvcy9fsSKKaRuO4BAlMyagszQidxcpLAr0qiW/q43DtSIDxO6hEbtYLiFZNXO/hdGw==", + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", "dev": true, "license": "MIT", "dependencies": { - "@shikijs/vscode-textmate": "^10.0.2", - "@types/hast": "^3.0.4" + "@types/unist": "*" } }, - "node_modules/@shikijs/types": { - "version": "3.23.0", - "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-3.23.0.tgz", - "integrity": "sha512-3JZ5HXOZfYjsYSk0yPwBrkupyYSLpAE26Qc0HLghhZNGTZg/SKxXIIgoxOpmmeQP0RRSDJTk1/vPfw9tbw+jSQ==", + "node_modules/@types/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/minimatch": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz", + "integrity": "sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "26.2.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.2.0.tgz", + "integrity": "sha512-5IviulTZeRNp2vAJ514cc/HUlY5nZ9fCbq9DMyC52BrhFZACo3nI0R7qBxhQmo/d27NFe96ur/b7Wwxklda+kg==", "dev": true, "license": "MIT", "dependencies": { - "@shikijs/vscode-textmate": "^10.0.2", - "@types/hast": "^3.0.4" + "undici-types": "~8.3.0" } }, - "node_modules/@shikijs/vscode-textmate": { - "version": "10.0.2", - "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz", - "integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==", + "node_modules/@types/resolve": { + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", + "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==", "dev": true, "license": "MIT" }, - "node_modules/@sinclair/typebox": { - "version": "0.27.10", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.10.tgz", - "integrity": "sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==", + "node_modules/@types/sinonjs__fake-timers": { + "version": "8.1.5", + "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.5.tgz", + "integrity": "sha512-mQkU2jY8jJEF7YHjHvsQO8+3ughTL1mcnn96igfhONmR+fUPSKIkefQYpSe8bsly2Ep7oQbn/6VG5/9/0qcArQ==", "dev": true, "license": "MIT" }, - "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/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", "dev": true, "license": "MIT" }, - "node_modules/@swc/core": { - "version": "1.15.43", - "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.15.43.tgz", - "integrity": "sha512-1CuKjFkPxIgGdeHVuNbkxmBxkcbdc08u0aiI43pFq6yY1tTVKmXT9hFEooyyKs/sJ3xf1GPHyEwTtk9Xl8dvQw==", + "node_modules/@types/validator": { + "version": "13.15.10", + "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.10.tgz", + "integrity": "sha512-T8L6i7wCuyoK8A/ZeLYt1+q0ty3Zb9+qbSSvrIVitzT3YjZqkTZ40IbRsPanlB4h1QB3JVL1SYCdR6ngtFYcuA==", "dev": true, - "hasInstallScript": true, - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "@swc/counter": "^0.1.3", - "@swc/types": "^0.1.27" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/swc" - }, - "optionalDependencies": { - "@swc/core-darwin-arm64": "1.15.43", - "@swc/core-darwin-x64": "1.15.43", - "@swc/core-linux-arm-gnueabihf": "1.15.43", - "@swc/core-linux-arm64-gnu": "1.15.43", - "@swc/core-linux-arm64-musl": "1.15.43", - "@swc/core-linux-ppc64-gnu": "1.15.43", - "@swc/core-linux-s390x-gnu": "1.15.43", - "@swc/core-linux-x64-gnu": "1.15.43", - "@swc/core-linux-x64-musl": "1.15.43", - "@swc/core-win32-arm64-msvc": "1.15.43", - "@swc/core-win32-ia32-msvc": "1.15.43", - "@swc/core-win32-x64-msvc": "1.15.43" - }, - "peerDependencies": { - "@swc/helpers": ">=0.5.17" - }, - "peerDependenciesMeta": { - "@swc/helpers": { - "optional": true - } - } + "license": "MIT" }, - "node_modules/@swc/core-darwin-arm64": { - "version": "1.15.43", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.15.43.tgz", - "integrity": "sha512-v1aVuvXdo/BHxJzco9V2xpHrvwWmhfS8t6gziY5wJxd+Z2h8AeJRnAwPD8itCDaGXVBwJ/CaKfxEzTkG0Va0OA==", + "node_modules/@types/web-bluetooth": { + "version": "0.0.21", + "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz", + "integrity": "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@types/which/-/which-2.0.2.tgz", + "integrity": "sha512-113D3mDkZDjo+EeUEHCFy0qniNc1ZpecGiAU7WSo7YDoSzolZIQKpYFHrPpjkB2nuyahcKfrmLXeQlh7gqJYdw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@typescript/typescript-aix-ppc64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-aix-ppc64/-/typescript-aix-ppc64-7.0.2.tgz", + "integrity": "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-darwin-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-arm64/-/typescript-darwin-arm64-7.0.2.tgz", + "integrity": "sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA==", "cpu": [ "arm64" ], "dev": true, - "license": "Apache-2.0 AND MIT", + "license": "Apache-2.0", "optional": true, "os": [ "darwin" ], - "peer": true, "engines": { - "node": ">=10" + "node": ">=16.20.0" } }, - "node_modules/@swc/core-darwin-x64": { - "version": "1.15.43", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.15.43.tgz", - "integrity": "sha512-lp3d4Lamc8dt5huYdGLSR+9hLxmfr1jb0l+4XXG2zPqZwYWRN9R0U2qYoTrggiU2RWW0oV9VbWM3kBnqIc2kdQ==", + "node_modules/@typescript/typescript-darwin-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-x64/-/typescript-darwin-x64-7.0.2.tgz", + "integrity": "sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA==", "cpu": [ "x64" ], "dev": true, - "license": "Apache-2.0 AND MIT", + "license": "Apache-2.0", "optional": true, "os": [ "darwin" ], - "peer": true, "engines": { - "node": ">=10" + "node": ">=16.20.0" } }, - "node_modules/@swc/core-linux-arm-gnueabihf": { - "version": "1.15.43", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.15.43.tgz", - "integrity": "sha512-JWTQQELtsG5GgphDrr/XqqmM2pDN3cZqbMS0Mrg+iTiXL3F74sn/S2IyYE/5u4h2KLkTf9qQ7dXyxsbx7YzkeA==", + "node_modules/@typescript/typescript-freebsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-arm64/-/typescript-freebsd-arm64-7.0.2.tgz", + "integrity": "sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ==", "cpu": [ - "arm" + "arm64" ], "dev": true, "license": "Apache-2.0", "optional": true, "os": [ - "linux" + "freebsd" ], - "peer": true, "engines": { - "node": ">=10" + "node": ">=16.20.0" } }, - "node_modules/@swc/core-linux-arm64-gnu": { - "version": "1.15.43", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.15.43.tgz", - "integrity": "sha512-B4otJRdPWIsmiSBf0uG7Z/+vMWmkufjz5MmYxubwKuZazDW14Zd3symga1N62QR4RT+kEFeHEgsXfZGyn/w0hw==", + "node_modules/@typescript/typescript-freebsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-x64/-/typescript-freebsd-x64-7.0.2.tgz", + "integrity": "sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw==", "cpu": [ - "arm64" + "x64" ], "dev": true, - "libc": [ - "glibc" - ], - "license": "Apache-2.0 AND MIT", + "license": "Apache-2.0", "optional": true, "os": [ - "linux" + "freebsd" ], - "peer": true, "engines": { - "node": ">=10" + "node": ">=16.20.0" } }, - "node_modules/@swc/core-linux-arm64-musl": { - "version": "1.15.43", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.15.43.tgz", - "integrity": "sha512-6zB6OnpViBxYy4tgY3v2i6AZY9fwkcHZ032UOwtwUuW1d19sdT07qF0kZe6/3UR1tUaK6jjg2rmVcUIBCEYVjQ==", + "node_modules/@typescript/typescript-linux-arm": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm/-/typescript-linux-arm-7.0.2.tgz", + "integrity": "sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ==", "cpu": [ - "arm64" + "arm" ], "dev": true, - "libc": [ - "musl" - ], - "license": "Apache-2.0 AND MIT", + "license": "Apache-2.0", "optional": true, "os": [ "linux" ], - "peer": true, "engines": { - "node": ">=10" + "node": ">=16.20.0" } }, - "node_modules/@swc/core-linux-ppc64-gnu": { - "version": "1.15.43", - "resolved": "https://registry.npmjs.org/@swc/core-linux-ppc64-gnu/-/core-linux-ppc64-gnu-1.15.43.tgz", - "integrity": "sha512-coxE1ZWdB3uSDVNoEtYNrRi/1epvckZx9cTJ8ICUxTMTxGk+yvQ/Twacp3ruZSaMPGCriUjP86C37VhaT6nyRg==", + "node_modules/@typescript/typescript-linux-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm64/-/typescript-linux-arm64-7.0.2.tgz", + "integrity": "sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ==", "cpu": [ - "ppc64" + "arm64" ], "dev": true, - "libc": [ - "glibc" - ], - "license": "Apache-2.0 AND MIT", + "license": "Apache-2.0", "optional": true, "os": [ "linux" ], - "peer": true, "engines": { - "node": ">=10" + "node": ">=16.20.0" } }, - "node_modules/@swc/core-linux-s390x-gnu": { - "version": "1.15.43", - "resolved": "https://registry.npmjs.org/@swc/core-linux-s390x-gnu/-/core-linux-s390x-gnu-1.15.43.tgz", - "integrity": "sha512-lXfLhs+LpBsD5inuYx+YDH5WsPPBQ95KPUiy8P5wq9ob9xKDZFqwNfU2QW6bGO8NqRO/H9JQomTSt5Yyh+FGfA==", + "node_modules/@typescript/typescript-linux-loong64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-loong64/-/typescript-linux-loong64-7.0.2.tgz", + "integrity": "sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ==", "cpu": [ - "s390x" + "loong64" ], "dev": true, - "libc": [ - "glibc" - ], - "license": "Apache-2.0 AND MIT", + "license": "Apache-2.0", "optional": true, "os": [ "linux" ], - "peer": true, "engines": { - "node": ">=10" + "node": ">=16.20.0" } }, - "node_modules/@swc/core-linux-x64-gnu": { - "version": "1.15.43", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.15.43.tgz", - "integrity": "sha512-07XnKwTmKy8TGOZG3D9fRnLWGynxPjwQnZLVmBFbo6F+7vHYzBIOuwXEhemrChBWb6yDNZsVCcMWCPX6FDD2xg==", + "node_modules/@typescript/typescript-linux-mips64el": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-mips64el/-/typescript-linux-mips64el-7.0.2.tgz", + "integrity": "sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA==", "cpu": [ - "x64" + "mips64el" ], "dev": true, - "libc": [ - "glibc" - ], - "license": "Apache-2.0 AND MIT", + "license": "Apache-2.0", "optional": true, "os": [ "linux" ], - "peer": true, "engines": { - "node": ">=10" + "node": ">=16.20.0" } }, - "node_modules/@swc/core-linux-x64-musl": { - "version": "1.15.43", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.15.43.tgz", - "integrity": "sha512-TJc+bsSIaBh+hZvZ5GRtW/K1bw66TJ9vsUwvVIsZdiWxU5ObLwZvfcnZ3UpgVfMnFibRes9uriJrQNBHEEogRQ==", + "node_modules/@typescript/typescript-linux-ppc64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-ppc64/-/typescript-linux-ppc64-7.0.2.tgz", + "integrity": "sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA==", "cpu": [ - "x64" + "ppc64" ], "dev": true, - "libc": [ - "musl" - ], - "license": "Apache-2.0 AND MIT", + "license": "Apache-2.0", "optional": true, "os": [ "linux" ], - "peer": true, "engines": { - "node": ">=10" + "node": ">=16.20.0" } }, - "node_modules/@swc/core-win32-arm64-msvc": { - "version": "1.15.43", - "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.15.43.tgz", - "integrity": "sha512-jfd7s2/bUQYkOHLs+LWQNKZdmDa8+sufKLllhpWAhVQ2GDCwsHe3vR/j+OSiItZNtkzFuaawa3+SAKz9y5gYfw==", + "node_modules/@typescript/typescript-linux-riscv64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-riscv64/-/typescript-linux-riscv64-7.0.2.tgz", + "integrity": "sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ==", "cpu": [ - "arm64" + "riscv64" ], "dev": true, - "license": "Apache-2.0 AND MIT", + "license": "Apache-2.0", "optional": true, "os": [ - "win32" + "linux" ], - "peer": true, "engines": { - "node": ">=10" + "node": ">=16.20.0" } }, - "node_modules/@swc/core-win32-ia32-msvc": { - "version": "1.15.43", - "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.15.43.tgz", - "integrity": "sha512-rLAE8JvucqEW1ZGohxPQrQWPBQeJG4+ypKbWfdlU/qmKScvCkxf9/Jxnzki1dkUQCQ7P5Enp13RlvqOlvx/32g==", + "node_modules/@typescript/typescript-linux-s390x": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-s390x/-/typescript-linux-s390x-7.0.2.tgz", + "integrity": "sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw==", "cpu": [ - "ia32" + "s390x" ], "dev": true, - "license": "Apache-2.0 AND MIT", + "license": "Apache-2.0", "optional": true, "os": [ - "win32" + "linux" ], - "peer": true, "engines": { - "node": ">=10" + "node": ">=16.20.0" } }, - "node_modules/@swc/core-win32-x64-msvc": { - "version": "1.15.43", - "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.15.43.tgz", - "integrity": "sha512-h8MLDHZcfIukwQWj03rIJZx1I0E81AYj2X7J/nGErG4nz+QAv6G1Z+peotvinL3lqpbo32tLYSMFo32/ySzxKg==", + "node_modules/@typescript/typescript-linux-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-x64/-/typescript-linux-x64-7.0.2.tgz", + "integrity": "sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A==", "cpu": [ "x64" ], "dev": true, - "license": "Apache-2.0 AND MIT", + "license": "Apache-2.0", "optional": true, "os": [ - "win32" + "linux" ], - "peer": true, "engines": { - "node": ">=10" + "node": ">=16.20.0" } }, - "node_modules/@swc/counter": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", - "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", + "node_modules/@typescript/typescript-netbsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-arm64/-/typescript-netbsd-arm64-7.0.2.tgz", + "integrity": "sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA==", + "cpu": [ + "arm64" + ], "dev": true, "license": "Apache-2.0", - "peer": true + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=16.20.0" + } }, - "node_modules/@swc/types": { - "version": "0.1.27", - "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.27.tgz", - "integrity": "sha512-K6h3iUlqeM946U4sXFYeahefR1YBbXJvko+hv8WS8/0BNJ4OHiHRywMnQUJCqkR7Y9+hqQ1TvEpiKqUhz7NEFg==", + "node_modules/@typescript/typescript-netbsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-x64/-/typescript-netbsd-x64-7.0.2.tgz", + "integrity": "sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA==", + "cpu": [ + "x64" + ], "dev": true, "license": "Apache-2.0", - "peer": true, - "dependencies": { - "@swc/counter": "^0.1.3" + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=16.20.0" } }, - "node_modules/@tootallnate/quickjs-emscripten": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", - "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@tybys/wasm-util": { - "version": "0.10.2", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", - "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "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", - "dependencies": { - "@types/deep-eql": "*", - "assertion-error": "^2.0.1" - } - }, - "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" - }, - "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" - }, - "node_modules/@types/google.maps": { - "version": "3.65.3", - "resolved": "https://registry.npmjs.org/@types/google.maps/-/google.maps-3.65.3.tgz", - "integrity": "sha512-tqbbx7MUtoDk+RwpZMymPDj6Skez0FhqDZNhLhS5UDmCx4D1dgP+BJOHTebiO/rhjJLa1f8te2p6mJJeFKVFOQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/hammerjs": { - "version": "2.0.46", - "resolved": "https://registry.npmjs.org/@types/hammerjs/-/hammerjs-2.0.46.tgz", - "integrity": "sha512-ynRvcq6wvqexJ9brDMS4BnBLzmr0e14d6ZJTEShTBWKymQiHwlAyGu0ZPEFI2Fh1U53F7tN9ufClWM5KvqkKOw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/hast": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", - "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/unist": "*" - } - }, - "node_modules/@types/jquery": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-4.0.1.tgz", - "integrity": "sha512-9a59A/tycXgYuPABcp6/3spSShn0NT2UOM4EfHvMumjYi4lJWTsK5SZWjhx3yRm9IHGCeWXdV2YfNsrWrft/CA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/linkify-it": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz", - "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/markdown-it": { - "version": "14.1.2", - "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz", - "integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/linkify-it": "^5", - "@types/mdurl": "^2" - } - }, - "node_modules/@types/mdast": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", - "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/unist": "*" - } - }, - "node_modules/@types/mdurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz", - "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/minimatch": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz", - "integrity": "sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "26.1.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.2.tgz", - "integrity": "sha512-Vu4a5UFA9rIIFJ7rB/Vaafh9lrCQszopTCx6KjFboXTGQbPNasehVR5TEiithSDGyd1DEiUByggTZsg8jukeIg==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~8.3.0" - } - }, - "node_modules/@types/resolve": { - "version": "1.20.2", - "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", - "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/sinonjs__fake-timers": { - "version": "8.1.5", - "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.5.tgz", - "integrity": "sha512-mQkU2jY8jJEF7YHjHvsQO8+3ughTL1mcnn96igfhONmR+fUPSKIkefQYpSe8bsly2Ep7oQbn/6VG5/9/0qcArQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/unist": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", - "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/validator": { - "version": "13.15.10", - "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.10.tgz", - "integrity": "sha512-T8L6i7wCuyoK8A/ZeLYt1+q0ty3Zb9+qbSSvrIVitzT3YjZqkTZ40IbRsPanlB4h1QB3JVL1SYCdR6ngtFYcuA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/web-bluetooth": { - "version": "0.0.21", - "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz", - "integrity": "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@types/which/-/which-2.0.2.tgz", - "integrity": "sha512-113D3mDkZDjo+EeUEHCFy0qniNc1ZpecGiAU7WSo7YDoSzolZIQKpYFHrPpjkB2nuyahcKfrmLXeQlh7gqJYdw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/ws": { - "version": "8.18.1", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", - "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/yauzl": { - "version": "2.10.3", - "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", - "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@typescript/typescript-aix-ppc64": { + "node_modules/@typescript/typescript-openbsd-arm64": { "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-aix-ppc64/-/typescript-aix-ppc64-7.0.2.tgz", - "integrity": "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==", + "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-arm64/-/typescript-openbsd-arm64-7.0.2.tgz", + "integrity": "sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ==", "cpu": [ - "ppc64" + "arm64" ], "dev": true, "license": "Apache-2.0", "optional": true, "os": [ - "aix" + "openbsd" ], "engines": { "node": ">=16.20.0" } }, - "node_modules/@typescript/typescript-darwin-arm64": { + "node_modules/@typescript/typescript-openbsd-x64": { "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-arm64/-/typescript-darwin-arm64-7.0.2.tgz", - "integrity": "sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA==", + "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-x64/-/typescript-openbsd-x64-7.0.2.tgz", + "integrity": "sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg==", "cpu": [ - "arm64" + "x64" ], "dev": true, "license": "Apache-2.0", "optional": true, "os": [ - "darwin" + "openbsd" ], "engines": { "node": ">=16.20.0" } }, - "node_modules/@typescript/typescript-darwin-x64": { + "node_modules/@typescript/typescript-sunos-x64": { "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-x64/-/typescript-darwin-x64-7.0.2.tgz", - "integrity": "sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA==", + "resolved": "https://registry.npmjs.org/@typescript/typescript-sunos-x64/-/typescript-sunos-x64-7.0.2.tgz", + "integrity": "sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g==", "cpu": [ "x64" ], @@ -2915,16 +3174,16 @@ "license": "Apache-2.0", "optional": true, "os": [ - "darwin" + "sunos" ], "engines": { "node": ">=16.20.0" } }, - "node_modules/@typescript/typescript-freebsd-arm64": { + "node_modules/@typescript/typescript-win32-arm64": { "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-arm64/-/typescript-freebsd-arm64-7.0.2.tgz", - "integrity": "sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ==", + "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-arm64/-/typescript-win32-arm64-7.0.2.tgz", + "integrity": "sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ==", "cpu": [ "arm64" ], @@ -2932,16 +3191,16 @@ "license": "Apache-2.0", "optional": true, "os": [ - "freebsd" + "win32" ], "engines": { "node": ">=16.20.0" } }, - "node_modules/@typescript/typescript-freebsd-x64": { + "node_modules/@typescript/typescript-win32-x64": { "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-x64/-/typescript-freebsd-x64-7.0.2.tgz", - "integrity": "sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw==", + "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-x64/-/typescript-win32-x64-7.0.2.tgz", + "integrity": "sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g==", "cpu": [ "x64" ], @@ -2949,1909 +3208,2570 @@ "license": "Apache-2.0", "optional": true, "os": [ - "freebsd" + "win32" ], "engines": { "node": ">=16.20.0" } }, - "node_modules/@typescript/typescript-linux-arm": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm/-/typescript-linux-arm-7.0.2.tgz", - "integrity": "sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ==", - "cpu": [ - "arm" - ], + "node_modules/@ungap/structured-clone": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.3.tgz", + "integrity": "sha512-60YRaenCQcVjYEKOcG824+DRGGIQ3VKErcBoAEDJZz5bKIs2ZG+X/H9Nk+Q6EVkwJk5QNApxbrc5QtBSwtrXAg==", "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=16.20.0" + "license": "ISC" + }, + "node_modules/@upsetjs/venn.js": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@upsetjs/venn.js/-/venn.js-2.0.0.tgz", + "integrity": "sha512-WbBhLrooyePuQ1VZxrJjtLvTc4NVfpOyKx0sKqioq9bX1C1m7Jgykkn8gLrtwumBioXIqam8DLxp88Adbue6Hw==", + "dev": true, + "license": "MIT", + "optionalDependencies": { + "d3-selection": "^3.0.0", + "d3-transition": "^3.0.1" } }, - "node_modules/@typescript/typescript-linux-arm64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm64/-/typescript-linux-arm64-7.0.2.tgz", - "integrity": "sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ==", - "cpu": [ - "arm64" - ], + "node_modules/@vercel/blob": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@vercel/blob/-/blob-2.8.0.tgz", + "integrity": "sha512-Nu+HWKpkgovCh/ezlG7wCVwF7RErTzLzZMbGKFBdGBCbTKyK+s5VXPLl+0+TpNEQPH8AVaGzOpIsXUOtkqylCQ==", "dev": true, "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "@vercel/oidc": "^3.6.1", + "async-retry": "^1.3.3", + "is-buffer": "^2.0.5", + "is-node-process": "^1.2.0", + "throttleit": "^2.1.0", + "undici": "^6.23.0" + }, "engines": { - "node": ">=16.20.0" + "node": ">=20.0.0" } }, - "node_modules/@typescript/typescript-linux-loong64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-loong64/-/typescript-linux-loong64-7.0.2.tgz", - "integrity": "sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ==", - "cpu": [ - "loong64" - ], + "node_modules/@vercel/blob/node_modules/undici": { + "version": "6.28.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.28.0.tgz", + "integrity": "sha512-LIY910g9TI13YS95lrMFrs8Rm/u/irgHeTWoKCoteeJ04CUJ92eEfj0rVn+7VKMPBpUPiUoBKfhNyLI23EE/KA==", "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], + "license": "MIT", "engines": { - "node": ">=16.20.0" + "node": ">=18.17" } }, - "node_modules/@typescript/typescript-linux-mips64el": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-mips64el/-/typescript-linux-mips64el-7.0.2.tgz", - "integrity": "sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA==", - "cpu": [ - "mips64el" - ], + "node_modules/@vercel/cli-config": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@vercel/cli-config/-/cli-config-0.2.3.tgz", + "integrity": "sha512-Ggh0Wmi92TUkUexmSUPkkDtvJmbjUr7IvF5T3FkSsWrXXs3GFzOujfxFpECdJZpux1JG4SWDv9BT4w++TDgD6A==", "dev": true, "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=16.20.0" + "dependencies": { + "xdg-app-paths": "5", + "zod": "4.1.11" } }, - "node_modules/@typescript/typescript-linux-ppc64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-ppc64/-/typescript-linux-ppc64-7.0.2.tgz", - "integrity": "sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA==", - "cpu": [ - "ppc64" - ], + "node_modules/@vercel/cli-exec": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@vercel/cli-exec/-/cli-exec-1.0.1.tgz", + "integrity": "sha512-g9XerViJ/paZujufXYcu5XYI2vU2rtB4sgdpjUHde5RnOkdmpu0ngH46LCFGHoPXO/C+qDPSczIHIRN+8Q2YKQ==", "dev": true, "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "execa": "5.1.1" + }, "engines": { - "node": ">=16.20.0" + "node": ">= 18" } }, - "node_modules/@typescript/typescript-linux-riscv64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-riscv64/-/typescript-linux-riscv64-7.0.2.tgz", - "integrity": "sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ==", - "cpu": [ - "riscv64" - ], + "node_modules/@vercel/cli-exec/node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, "engines": { - "node": ">=16.20.0" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" } }, - "node_modules/@typescript/typescript-linux-s390x": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-s390x/-/typescript-linux-s390x-7.0.2.tgz", - "integrity": "sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw==", - "cpu": [ - "s390x" - ], + "node_modules/@vercel/cli-exec/node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], + "license": "MIT", "engines": { - "node": ">=16.20.0" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@typescript/typescript-linux-x64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-x64/-/typescript-linux-x64-7.0.2.tgz", - "integrity": "sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A==", - "cpu": [ - "x64" - ], + "node_modules/@vercel/cli-exec/node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", "dev": true, "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], "engines": { - "node": ">=16.20.0" + "node": ">=10.17.0" } }, - "node_modules/@typescript/typescript-netbsd-arm64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-arm64/-/typescript-netbsd-arm64-7.0.2.tgz", - "integrity": "sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA==", - "cpu": [ - "arm64" - ], + "node_modules/@vercel/cli-exec/node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "netbsd" - ], + "license": "MIT", "engines": { - "node": ">=16.20.0" + "node": ">=6" } }, - "node_modules/@typescript/typescript-netbsd-x64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-x64/-/typescript-netbsd-x64-7.0.2.tgz", - "integrity": "sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA==", - "cpu": [ - "x64" - ], + "node_modules/@vercel/cli-exec/node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "netbsd" - ], + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, "engines": { - "node": ">=16.20.0" + "node": ">=8" } }, - "node_modules/@typescript/typescript-openbsd-arm64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-arm64/-/typescript-openbsd-arm64-7.0.2.tgz", - "integrity": "sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ==", - "cpu": [ - "arm64" - ], + "node_modules/@vercel/cli-exec/node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@vercel/cli-exec/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/@vercel/cli-exec/node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/@vercel/oidc": { + "version": "3.8.4", + "resolved": "https://registry.npmjs.org/@vercel/oidc/-/oidc-3.8.4.tgz", + "integrity": "sha512-FGNvVZ5pgX9FaBqkPt6VkYFZ6bWAMDzYi7nxW+1Xt+Z4fn5PuTULVwsxjKc+0uKhysyWBQmvsmM50Oh6C2/oMA==", "dev": true, "license": "Apache-2.0", - "optional": true, - "os": [ - "openbsd" - ], + "dependencies": { + "@vercel/cli-config": "0.2.3", + "@vercel/cli-exec": "1.0.1", + "jose": "^5.9.6" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@vitest/browser": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/browser/-/browser-4.1.10.tgz", + "integrity": "sha512-UDwuWGwXj646CBx/bQHOaJSX7np0I8JL/UKQYa1e4QrVHH8VdWtx8eaOuf8sy0ShwDgR6NjJAsp5eF6vjF6qng==", + "dev": true, + "license": "MIT", + "dependencies": { + "@blazediff/core": "1.9.1", + "@vitest/mocker": "4.1.10", + "@vitest/utils": "4.1.10", + "magic-string": "^0.30.21", + "pngjs": "^7.0.0", + "sirv": "^3.0.2", + "tinyrainbow": "^3.1.0", + "ws": "^8.19.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "vitest": "4.1.10" + } + }, + "node_modules/@vitest/browser-playwright": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/browser-playwright/-/browser-playwright-4.1.10.tgz", + "integrity": "sha512-nMoXGEiRpT7m3W7NsbvrM2aKNwiNHZf+zEpUCvMteGjZFvfT96Q9fh7QyB98dvDWXiKvrLxA7bJ1mCOOv+JQPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/browser": "4.1.10", + "@vitest/mocker": "4.1.10", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "playwright": "*", + "vitest": "4.1.10" + }, + "peerDependenciesMeta": { + "playwright": { + "optional": false + } + } + }, + "node_modules/@vitest/browser-webdriverio": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/browser-webdriverio/-/browser-webdriverio-4.1.10.tgz", + "integrity": "sha512-CnZZFYFV7kIda1QhnV8ZxTbO/EoGb1S48RSMf3/iB4JjXlDl2sRWJ0VtbSq6Aav3YpqDc/X5oq3p91nUIU1psA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/browser": "4.1.10" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "vitest": "4.1.10", + "webdriverio": "*" + }, + "peerDependenciesMeta": { + "webdriverio": { + "optional": false + } + } + }, + "node_modules/@vitest/browser/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", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/@vitest/expect": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.10.tgz", + "integrity": "sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.10", + "@vitest/utils": "4.1.10", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.10.tgz", + "integrity": "sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.10", + "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/@vitest/mocker/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": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/@vitest/mocker/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", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.10.tgz", + "integrity": "sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.10.tgz", + "integrity": "sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.10", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.10.tgz", + "integrity": "sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.10", + "@vitest/utils": "4.1.10", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot/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", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.10.tgz", + "integrity": "sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/ui": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/ui/-/ui-4.1.10.tgz", + "integrity": "sha512-EOUqfXHTXtpSHsyLHH40ts3Ue+hRhSGwzwzMlK0dTEOLSDYyOXLyr5JDGmHQWhN2DYI30gw6dVx3cdgM9FZl+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.10", + "fflate": "^0.8.2", + "flatted": "^3.4.2", + "pathe": "^2.0.3", + "sirv": "^3.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "vitest": "4.1.10" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.10.tgz", + "integrity": "sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.10", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.41", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.41.tgz", + "integrity": "sha512-q0Xtv/F9w2YO/7htQhtiL+Ev2WCJbe5N2hc+XfgyKkEKqWpSxknmT8QOuGdEKNdjPq0c3F7rNpFkTo3Kfrm7pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.8", + "@vue/shared": "3.5.41", + "entities": "^7.0.1", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-core/node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.41", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.41.tgz", + "integrity": "sha512-oKacVfNglLvGjnS6BXOlGL7EyG2h8X03pqXCjzotRZUaXGjbrTJUnVAQjrCqUnS+lyu31nwQjZY/d817GmCnfw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.41", + "@vue/shared": "3.5.41" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.41", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.41.tgz", + "integrity": "sha512-XJhip7R2wy6vX3knCxdZN4KracFaZUef58s1KYewqluedHIJaPIVfXoYT7MF1F8nCvv6k8bWWxDC8opMkg1VTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.8", + "@vue/compiler-core": "3.5.41", + "@vue/compiler-dom": "3.5.41", + "@vue/compiler-ssr": "3.5.41", + "@vue/shared": "3.5.41", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.19", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-sfc/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", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.41", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.41.tgz", + "integrity": "sha512-U3v5OejKEGqOI0Wy0+Sz7hGuIFZHA4LSXzrNM3IMIeDyJEBBfTpX26n3SDgToRpP2bLc9FfI2j/kSgcJ8Emq5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.41", + "@vue/shared": "3.5.41" + } + }, + "node_modules/@vue/devtools-api": { + "version": "7.7.10", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-7.7.10.tgz", + "integrity": "sha512-KxtEpUOOpFz/qOGRrAwA36QF7DqIA+FXgCYit9mk9wjbaZt0sXOFz81ElOZtKA4HbWHUdwNjZHBFsFFyp5BZiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/devtools-kit": "^7.7.10" + } + }, + "node_modules/@vue/devtools-kit": { + "version": "7.7.10", + "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-7.7.10.tgz", + "integrity": "sha512-3WNi2Kq4tbpVbmhml7RiphmAt0279oh3fKNeWMQIrltfX8Q91b4i5PL8DtyNKdwmcsGrV4fg+erwWOmD05CLIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/devtools-shared": "^7.7.10", + "birpc": "^2.3.0", + "hookable": "^5.5.3", + "mitt": "^3.0.1", + "perfect-debounce": "^1.0.0", + "speakingurl": "^14.0.1", + "superjson": "^2.2.2" + } + }, + "node_modules/@vue/devtools-shared": { + "version": "7.7.10", + "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-7.7.10.tgz", + "integrity": "sha512-wOPslzB8vTvpxwdaOcR2qAbwmuSP0L+rhpoC6Cf56V3Jip+HWb7PQQXOUPgBNQARpXsbQX/+mvi8kKucmBGRwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "rfdc": "^1.4.1" + } + }, + "node_modules/@vue/reactivity": { + "version": "3.5.41", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.41.tgz", + "integrity": "sha512-rznsqKM0np0x18EjzF8x88MpEhdNsffbvFbckLL5+oUKz1BxAImEmO7J1ArRYSyo6aQaVoBDp7jEkT91OOxydA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.41" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.41", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.41.tgz", + "integrity": "sha512-Vcry58hiAKwGen9Z1jUZE0feFsNArPCMOImYI8el48A9Idf6DuQYD0U05zZIF2Iad1hGhPSvcbBbAOhNr55fhg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.41", + "@vue/shared": "3.5.41" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.41", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.41.tgz", + "integrity": "sha512-3vVBahVBS9+U6cmXBLyb8nE6/yYo4J/CGI9eVFs3KiMc0YHuudwKyShTD65jtJy/L9PUUxNAFu4cj4LiJ0UFbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.41", + "@vue/runtime-core": "3.5.41", + "@vue/shared": "3.5.41", + "csstype": "^3.2.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.41", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.41.tgz", + "integrity": "sha512-n6hx/pNFfbD6SuyeuMVkvqox8bwf/ET9JlA/kAz/imw8sw++wkqKe2mHX5KutjPpbKE4Z56yTHszoOjGMI9igQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.41", + "@vue/runtime-dom": "3.5.41", + "@vue/shared": "3.5.41" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.41", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.41.tgz", + "integrity": "sha512-IOnwSCma8j+9xJT6b8H0dEYidC80NsYmNMlZxRsukYcSoGaDBohog5hDxzeUXdFeGWFA++vWvxqOmrr96VlqMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vueuse/core": { + "version": "12.8.2", + "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-12.8.2.tgz", + "integrity": "sha512-HbvCmZdzAu3VGi/pWYm5Ut+Kd9mn1ZHnn4L5G8kOQTPs/IwIAmJoBrmYk2ckLArgMXZj0AW3n5CAejLUO+PhdQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/web-bluetooth": "^0.0.21", + "@vueuse/metadata": "12.8.2", + "@vueuse/shared": "12.8.2", + "vue": "^3.5.13" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/integrations": { + "version": "12.8.2", + "resolved": "https://registry.npmjs.org/@vueuse/integrations/-/integrations-12.8.2.tgz", + "integrity": "sha512-fbGYivgK5uBTRt7p5F3zy6VrETlV9RtZjBqd1/HxGdjdckBgBM4ugP8LHpjolqTj14TXTxSK1ZfgPbHYyGuH7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vueuse/core": "12.8.2", + "@vueuse/shared": "12.8.2", + "vue": "^3.5.13" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "async-validator": "^4", + "axios": "^1", + "change-case": "^5", + "drauu": "^0.4", + "focus-trap": "^7", + "fuse.js": "^7", + "idb-keyval": "^6", + "jwt-decode": "^4", + "nprogress": "^0.2", + "qrcode": "^1.5", + "sortablejs": "^1", + "universal-cookie": "^7" + }, + "peerDependenciesMeta": { + "async-validator": { + "optional": true + }, + "axios": { + "optional": true + }, + "change-case": { + "optional": true + }, + "drauu": { + "optional": true + }, + "focus-trap": { + "optional": true + }, + "fuse.js": { + "optional": true + }, + "idb-keyval": { + "optional": true + }, + "jwt-decode": { + "optional": true + }, + "nprogress": { + "optional": true + }, + "qrcode": { + "optional": true + }, + "sortablejs": { + "optional": true + }, + "universal-cookie": { + "optional": true + } + } + }, + "node_modules/@vueuse/metadata": { + "version": "12.8.2", + "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-12.8.2.tgz", + "integrity": "sha512-rAyLGEuoBJ/Il5AmFHiziCPdQzRt88VxR+Y/A/QhJ1EWtWqPBBAxTAFaSkviwEuOEZNtW8pvkPgoCZQ+HxqW1A==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/shared": { + "version": "12.8.2", + "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-12.8.2.tgz", + "integrity": "sha512-dznP38YzxZoNloI0qpEfpkms8knDtaoQ6Y/sfS0L7Yki4zh40LFHEhur0odJC6xTHG5dxWVPiUWBXn+wCG2s5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "vue": "^3.5.13" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@wdio/config": { + "version": "9.30.1", + "resolved": "https://registry.npmjs.org/@wdio/config/-/config-9.30.1.tgz", + "integrity": "sha512-55D7g1RBCyHTwMkb4b6sQU0ET5lf5UUM3SgTHDGOo2KHfiydtCqOvfGwzxaf9nv200+QGY6BqI6yD0Ad1VGV0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@wdio/logger": "9.29.1", + "@wdio/types": "9.30.1", + "@wdio/utils": "9.30.1", + "deepmerge-ts": "^7.0.3", + "glob": "^10.2.2", + "import-meta-resolve": "^4.0.0", + "jiti": "^2.6.1" + }, + "engines": { + "node": ">=18.20.0" + } + }, + "node_modules/@wdio/logger": { + "version": "9.29.1", + "resolved": "https://registry.npmjs.org/@wdio/logger/-/logger-9.29.1.tgz", + "integrity": "sha512-0ZAEIo6PNyMIJPlOGkIgyOJUjcd0pC8/QHlVAAe1c91/IcjZ1X+k0yidXHaboJdN7dq1XPUacmhRdtua0U5EZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^5.1.2", + "loglevel": "^1.6.0", + "loglevel-plugin-prefix": "^0.8.4", + "safe-regex2": "^5.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18.20.0" + } + }, + "node_modules/@wdio/protocols": { + "version": "9.30.1", + "resolved": "https://registry.npmjs.org/@wdio/protocols/-/protocols-9.30.1.tgz", + "integrity": "sha512-kV0eHy6scYoXqZuAWvtYS5ebkyIF2/JdApSEu6kIdS2EppnROuvUzsNjTUzNkT6gk+0Ib9C/CbOqiuyZuhBUcA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@wdio/repl": { + "version": "9.16.2", + "resolved": "https://registry.npmjs.org/@wdio/repl/-/repl-9.16.2.tgz", + "integrity": "sha512-FLTF0VL6+o5BSTCO7yLSXocm3kUnu31zYwzdsz4n9s5YWt83sCtzGZlZpt7TaTzb3jVUfxuHNQDTb8UMkCu0lQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "^20.1.0" + }, + "engines": { + "node": ">=18.20.0" + } + }, + "node_modules/@wdio/repl/node_modules/@types/node": { + "version": "20.19.43", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz", + "integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@wdio/repl/node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@wdio/types": { + "version": "9.30.1", + "resolved": "https://registry.npmjs.org/@wdio/types/-/types-9.30.1.tgz", + "integrity": "sha512-umKQDbSmu3/ucUsIA5UUTK5gCWnClyVpWz3fC39044rCXQ3zCd1a3GoJibbAITKiwnPFf2yU3LvTa+C78sh9Lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "^20.1.0" + }, + "engines": { + "node": ">=18.20.0" + } + }, + "node_modules/@wdio/types/node_modules/@types/node": { + "version": "20.19.43", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz", + "integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@wdio/types/node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@wdio/utils": { + "version": "9.30.1", + "resolved": "https://registry.npmjs.org/@wdio/utils/-/utils-9.30.1.tgz", + "integrity": "sha512-V+JE4G6ZO9ubdjDfHDHgp/2EWs5WCXk4IT2c2ZMoWy2qALqq4wqaeZ0ce05c3z0+N+8+xMS/Q9donDiIY1yk5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@puppeteer/browsers": "^2.2.0", + "@wdio/logger": "9.29.1", + "@wdio/types": "9.30.1", + "decamelize": "^6.0.0", + "deepmerge-ts": "^7.0.3", + "edgedriver": "^6.1.2", + "geckodriver": "^6.1.1", + "get-port": "^7.0.0", + "import-meta-resolve": "^4.0.0", + "locate-app": "^2.2.24", + "mitt": "^3.0.1", + "safaridriver": "^1.0.0", + "split2": "^4.2.0", + "wait-port": "^1.1.0" + }, "engines": { - "node": ">=16.20.0" + "node": ">=18.20.0" } }, - "node_modules/@typescript/typescript-openbsd-x64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-x64/-/typescript-openbsd-x64-7.0.2.tgz", - "integrity": "sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg==", - "cpu": [ - "x64" - ], + "node_modules/@wdio/utils/node_modules/@puppeteer/browsers": { + "version": "2.13.2", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.13.2.tgz", + "integrity": "sha512-5EUZSUIc37H6aIXyWO0Z4y8NlF8NnjgmqeQgOGiswAU7pY0HOo16ho4+alIWmSfdZnjqBRawMsP3I5YqLSn6kw==", "dev": true, "license": "Apache-2.0", - "optional": true, - "os": [ - "openbsd" - ], + "dependencies": { + "debug": "^4.4.3", + "extract-zip": "^2.0.1", + "progress": "^2.0.3", + "proxy-agent": "^6.5.0", + "semver": "^7.7.4", + "tar-fs": "^3.1.1", + "yargs": "^17.7.2" + }, + "bin": { + "browsers": "lib/cjs/main-cli.js" + }, "engines": { - "node": ">=16.20.0" + "node": ">=18" } }, - "node_modules/@typescript/typescript-sunos-x64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-sunos-x64/-/typescript-sunos-x64-7.0.2.tgz", - "integrity": "sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g==", - "cpu": [ - "x64" - ], + "node_modules/@wdio/utils/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": "Apache-2.0", - "optional": true, - "os": [ - "sunos" - ], + "license": "MIT", "engines": { - "node": ">=16.20.0" + "node": ">=8" } }, - "node_modules/@typescript/typescript-win32-arm64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-arm64/-/typescript-win32-arm64-7.0.2.tgz", - "integrity": "sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ==", - "cpu": [ - "arm64" - ], + "node_modules/@wdio/utils/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": "Apache-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=16.20.0" - } + "license": "MIT" }, - "node_modules/@typescript/typescript-win32-x64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-x64/-/typescript-win32-x64-7.0.2.tgz", - "integrity": "sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g==", - "cpu": [ - "x64" - ], + "node_modules/@wdio/utils/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "win32" - ], + "license": "ISC", "engines": { - "node": ">=16.20.0" + "node": ">=12" } }, - "node_modules/@ungap/structured-clone": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.1.tgz", - "integrity": "sha512-mUFwbeTqrVgDQxFveS+df2yfap6iuP20NAKAsBt5jDEoOTDew+zwLAOilHCeQJOVSvmgCX4ogqIrA0mnyr08yQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/@vercel/blob": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/@vercel/blob/-/blob-2.4.0.tgz", - "integrity": "sha512-ncQ8CRb6XoEAYJwjOTRGpACRT6h/AeY+/33gLyeVxG5BIes27OPm1jmqreF+JHjcTmGhClTP+kBpmyLfbV0xew==", + "node_modules/@wdio/utils/node_modules/proxy-agent": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz", + "integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", "dependencies": { - "async-retry": "^1.3.3", - "is-buffer": "^2.0.5", - "is-node-process": "^1.2.0", - "throttleit": "^2.1.0", - "undici": "^6.23.0" + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "http-proxy-agent": "^7.0.1", + "https-proxy-agent": "^7.0.6", + "lru-cache": "^7.14.1", + "pac-proxy-agent": "^7.1.0", + "proxy-from-env": "^1.1.0", + "socks-proxy-agent": "^8.0.5" }, "engines": { - "node": ">=20.0.0" + "node": ">= 14" } }, - "node_modules/@vitest/browser": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@vitest/browser/-/browser-4.1.9.tgz", - "integrity": "sha512-j1BKtWmPcqpMhmx/L9EPLgAJpCb0zKfwoWLmqBbxaogCXHjOwHFSEoHCBfnGtx93xKQwilZ26m+UOsHqHMkRNg==", + "node_modules/@wdio/utils/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": { - "@blazediff/core": "1.9.1", - "@vitest/mocker": "4.1.9", - "@vitest/utils": "4.1.9", - "magic-string": "^0.30.21", - "pngjs": "^7.0.0", - "sirv": "^3.0.2", - "tinyrainbow": "^3.1.0", - "ws": "^8.19.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" }, - "peerDependencies": { - "vitest": "4.1.9" + "engines": { + "node": ">=8" } }, - "node_modules/@vitest/browser-playwright": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@vitest/browser-playwright/-/browser-playwright-4.1.9.tgz", - "integrity": "sha512-Bq1rOGf9waevzG3EOkO/dene6bvKTUsZMVg8S1i+WH3JcMjuXEjiahP9rAqZRELUqjBySOJsvvSWqK/B3wjKQw==", + "node_modules/@wdio/utils/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": { - "@vitest/browser": "4.1.9", - "@vitest/mocker": "4.1.9", - "tinyrainbow": "^3.1.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "playwright": "*", - "vitest": "4.1.9" + "ansi-regex": "^5.0.1" }, - "peerDependenciesMeta": { - "playwright": { - "optional": false - } + "engines": { + "node": ">=8" } }, - "node_modules/@vitest/browser-webdriverio": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@vitest/browser-webdriverio/-/browser-webdriverio-4.1.9.tgz", - "integrity": "sha512-bZFBg9PfjsiMJkD1cX3n7nUX+7kynZloLhnahBcbVPUP9NvxbyEhb52nSuiua2Fj+iSEhBqM/fWVDGMDZqloNg==", + "node_modules/@wdio/utils/node_modules/tar-fs": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.3.tgz", + "integrity": "sha512-/hU4AXnIdZu+Gvl1pk0oI5f5HxWsCJRtY2aFaJdk9VvyL48DWU6iU5WAIPG+wIi1YvWA6eTJvIviP/tMAZZNwQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/browser": "4.1.9" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "vitest": "4.1.9", - "webdriverio": "*" + "pump": "^3.0.0", + "tar-stream": "^3.1.5" }, - "peerDependenciesMeta": { - "webdriverio": { - "optional": false - } + "optionalDependencies": { + "bare-fs": "^4.0.1", + "bare-path": "^3.0.0" } }, - "node_modules/@vitest/expect": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.9.tgz", - "integrity": "sha512-vl/rYsUKcBr3SnQn166+XR5ZQcgMx3DQhFWdfli/cWpLnLUmbxZvyrJZotLFUryib+LtArYMSTJ5RbQ57ZqrlA==", + "node_modules/@wdio/utils/node_modules/yargs": { + "version": "17.7.3", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", + "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", "dev": true, "license": "MIT", "dependencies": { - "@standard-schema/spec": "^1.1.0", - "@types/chai": "^5.2.2", - "@vitest/spy": "4.1.9", - "@vitest/utils": "4.1.9", - "chai": "^6.2.2", - "tinyrainbow": "^3.1.0" + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" }, - "funding": { - "url": "https://opencollective.com/vitest" + "engines": { + "node": ">=12" } }, - "node_modules/@vitest/mocker": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.9.tgz", - "integrity": "sha512-EVkXzBjrPGM+cK8/ANWgBrkUCfJfb38/EfTSO8h7pWvKkyPkpWxvR7BkD2MyItMF62C97zAEoqdpUixwR/e+Rw==", + "node_modules/@zip.js/zip.js": { + "version": "2.8.44", + "resolved": "https://registry.npmjs.org/@zip.js/zip.js/-/zip.js-2.8.44.tgz", + "integrity": "sha512-zO8tDX/UVPWNgsPGcXdl/J33XlhIfF7a4C/TXi3cyZmb4IhL/HIGkPOM9ab9alpOYlXsSkUNt8U8XQsE48H1dg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "bun": ">=0.7.0", + "deno": ">=1.0.0", + "node": ">=18.0.0" + } + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "4.1.9", - "estree-walker": "^3.0.3", - "magic-string": "^0.30.21" + "event-target-shim": "^5.0.0" }, - "funding": { - "url": "https://opencollective.com/vitest" + "engines": { + "node": ">=6.5" + } + }, + "node_modules/acorn": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", + "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-import-attributes": { + "version": "1.9.5", + "resolved": "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz", + "integrity": "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==", + "dev": true, + "license": "MIT", "peerDependencies": { - "msw": "^2.4.9", - "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "msw": { - "optional": true - }, - "vite": { - "optional": true - } + "acorn": "^8" } }, - "node_modules/@vitest/mocker/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/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", "dev": true, "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0" + "engines": { + "node": ">= 14" } }, - "node_modules/@vitest/pretty-format": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.9.tgz", - "integrity": "sha512-s0iufns3iIFitdgm+YR7g1whCAaGtXz459VS9/PqyKDEEFgYIhsHOQmXgIgDuYCt7DeQmiZT0Qe2OA2p4ZPu5A==", + "node_modules/algoliasearch": { + "version": "5.56.0", + "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-5.56.0.tgz", + "integrity": "sha512-PrqppUmhT4ENdas2pH9caE7efUcxy6EcSFhWzosiVuQBzu2tQ5yLTI6jwomT/1cuBnivzGfxiJCqDNN9FRRh+Q==", "dev": true, "license": "MIT", "dependencies": { - "tinyrainbow": "^3.1.0" + "@algolia/abtesting": "1.22.0", + "@algolia/client-abtesting": "5.56.0", + "@algolia/client-analytics": "5.56.0", + "@algolia/client-common": "5.56.0", + "@algolia/client-insights": "5.56.0", + "@algolia/client-personalization": "5.56.0", + "@algolia/client-query-suggestions": "5.56.0", + "@algolia/client-search": "5.56.0", + "@algolia/ingestion": "1.56.0", + "@algolia/monitoring": "1.56.0", + "@algolia/recommend": "5.56.0", + "@algolia/requester-browser-xhr": "5.56.0", + "@algolia/requester-fetch": "5.56.0", + "@algolia/requester-node-http": "5.56.0" }, - "funding": { - "url": "https://opencollective.com/vitest" + "engines": { + "node": ">= 14.0.0" } }, - "node_modules/@vitest/runner": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.9.tgz", - "integrity": "sha512-KXLMDtc7oe70+3mJfGrPUWPesswH+3sTxAMAMl8DG7I8IUQT4XW718dY5ID3vPUcmlu27CcKfY4P3h3I29SLJg==", + "node_modules/ansi-regex": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.3.0.tgz", + "integrity": "sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ==", "dev": true, "license": "MIT", - "dependencies": { - "@vitest/utils": "4.1.9", - "pathe": "^2.0.3" + "engines": { + "node": ">=12" }, "funding": { - "url": "https://opencollective.com/vitest" + "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, - "node_modules/@vitest/snapshot": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.9.tgz", - "integrity": "sha512-Jc7RKGNBo8Z28WYIm0Niej4xdSPByRf6mU58VpHQkd6Zh05rlnA+twjbK5HyeIGHxrzsc3mJgS43uM0CZKzaIA==", + "node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", "dev": true, "license": "MIT", - "dependencies": { - "@vitest/pretty-format": "4.1.9", - "@vitest/utils": "4.1.9", - "magic-string": "^0.30.21", - "pathe": "^2.0.3" + "engines": { + "node": ">=12" }, "funding": { - "url": "https://opencollective.com/vitest" + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/@vitest/spy": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.9.tgz", - "integrity": "sha512-fHpsS6mIi+PiEW+vcRVOMkX1oSaPKne3VOclSFICPcGOmfKgXPU5iAah+wcNcj2xPrCCmfq99IDGf+EojhhvhA==", + "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", - "funding": { - "url": "https://opencollective.com/vitest" - } + "license": "MIT" }, - "node_modules/@vitest/ui": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@vitest/ui/-/ui-4.1.9.tgz", - "integrity": "sha512-U/cRvtqfEPj27FI1n9cyUvi4vXXdcLhjJiI+InYKdk8hP4VrS6RXOjGL7rfFaeBc37iRKANsR6eEzIoC7lmgBQ==", + "node_modules/anynum": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/anynum/-/anynum-1.0.1.tgz", + "integrity": "sha512-N6//FLET/tXYNM/F6ABca1oH6fWB+KlTt909Le28WMDBk8oaT4vY17DCrwg2MvmuqUKt3Ni4N5dGJ/EoBgcO6A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, + "node_modules/archiver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/archiver/-/archiver-7.0.1.tgz", + "integrity": "sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "4.1.9", - "fflate": "^0.8.2", - "flatted": "^3.4.2", - "pathe": "^2.0.3", - "sirv": "^3.0.2", - "tinyglobby": "^0.2.15", - "tinyrainbow": "^3.1.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" + "archiver-utils": "^5.0.2", + "async": "^3.2.4", + "buffer-crc32": "^1.0.0", + "readable-stream": "^4.0.0", + "readdir-glob": "^1.1.2", + "tar-stream": "^3.0.0", + "zip-stream": "^6.0.1" }, - "peerDependencies": { - "vitest": "4.1.9" + "engines": { + "node": ">= 14" } }, - "node_modules/@vitest/utils": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.9.tgz", - "integrity": "sha512-A51o8ymO5PpqlWNnBP9ZHPXDIpuMtTLlGSjN7la4US+LJzoUMyhwjA5QXlm39JexgwHKW4Xjs8Z2d3dLCXOeuA==", + "node_modules/archiver-utils": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-5.0.2.tgz", + "integrity": "sha512-wuLJMmIBQYCsGZgYLTy5FIB2pF6Lfb6cXMSF8Qywwk3t20zWnAi7zLcQFdKQmIB8wyZpY5ER38x08GbwtR2cLA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.1.9", - "convert-source-map": "^2.0.0", - "tinyrainbow": "^3.1.0" + "glob": "^10.0.0", + "graceful-fs": "^4.2.0", + "is-stream": "^2.0.1", + "lazystream": "^1.0.0", + "lodash": "^4.17.15", + "normalize-path": "^3.0.0", + "readable-stream": "^4.0.0" }, - "funding": { - "url": "https://opencollective.com/vitest" + "engines": { + "node": ">= 14" } }, - "node_modules/@vue/compiler-core": { - "version": "3.5.40", - "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.40.tgz", - "integrity": "sha512-39E8IgOhTbVDnoJFMKc2DvYnypcZwUqgUhQkccva/0m6FUwtIKSGV7n1hpVmYcFaoRAwf9pBcwnKlCEsN63ZEQ==", + "node_modules/archiver/node_modules/buffer-crc32": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-1.0.0.tgz", + "integrity": "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==", "dev": true, "license": "MIT", - "dependencies": { - "@babel/parser": "^7.29.7", - "@vue/shared": "3.5.40", - "entities": "^7.0.1", - "estree-walker": "^2.0.2", - "source-map-js": "^1.2.1" + "engines": { + "node": ">=8.0.0" } }, - "node_modules/@vue/compiler-dom": { - "version": "3.5.40", - "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.40.tgz", - "integrity": "sha512-pwkx4vqlqOspFstrcmzwkKLePVMD3PT65imRzLhanU2V1Fj4K13g6OXjanOyzw3aTAuRk84BOmY8f3rEHqPaVA==", + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true, - "license": "MIT", - "dependencies": { - "@vue/compiler-core": "3.5.40", - "@vue/shared": "3.5.40" + "license": "Python-2.0" + }, + "node_modules/aria-query": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" } }, - "node_modules/@vue/compiler-sfc": { - "version": "3.5.40", - "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.40.tgz", - "integrity": "sha512-gIf497P4kpuALcvs5n3AEg1Vdn0pSY4XbjASIfHNYF1/MP3T2Mf2STERTubysBxCRxzJGJYtF/O7vwJrxFB3Vw==", + "node_modules/array-differ": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/array-differ/-/array-differ-3.0.0.tgz", + "integrity": "sha512-THtfYS6KtME/yIAhKjZ2ul7XI96lQGHRputJQHO80LAWQnuGP4iCIN8vdMRboGbIEYBwU33q8Tch1os2+X0kMg==", "dev": true, "license": "MIT", - "dependencies": { - "@babel/parser": "^7.29.7", - "@vue/compiler-core": "3.5.40", - "@vue/compiler-dom": "3.5.40", - "@vue/compiler-ssr": "3.5.40", - "@vue/shared": "3.5.40", - "estree-walker": "^2.0.2", - "magic-string": "^0.30.21", - "postcss": "^8.5.19", - "source-map-js": "^1.2.1" + "engines": { + "node": ">=8" } }, - "node_modules/@vue/compiler-ssr": { - "version": "3.5.40", - "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.40.tgz", - "integrity": "sha512-rrE5xiXG663+vHCHa3J9p2z5OcBRjXmoqenprJxAFQxg5pSshzeBiCE6pu46axapRJ2Adk0YDA2BRZVjiHXnhg==", + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", "dev": true, "license": "MIT", - "dependencies": { - "@vue/compiler-dom": "3.5.40", - "@vue/shared": "3.5.40" + "engines": { + "node": ">=8" } }, - "node_modules/@vue/devtools-api": { - "version": "7.7.9", - "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-7.7.9.tgz", - "integrity": "sha512-kIE8wvwlcZ6TJTbNeU2HQNtaxLx3a84aotTITUuL/4bzfPxzajGBOoqjMhwZJ8L9qFYDU/lAYMEEm11dnZOD6g==", + "node_modules/arrify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", + "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==", "dev": true, "license": "MIT", - "dependencies": { - "@vue/devtools-kit": "^7.7.9" + "engines": { + "node": ">=8" } }, - "node_modules/@vue/devtools-kit": { - "version": "7.7.9", - "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-7.7.9.tgz", - "integrity": "sha512-PyQ6odHSgiDVd4hnTP+aDk2X4gl2HmLDfiyEnn3/oV+ckFDuswRs4IbBT7vacMuGdwY/XemxBoh302ctbsptuA==", + "node_modules/assert": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/assert/-/assert-2.1.0.tgz", + "integrity": "sha512-eLHpSK/Y4nhMJ07gDaAzoX/XAKS8PSaojml3M0DM4JpV1LAi5JOJ/p6H/XWrl8L+DzVEvVCW1z3vWAaB9oTsQw==", "dev": true, "license": "MIT", "dependencies": { - "@vue/devtools-shared": "^7.7.9", - "birpc": "^2.3.0", - "hookable": "^5.5.3", - "mitt": "^3.0.1", - "perfect-debounce": "^1.0.0", - "speakingurl": "^14.0.1", - "superjson": "^2.2.2" + "call-bind": "^1.0.2", + "is-nan": "^1.3.2", + "object-is": "^1.1.5", + "object.assign": "^4.1.4", + "util": "^0.12.5" } }, - "node_modules/@vue/devtools-shared": { - "version": "7.7.9", - "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-7.7.9.tgz", - "integrity": "sha512-iWAb0v2WYf0QWmxCGy0seZNDPdO3Sp5+u78ORnyeonS6MT4PC7VPrryX2BpMJrwlDeaZ6BD4vP4XKjK0SZqaeA==", + "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", - "dependencies": { - "rfdc": "^1.4.1" + "engines": { + "node": ">=12" } }, - "node_modules/@vue/reactivity": { - "version": "3.5.40", - "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.40.tgz", - "integrity": "sha512-B7ot9UlUZOi1zbq61/LvE88ZLTV8IlajTdiZTAEiDQgrnIMIZoPr9kGw0Zw46ObW62O9+H/Be3kMbfb7kYPQZA==", + "node_modules/ast-types": { + "version": "0.13.4", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", + "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", "dev": true, "license": "MIT", "dependencies": { - "@vue/shared": "3.5.40" + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" } }, - "node_modules/@vue/runtime-core": { - "version": "3.5.40", - "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.40.tgz", - "integrity": "sha512-KAZLweuZ6uUJPK1PMSQPgBU5gCjgrrfjUhSglmU9NhH+Zjepa8cnwSydPWDWHDwOgY4g3VcZ+PljbiHlURNCbw==", + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true, + "license": "MIT" + }, + "node_modules/async-retry": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/async-retry/-/async-retry-1.3.3.tgz", + "integrity": "sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==", "dev": true, "license": "MIT", "dependencies": { - "@vue/reactivity": "3.5.40", - "@vue/shared": "3.5.40" + "retry": "0.13.1" } }, - "node_modules/@vue/runtime-dom": { - "version": "3.5.40", - "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.40.tgz", - "integrity": "sha512-ZfrX8ssZQds900L9pr8AuK05ddnMsR4MPMZr8cPN9GoqoPWcXLhjvvbIA2SMv+7a97sJ1vv9pj/zxK0Cq/eEFQ==", + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", "dev": true, "license": "MIT", "dependencies": { - "@vue/reactivity": "3.5.40", - "@vue/runtime-core": "3.5.40", - "@vue/shared": "3.5.40", - "csstype": "^3.2.3" + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/@vue/server-renderer": { - "version": "3.5.40", - "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.40.tgz", - "integrity": "sha512-XNJym9WpevhTVt1HuwOrCRJ5Q+9z4BjTMrDtjTrvx74SmUll8spNTw6whWJa9mEkO4PKn5TihI/bm/8ds2QVJw==", + "node_modules/b4a": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.1.tgz", + "integrity": "sha512-aiqre1Nr0B/6DgE2N5vwTc+2/oQZ4Wh1t4NznYY4E00y8LCt6NqdRv81so00oo27D8MVKTpUa/MwUUtBLXCoDw==", "dev": true, - "license": "MIT", - "dependencies": { - "@vue/compiler-ssr": "3.5.40", - "@vue/runtime-dom": "3.5.40", - "@vue/shared": "3.5.40" + "license": "Apache-2.0", + "peerDependencies": { + "react-native-b4a": "*" + }, + "peerDependenciesMeta": { + "react-native-b4a": { + "optional": true + } } }, - "node_modules/@vue/shared": { - "version": "3.5.40", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.40.tgz", - "integrity": "sha512-WxnBtruIqOoV3rA4jeKDWzrYI5h7Cp4+pjwDi8kWGHz+IslhiN+wguLVVhtv2l8VoU02rzDCVfDjgCl1lNpZVg==", + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true, "license": "MIT" }, - "node_modules/@vueuse/core": { - "version": "12.8.2", - "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-12.8.2.tgz", - "integrity": "sha512-HbvCmZdzAu3VGi/pWYm5Ut+Kd9mn1ZHnn4L5G8kOQTPs/IwIAmJoBrmYk2ckLArgMXZj0AW3n5CAejLUO+PhdQ==", + "node_modules/bare-events": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.9.1.tgz", + "integrity": "sha512-Z0oHEHAFDZkffN8Qc39zNZjQlMDkPJRyyyZieU1VH7u8c5S+qHZ2S8ixdKIAxEjfHO7FJxXmJWgteOghVanIsg==", "dev": true, - "license": "MIT", + "license": "Apache-2.0", + "peerDependencies": { + "bare-abort-controller": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + } + } + }, + "node_modules/bare-fs": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.8.0.tgz", + "integrity": "sha512-fM+MhCvdQhZ7NV6S95a07gPSqjIYKn6mFaXfx266wN3ajZGl/+1AzH+ubkXQ0fFZvOe2nk9VHkzdYkQE5zMV3Q==", + "dev": true, + "license": "Apache-2.0", "dependencies": { - "@types/web-bluetooth": "^0.0.21", - "@vueuse/metadata": "12.8.2", - "@vueuse/shared": "12.8.2", - "vue": "^3.5.13" + "bare-events": "^2.5.4", + "bare-path": "^3.0.0", + "bare-stream": "^2.6.4", + "bare-url": "^2.2.2", + "fast-fifo": "^1.3.2" }, - "funding": { - "url": "https://github.com/sponsors/antfu" + "engines": { + "bare": ">=1.28.0" + }, + "peerDependencies": { + "bare-buffer": "*" + }, + "peerDependenciesMeta": { + "bare-buffer": { + "optional": true + } } }, - "node_modules/@vueuse/integrations": { - "version": "12.8.2", - "resolved": "https://registry.npmjs.org/@vueuse/integrations/-/integrations-12.8.2.tgz", - "integrity": "sha512-fbGYivgK5uBTRt7p5F3zy6VrETlV9RtZjBqd1/HxGdjdckBgBM4ugP8LHpjolqTj14TXTxSK1ZfgPbHYyGuH7g==", + "node_modules/bare-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.1.1.tgz", + "integrity": "sha512-JprUlveX3QjApC1cTpsUOiscADftCGVWkzitbHsRqv84hzYwYHw2mbluddsq5TvI8mH/8Ov1f4BiMAdcB0oYnQ==", "dev": true, - "license": "MIT", + "license": "Apache-2.0" + }, + "node_modules/bare-stream": { + "version": "2.13.3", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.13.3.tgz", + "integrity": "sha512-Kc+brLqvEqGkjyfiwJmImAOqLZL7OsoLKuavx+hJjgVV3nLTOjloJyPMFxjUPerGGHrNH0fLU06jjykMLWrERQ==", + "dev": true, + "license": "Apache-2.0", "dependencies": { - "@vueuse/core": "12.8.2", - "@vueuse/shared": "12.8.2", - "vue": "^3.5.13" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" + "b4a": "^1.8.1", + "streamx": "^2.25.0", + "teex": "^1.0.1" }, "peerDependencies": { - "async-validator": "^4", - "axios": "^1", - "change-case": "^5", - "drauu": "^0.4", - "focus-trap": "^7", - "fuse.js": "^7", - "idb-keyval": "^6", - "jwt-decode": "^4", - "nprogress": "^0.2", - "qrcode": "^1.5", - "sortablejs": "^1", - "universal-cookie": "^7" + "bare-abort-controller": "*", + "bare-buffer": "*", + "bare-events": "*" }, "peerDependenciesMeta": { - "async-validator": { - "optional": true - }, - "axios": { - "optional": true - }, - "change-case": { - "optional": true - }, - "drauu": { - "optional": true - }, - "focus-trap": { - "optional": true - }, - "fuse.js": { - "optional": true - }, - "idb-keyval": { - "optional": true - }, - "jwt-decode": { - "optional": true - }, - "nprogress": { - "optional": true - }, - "qrcode": { + "bare-abort-controller": { "optional": true }, - "sortablejs": { + "bare-buffer": { "optional": true }, - "universal-cookie": { + "bare-events": { "optional": true } } }, - "node_modules/@vueuse/metadata": { - "version": "12.8.2", - "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-12.8.2.tgz", - "integrity": "sha512-rAyLGEuoBJ/Il5AmFHiziCPdQzRt88VxR+Y/A/QhJ1EWtWqPBBAxTAFaSkviwEuOEZNtW8pvkPgoCZQ+HxqW1A==", + "node_modules/bare-url": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.5.2.tgz", + "integrity": "sha512-L13PCJzKG8RGvx8V1/DdMi12ERhC3tprr7/8a94BxpmnRsFqxh5XZNdhtMxu5HPkRshYOOWRGY8lDP7ZhpG9Cg==", "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/antfu" + "license": "Apache-2.0", + "dependencies": { + "bare-path": "^3.0.0" } }, - "node_modules/@vueuse/shared": { - "version": "12.8.2", - "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-12.8.2.tgz", - "integrity": "sha512-dznP38YzxZoNloI0qpEfpkms8knDtaoQ6Y/sfS0L7Yki4zh40LFHEhur0odJC6xTHG5dxWVPiUWBXn+wCG2s5w==", + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", "dev": true, - "license": "MIT", - "dependencies": { - "vue": "^3.5.13" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" }, - "node_modules/@wdio/config": { - "version": "9.29.0", - "resolved": "https://registry.npmjs.org/@wdio/config/-/config-9.29.0.tgz", - "integrity": "sha512-uwedUj6gzDy4cGJ3dj8ZiQzlrijgNMS0RxNnPzuNzbjNrgsX1aiihbWSG+c9Vonh9IXNJgZXBwXv+4N7w7sLrg==", + "node_modules/basic-ftp": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.3.1.tgz", + "integrity": "sha512-bopVNp6ugyA150DDuZfPFdt1KZ5a94ZDiwX4hMgZDzF+GttD80lEy8kj98kbyhLXnPvhtIo93mdnLIjpCAeeOw==", "dev": true, "license": "MIT", - "dependencies": { - "@wdio/logger": "9.18.0", - "@wdio/types": "9.29.0", - "@wdio/utils": "9.29.0", - "deepmerge-ts": "^7.0.3", - "glob": "^10.2.2", - "import-meta-resolve": "^4.0.0", - "jiti": "^2.6.1" - }, "engines": { - "node": ">=18.20.0" + "node": ">=10.0.0" } }, - "node_modules/@wdio/logger": { - "version": "9.18.0", - "resolved": "https://registry.npmjs.org/@wdio/logger/-/logger-9.18.0.tgz", - "integrity": "sha512-HdzDrRs+ywAqbXGKqe1i/bLtCv47plz4TvsHFH3j729OooT5VH38ctFn5aLXgECmiAKDkmH/A6kOq2Zh5DIxww==", + "node_modules/birpc": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/birpc/-/birpc-2.9.0.tgz", + "integrity": "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==", "dev": true, "license": "MIT", - "dependencies": { - "chalk": "^5.1.2", - "loglevel": "^1.6.0", - "loglevel-plugin-prefix": "^0.8.4", - "safe-regex2": "^5.0.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18.20.0" + "funding": { + "url": "https://github.com/sponsors/antfu" } }, - "node_modules/@wdio/protocols": { - "version": "9.29.0", - "resolved": "https://registry.npmjs.org/@wdio/protocols/-/protocols-9.29.0.tgz", - "integrity": "sha512-IFpft5Qvrv3n5mHZce3l9vGxg2sL+llld4G6IkBYku/lgbvevNjkAQtv0WJ12yusc+fSj4kVHvxrPNowYlPxfw==", + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", "dev": true, - "license": "MIT" + "license": "ISC" }, - "node_modules/@wdio/repl": { - "version": "9.16.2", - "resolved": "https://registry.npmjs.org/@wdio/repl/-/repl-9.16.2.tgz", - "integrity": "sha512-FLTF0VL6+o5BSTCO7yLSXocm3kUnu31zYwzdsz4n9s5YWt83sCtzGZlZpt7TaTzb3jVUfxuHNQDTb8UMkCu0lQ==", + "node_modules/brace-expansion": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz", + "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==", "dev": true, "license": "MIT", "dependencies": { - "@types/node": "^20.1.0" - }, - "engines": { - "node": ">=18.20.0" + "balanced-match": "^1.0.0" } }, - "node_modules/@wdio/repl/node_modules/@types/node": { - "version": "20.19.43", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz", - "integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==", + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", "dev": true, "license": "MIT", - "dependencies": { - "undici-types": "~6.21.0" + "engines": { + "node": "*" } }, - "node_modules/@wdio/repl/node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "dev": true, "license": "MIT" }, - "node_modules/@wdio/types": { - "version": "9.29.0", - "resolved": "https://registry.npmjs.org/@wdio/types/-/types-9.29.0.tgz", - "integrity": "sha512-hofXuWlSVTiQoqRLTHtutp4G8qvrx3bdktH9VEJuNP+hPX7eoGa/d52tCrjw06iLKuQGBjx+/+NhJG180TOidw==", + "node_modules/bundle-require": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/bundle-require/-/bundle-require-5.1.0.tgz", + "integrity": "sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==", "dev": true, "license": "MIT", "dependencies": { - "@types/node": "^20.1.0" + "load-tsconfig": "^0.2.3" }, "engines": { - "node": ">=18.20.0" + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "peerDependencies": { + "esbuild": ">=0.18" } }, - "node_modules/@wdio/types/node_modules/@types/node": { - "version": "20.19.43", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz", - "integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==", + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", "dev": true, "license": "MIT", - "dependencies": { - "undici-types": "~6.21.0" + "engines": { + "node": ">=8" } }, - "node_modules/@wdio/types/node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@wdio/utils": { - "version": "9.29.0", - "resolved": "https://registry.npmjs.org/@wdio/utils/-/utils-9.29.0.tgz", - "integrity": "sha512-2vnwgHt2C7hFxwzTzXPXYjqlUSCIAicB8UVXpgvvhl55PXnDWn1mnDBXwHTZ1sVxFSh+MSC1D5zNIAgcT1Mk3w==", + "node_modules/call-bind": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", + "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==", "dev": true, "license": "MIT", "dependencies": { - "@puppeteer/browsers": "^2.2.0", - "@wdio/logger": "9.18.0", - "@wdio/types": "9.29.0", - "decamelize": "^6.0.0", - "deepmerge-ts": "^7.0.3", - "edgedriver": "^6.1.2", - "geckodriver": "^6.1.0", - "get-port": "^7.0.0", - "import-meta-resolve": "^4.0.0", - "locate-app": "^2.2.24", - "mitt": "^3.0.1", - "safaridriver": "^1.0.0", - "split2": "^4.2.0", - "wait-port": "^1.1.0" + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "get-intrinsic": "^1.3.0", + "set-function-length": "^1.2.2" }, "engines": { - "node": ">=18.20.0" - } - }, - "node_modules/@zip.js/zip.js": { - "version": "2.8.26", - "resolved": "https://registry.npmjs.org/@zip.js/zip.js/-/zip.js-2.8.26.tgz", - "integrity": "sha512-RQ4h9F6DOiHxpdocUDrOl6xBM+yOtz+LkUol47AVWcfebGBDpZ7w7Xvz9PS24JgXvLGiXXzSAfdCdVy1tPlaFA==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "bun": ">=0.7.0", - "deno": ">=1.0.0", - "node": ">=18.0.0" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/abort-controller": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", - "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", "dev": true, "license": "MIT", "dependencies": { - "event-target-shim": "^5.0.0" + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" }, "engines": { - "node": ">=6.5" + "node": ">= 0.4" } }, - "node_modules/acorn": { - "version": "8.17.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", - "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", "dev": true, "license": "MIT", - "bin": { - "acorn": "bin/acorn" + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" }, "engines": { - "node": ">=0.4.0" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/acorn-import-attributes": { - "version": "1.9.5", - "resolved": "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz", - "integrity": "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==", + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", "dev": true, "license": "MIT", - "peerDependencies": { - "acorn": "^8" + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/acorn-walk": { - "version": "8.3.5", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", - "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", "dev": true, "license": "MIT", - "dependencies": { - "acorn": "^8.11.0" - }, "engines": { - "node": ">=0.4.0" + "node": ">=18" } }, - "node_modules/agent-base": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", - "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "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": ">= 14" + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/algoliasearch": { - "version": "5.54.0", - "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-5.54.0.tgz", - "integrity": "sha512-APAX4ajIOgsmYoUlGe++oNZkSTBgmXYM4maHC0OxC+Yo7xkaKQElV0ATZYCZA7jzrSJX1OBiqEs7mk+ZxXgYqA==", + "node_modules/chance": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/chance/-/chance-1.1.13.tgz", + "integrity": "sha512-V6lQCljcLznE7tUYUM9EOAnnKXbctE6j/rdQkYOHIWbfGQbrzTsAXNW9CdU5XCo4ArXQCj/rb6HgxPlmGJcaUg==", + "dev": true, + "license": "MIT" + }, + "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", - "dependencies": { - "@algolia/abtesting": "1.20.0", - "@algolia/client-abtesting": "5.54.0", - "@algolia/client-analytics": "5.54.0", - "@algolia/client-common": "5.54.0", - "@algolia/client-insights": "5.54.0", - "@algolia/client-personalization": "5.54.0", - "@algolia/client-query-suggestions": "5.54.0", - "@algolia/client-search": "5.54.0", - "@algolia/ingestion": "1.54.0", - "@algolia/monitoring": "1.54.0", - "@algolia/recommend": "5.54.0", - "@algolia/requester-browser-xhr": "5.54.0", - "@algolia/requester-fetch": "5.54.0", - "@algolia/requester-node-http": "5.54.0" - }, "engines": { - "node": ">= 14.0.0" + "node": ">=10" } }, - "node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", "dev": true, "license": "MIT", - "engines": { - "node": ">=12" - }, "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", "dev": true, "license": "MIT", - "engines": { - "node": ">=12" - }, "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, - "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" - }, - "node_modules/anynum": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/anynum/-/anynum-1.0.1.tgz", - "integrity": "sha512-N6//FLET/tXYNM/F6ABca1oH6fWB+KlTt909Le28WMDBk8oaT4vY17DCrwg2MvmuqUKt3Ni4N5dGJ/EoBgcO6A==", + "node_modules/charenc": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", + "integrity": "sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==", "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "license": "MIT" + "license": "BSD-3-Clause", + "engines": { + "node": "*" + } }, - "node_modules/archiver": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/archiver/-/archiver-7.0.1.tgz", - "integrity": "sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ==", + "node_modules/cheerio": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.2.0.tgz", + "integrity": "sha512-WDrybc/gKFpTYQutKIK6UvfcuxijIZfMfXaYm8NMsPQxSYvf+13fXUJ4rztGGbJcBQ/GF55gvrZ0Bc0bj/mqvg==", "dev": true, "license": "MIT", "dependencies": { - "archiver-utils": "^5.0.2", - "async": "^3.2.4", - "buffer-crc32": "^1.0.0", - "readable-stream": "^4.0.0", - "readdir-glob": "^1.1.2", - "tar-stream": "^3.0.0", - "zip-stream": "^6.0.1" + "cheerio-select": "^2.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "encoding-sniffer": "^0.2.1", + "htmlparser2": "^10.1.0", + "parse5": "^7.3.0", + "parse5-htmlparser2-tree-adapter": "^7.1.0", + "parse5-parser-stream": "^7.1.2", + "undici": "^7.19.0", + "whatwg-mimetype": "^4.0.0" }, "engines": { - "node": ">= 14" + "node": ">=20.18.1" + }, + "funding": { + "url": "https://github.com/cheeriojs/cheerio?sponsor=1" } }, - "node_modules/archiver-utils": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-5.0.2.tgz", - "integrity": "sha512-wuLJMmIBQYCsGZgYLTy5FIB2pF6Lfb6cXMSF8Qywwk3t20zWnAi7zLcQFdKQmIB8wyZpY5ER38x08GbwtR2cLA==", + "node_modules/cheerio-select": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", + "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-select": "^5.1.0", + "css-what": "^6.1.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", "dev": true, "license": "MIT", "dependencies": { - "glob": "^10.0.0", - "graceful-fs": "^4.2.0", - "is-stream": "^2.0.1", - "lazystream": "^1.0.0", - "lodash": "^4.17.15", - "normalize-path": "^3.0.0", - "readable-stream": "^4.0.0" + "readdirp": "^4.0.1" }, "engines": { - "node": ">= 14" + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" } }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "node_modules/class-validator": { + "version": "0.14.3", + "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.14.3.tgz", + "integrity": "sha512-rXXekcjofVN1LTOSw+u4u9WXVEUvNBVjORW154q/IdmYWy1nMbOU9aNtZB0t8m+FJQ9q91jlr2f9CwwUFdFMRA==", "dev": true, - "license": "Python-2.0" + "license": "MIT", + "dependencies": { + "@types/validator": "^13.15.3", + "libphonenumber-js": "^1.11.1", + "validator": "^13.15.20" + } }, - "node_modules/aria-query": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", - "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", "dev": true, - "license": "Apache-2.0", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, "engines": { - "node": ">= 0.4" + "node": ">=12" } }, - "node_modules/array-differ": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/array-differ/-/array-differ-3.0.0.tgz", - "integrity": "sha512-THtfYS6KtME/yIAhKjZ2ul7XI96lQGHRputJQHO80LAWQnuGP4iCIN8vdMRboGbIEYBwU33q8Tch1os2+X0kMg==", + "node_modules/cliui/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/array-union": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "node_modules/cliui/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/cliui/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/cliui/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/arrify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", - "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==", + "node_modules/cliui/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/assert": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/assert/-/assert-2.1.0.tgz", - "integrity": "sha512-eLHpSK/Y4nhMJ07gDaAzoX/XAKS8PSaojml3M0DM4JpV1LAi5JOJ/p6H/XWrl8L+DzVEvVCW1z3vWAaB9oTsQw==", + "node_modules/cliui/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": { - "call-bind": "^1.0.2", - "is-nan": "^1.3.2", - "object-is": "^1.1.5", - "object.assign": "^4.1.4", - "util": "^0.12.5" - } - }, - "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", + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, "engines": { - "node": ">=12" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/ast-types": { - "version": "0.13.4", - "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", - "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", + "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", "dependencies": { - "tslib": "^2.0.1" + "color-name": "~1.1.4" }, "engines": { - "node": ">=4" + "node": ">=7.0.0" } }, - "node_modules/async": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", - "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "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/async-retry": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/async-retry/-/async-retry-1.3.3.tgz", - "integrity": "sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==", + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", "dev": true, "license": "MIT", - "dependencies": { - "retry": "0.13.1" + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/available-typed-arrays": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", - "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", "dev": true, "license": "MIT", - "dependencies": { - "possible-typed-array-names": "^1.0.0" - }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">= 6" } }, - "node_modules/b4a": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.1.tgz", - "integrity": "sha512-aiqre1Nr0B/6DgE2N5vwTc+2/oQZ4Wh1t4NznYY4E00y8LCt6NqdRv81so00oo27D8MVKTpUa/MwUUtBLXCoDw==", + "node_modules/compress-commons": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-6.0.2.tgz", + "integrity": "sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg==", "dev": true, - "license": "Apache-2.0", - "peerDependencies": { - "react-native-b4a": "*" + "license": "MIT", + "dependencies": { + "crc-32": "^1.2.0", + "crc32-stream": "^6.0.0", + "is-stream": "^2.0.1", + "normalize-path": "^3.0.0", + "readable-stream": "^4.0.0" }, - "peerDependenciesMeta": { - "react-native-b4a": { - "optional": true - } + "engines": { + "node": ">= 14" } }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true, "license": "MIT" }, - "node_modules/bare-events": { - "version": "2.9.1", - "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.9.1.tgz", - "integrity": "sha512-Z0oHEHAFDZkffN8Qc39zNZjQlMDkPJRyyyZieU1VH7u8c5S+qHZ2S8ixdKIAxEjfHO7FJxXmJWgteOghVanIsg==", - "dev": true, - "license": "Apache-2.0", - "peerDependencies": { - "bare-abort-controller": "*" - }, - "peerDependenciesMeta": { - "bare-abort-controller": { - "optional": true - } - } - }, - "node_modules/bare-fs": { - "version": "4.7.2", - "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.7.2.tgz", - "integrity": "sha512-aTvMFUWkBmjzKtEQMDGGDNF8bkfpD5N1b/FCwt7A3wrU4t1o/e/85Wzkluh6JlODCjqVESYCkQCdTXqZ9G7VFg==", + "node_modules/confbox": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", "dev": true, - "license": "Apache-2.0", - "dependencies": { - "bare-events": "^2.5.4", - "bare-path": "^3.0.0", - "bare-stream": "^2.6.4", - "bare-url": "^2.2.2", - "fast-fifo": "^1.3.2" - }, - "engines": { - "bare": ">=1.16.0" - }, - "peerDependencies": { - "bare-buffer": "*" - }, - "peerDependenciesMeta": { - "bare-buffer": { - "optional": true - } - } + "license": "MIT" }, - "node_modules/bare-os": { - "version": "3.9.1", - "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.9.1.tgz", - "integrity": "sha512-6M5XjcnsygQNPMCMPXSK379xrJFiZ/AEMNBmFEmQW8d/789VQATvriyi5r0HYTL9TkQ26rn3kgdTG3aisbrXkQ==", + "node_modules/consola": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", + "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", "engines": { - "bare": ">=1.14.0" + "node": "^14.18.0 || >=16.10.0" } }, - "node_modules/bare-path": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.1.tgz", - "integrity": "sha512-ghj2DSK/2e99a1anTVPCV4m4YIYtrbXhfM7V3D7XZLOTsybnYyaJloymGqssQc8l/or0UoDyRtNQkmkEF/ysgQ==", + "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": "Apache-2.0", - "dependencies": { - "bare-os": "^3.0.1" - } + "license": "MIT" }, - "node_modules/bare-stream": { - "version": "2.13.3", - "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.13.3.tgz", - "integrity": "sha512-Kc+brLqvEqGkjyfiwJmImAOqLZL7OsoLKuavx+hJjgVV3nLTOjloJyPMFxjUPerGGHrNH0fLU06jjykMLWrERQ==", + "node_modules/copy-anything": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-4.0.5.tgz", + "integrity": "sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", "dependencies": { - "b4a": "^1.8.1", - "streamx": "^2.25.0", - "teex": "^1.0.1" + "is-what": "^5.2.0" }, - "peerDependencies": { - "bare-abort-controller": "*", - "bare-buffer": "*", - "bare-events": "*" + "engines": { + "node": ">=18" }, - "peerDependenciesMeta": { - "bare-abort-controller": { - "optional": true - }, - "bare-buffer": { - "optional": true - }, - "bare-events": { - "optional": true - } - } - }, - "node_modules/bare-url": { - "version": "2.4.5", - "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.4.5.tgz", - "integrity": "sha512-K+y9xF1tN+CdPu4qWwr0QiK1Al07eFPGYK5M2pDXcmHdMdgC/tT/bpmMe1hrmRHaidKLkXrC+cRNYf3XVDUhSQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "bare-path": "^3.0.0" + "funding": { + "url": "https://github.com/sponsors/mesqueeb" } }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/basic-ftp": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.3.1.tgz", - "integrity": "sha512-bopVNp6ugyA150DDuZfPFdt1KZ5a94ZDiwX4hMgZDzF+GttD80lEy8kj98kbyhLXnPvhtIo93mdnLIjpCAeeOw==", + "license": "MIT" + }, + "node_modules/cose-base": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-1.0.3.tgz", + "integrity": "sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==", "dev": true, "license": "MIT", - "engines": { - "node": ">=10.0.0" + "dependencies": { + "layout-base": "^1.0.0" } }, - "node_modules/birpc": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/birpc/-/birpc-2.9.0.tgz", - "integrity": "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==", + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/antfu" + "license": "Apache-2.0", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" } }, - "node_modules/boolbase": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", - "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "node_modules/crc32-stream": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-6.0.0.tgz", + "integrity": "sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g==", "dev": true, - "license": "ISC" + "license": "MIT", + "dependencies": { + "crc-32": "^1.2.0", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } }, - "node_modules/brace-expansion": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz", - "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==", + "node_modules/cross-env": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-10.1.0.tgz", + "integrity": "sha512-GsYosgnACZTADcmEyJctkJIoqAhHjttw7RsFrVoJNXbsWWqaq6Ym+7kZjq6mS45O0jij6vtiReppKQEtqWy6Dw==", "dev": true, "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0" + "@epic-web/invariant": "^1.0.0", + "cross-spawn": "^7.0.6" + }, + "bin": { + "cross-env": "dist/bin/cross-env.js", + "cross-env-shell": "dist/bin/cross-env-shell.js" + }, + "engines": { + "node": ">=20" } }, - "node_modules/buffer": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", - "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], "license": "MIT", "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.2.1" + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" } }, - "node_modules/buffer-crc32": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-1.0.0.tgz", - "integrity": "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==", + "node_modules/crypt": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", + "integrity": "sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==", "dev": true, - "license": "MIT", + "license": "BSD-3-Clause", "engines": { - "node": ">=8.0.0" + "node": "*" } }, - "node_modules/buffer-from": { + "node_modules/css-select": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", + "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-shorthand-properties": { "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "resolved": "https://registry.npmjs.org/css-shorthand-properties/-/css-shorthand-properties-1.1.2.tgz", + "integrity": "sha512-C2AugXIpRGQTxaCW0N7n5jD/p5irUmCrwl03TrnMFBHDbdq44CFWR2zO7rK9xPN4Eo3pUxC4vQzQgbIpzrD1PQ==", "dev": true, "license": "MIT" }, - "node_modules/bundle-require": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/bundle-require/-/bundle-require-5.1.0.tgz", - "integrity": "sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==", + "node_modules/css-value": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/css-value/-/css-value-0.0.1.tgz", + "integrity": "sha512-FUV3xaJ63buRLgHrLQVlVgQnQdR4yqdLGaDu7g8CQcWjInDfM9plBTPI9FRfpahju1UBSaMckeb2/46ApS/V1Q==", + "dev": true + }, + "node_modules/css-what": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", "dev": true, - "license": "MIT", - "dependencies": { - "load-tsconfig": "^0.2.3" - }, + "license": "BSD-2-Clause", "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "node": ">= 6" }, - "peerDependencies": { - "esbuild": ">=0.18" + "funding": { + "url": "https://github.com/sponsors/fb55" } }, - "node_modules/cac": { - "version": "6.7.14", - "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", - "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cytoscape": { + "version": "3.34.1", + "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.34.1.tgz", + "integrity": "sha512-Lr0RvH9H75y9ar8h9Toy6u4lxRSCcxUq+hHcQ26sVWo6BnaQp1gwEZOYqwuYTZhyW7npyKnNLP8oJ2p1/3OZ7g==", "dev": true, "license": "MIT", "engines": { - "node": ">=8" + "node": ">=0.10" } }, - "node_modules/call-bind": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", - "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==", + "node_modules/cytoscape-cose-bilkent": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cytoscape-cose-bilkent/-/cytoscape-cose-bilkent-4.1.0.tgz", + "integrity": "sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==", "dev": true, "license": "MIT", "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "get-intrinsic": "^1.3.0", - "set-function-length": "^1.2.2" - }, - "engines": { - "node": ">= 0.4" + "cose-base": "^1.0.0" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "peerDependencies": { + "cytoscape": "^3.2.0" } }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "node_modules/cytoscape-fcose": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cytoscape-fcose/-/cytoscape-fcose-2.2.0.tgz", + "integrity": "sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ==", "dev": true, "license": "MIT", "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" + "cose-base": "^2.2.0" }, - "engines": { - "node": ">= 0.4" + "peerDependencies": { + "cytoscape": "^3.2.0" } }, - "node_modules/call-bound": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "node_modules/cytoscape-fcose/node_modules/cose-base": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-2.2.0.tgz", + "integrity": "sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==", "dev": true, "license": "MIT", "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "layout-base": "^2.0.0" } }, - "node_modules/ccount": { + "node_modules/cytoscape-fcose/node_modules/layout-base": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", - "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-2.0.1.tgz", + "integrity": "sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg==", "dev": true, - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } + "license": "MIT" }, - "node_modules/chai": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", - "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "node_modules/d3": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/d3/-/d3-7.9.0.tgz", + "integrity": "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==", "dev": true, - "license": "MIT", + "license": "ISC", + "dependencies": { + "d3-array": "3", + "d3-axis": "3", + "d3-brush": "3", + "d3-chord": "3", + "d3-color": "3", + "d3-contour": "4", + "d3-delaunay": "6", + "d3-dispatch": "3", + "d3-drag": "3", + "d3-dsv": "3", + "d3-ease": "3", + "d3-fetch": "3", + "d3-force": "3", + "d3-format": "3", + "d3-geo": "3", + "d3-hierarchy": "3", + "d3-interpolate": "3", + "d3-path": "3", + "d3-polygon": "3", + "d3-quadtree": "3", + "d3-random": "3", + "d3-scale": "4", + "d3-scale-chromatic": "3", + "d3-selection": "3", + "d3-shape": "3", + "d3-time": "3", + "d3-time-format": "4", + "d3-timer": "3", + "d3-transition": "3", + "d3-zoom": "3" + }, "engines": { - "node": ">=18" + "node": ">=12" } }, - "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==", + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", "dev": true, - "license": "MIT", - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "engines": { + "node": ">=12" } }, - "node_modules/chance": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/chance/-/chance-1.1.13.tgz", - "integrity": "sha512-V6lQCljcLznE7tUYUM9EOAnnKXbctE6j/rdQkYOHIWbfGQbrzTsAXNW9CdU5XCo4ArXQCj/rb6HgxPlmGJcaUg==", - "dev": true, - "license": "MIT" - }, - "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==", + "node_modules/d3-axis": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz", + "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==", "dev": true, - "license": "MIT", + "license": "ISC", "engines": { - "node": ">=10" + "node": ">=12" } }, - "node_modules/character-entities-html4": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", - "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "node_modules/d3-brush": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz", + "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==", "dev": true, - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "3", + "d3-transition": "3" + }, + "engines": { + "node": ">=12" } }, - "node_modules/character-entities-legacy": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", - "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "node_modules/d3-chord": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz", + "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==", "dev": true, - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" + "license": "ISC", + "dependencies": { + "d3-path": "1 - 3" + }, + "engines": { + "node": ">=12" } }, - "node_modules/charenc": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", - "integrity": "sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==", + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", "dev": true, - "license": "BSD-3-Clause", + "license": "ISC", "engines": { - "node": "*" + "node": ">=12" } }, - "node_modules/check-error": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", - "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", + "node_modules/d3-contour": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz", + "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==", "dev": true, - "license": "MIT", + "license": "ISC", "dependencies": { - "get-func-name": "^2.0.2" + "d3-array": "^3.2.0" }, "engines": { - "node": "*" + "node": ">=12" } }, - "node_modules/chokidar": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", - "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "node_modules/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", "dev": true, - "license": "MIT", + "license": "ISC", "dependencies": { - "readdirp": "^4.0.1" + "delaunator": "5" }, "engines": { - "node": ">= 14.16.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" + "node": ">=12" } }, - "node_modules/class-validator": { - "version": "0.14.3", - "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.14.3.tgz", - "integrity": "sha512-rXXekcjofVN1LTOSw+u4u9WXVEUvNBVjORW154q/IdmYWy1nMbOU9aNtZB0t8m+FJQ9q91jlr2f9CwwUFdFMRA==", + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", "dev": true, - "license": "MIT", - "dependencies": { - "@types/validator": "^13.15.3", - "libphonenumber-js": "^1.11.1", - "validator": "^13.15.20" + "license": "ISC", + "engines": { + "node": ">=12" } }, - "node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", "dev": true, "license": "ISC", "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" + "d3-dispatch": "1 - 3", + "d3-selection": "3" }, "engines": { "node": ">=12" } }, - "node_modules/cliui/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==", + "node_modules/d3-dsv": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz", + "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", "dev": true, - "license": "MIT", + "license": "ISC", + "dependencies": { + "commander": "7", + "iconv-lite": "0.6", + "rw": "1" + }, + "bin": { + "csv2json": "bin/dsv2json.js", + "csv2tsv": "bin/dsv2dsv.js", + "dsv2dsv": "bin/dsv2dsv.js", + "dsv2json": "bin/dsv2json.js", + "json2csv": "bin/json2dsv.js", + "json2dsv": "bin/json2dsv.js", + "json2tsv": "bin/json2dsv.js", + "tsv2csv": "bin/dsv2dsv.js", + "tsv2json": "bin/dsv2json.js" + }, "engines": { - "node": ">=8" + "node": ">=12" } }, - "node_modules/cliui/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==", + "node_modules/d3-dsv/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", "dev": true, "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "node": ">= 10" } }, - "node_modules/cliui/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==", + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", "dev": true, - "license": "MIT" + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } }, - "node_modules/cliui/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==", + "node_modules/d3-fetch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz", + "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==", "dev": true, - "license": "MIT", + "license": "ISC", "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" + "d3-dsv": "1 - 3" }, "engines": { - "node": ">=8" + "node": ">=12" } }, - "node_modules/cliui/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==", + "node_modules/d3-force": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz", + "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", "dev": true, - "license": "MIT", + "license": "ISC", "dependencies": { - "ansi-regex": "^5.0.1" + "d3-dispatch": "1 - 3", + "d3-quadtree": "1 - 3", + "d3-timer": "1 - 3" }, "engines": { - "node": ">=8" + "node": ">=12" } }, - "node_modules/cliui/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==", + "node_modules/d3-format": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", + "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", "dev": true, - "license": "MIT", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-geo": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz", + "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==", + "dev": true, + "license": "ISC", "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" + "d3-array": "2.5.0 - 3" }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + "node": ">=12" } }, - "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==", + "node_modules/d3-hierarchy": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", + "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==", "dev": true, - "license": "MIT", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "dev": true, + "license": "ISC", "dependencies": { - "color-name": "~1.1.4" + "d3-color": "1 - 3" }, "engines": { - "node": ">=7.0.0" + "node": ">=12" } }, - "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==", + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", "dev": true, - "license": "MIT" + "license": "ISC", + "engines": { + "node": ">=12" + } }, - "node_modules/comma-separated-tokens": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", - "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "node_modules/d3-polygon": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz", + "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==", "dev": true, - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" + "license": "ISC", + "engines": { + "node": ">=12" } }, - "node_modules/commander": { - "version": "9.5.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", - "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "node_modules/d3-quadtree": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz", + "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==", "dev": true, - "license": "MIT", + "license": "ISC", "engines": { - "node": "^12.20.0 || >=14" + "node": ">=12" } }, - "node_modules/compress-commons": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-6.0.2.tgz", - "integrity": "sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg==", + "node_modules/d3-random": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz", + "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==", "dev": true, - "license": "MIT", - "dependencies": { - "crc-32": "^1.2.0", - "crc32-stream": "^6.0.0", - "is-stream": "^2.0.1", - "normalize-path": "^3.0.0", - "readable-stream": "^4.0.0" - }, + "license": "ISC", "engines": { - "node": ">= 14" + "node": ">=12" } }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "node_modules/d3-sankey": { + "version": "0.12.3", + "resolved": "https://registry.npmjs.org/d3-sankey/-/d3-sankey-0.12.3.tgz", + "integrity": "sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ==", "dev": true, - "license": "MIT" + "license": "BSD-3-Clause", + "dependencies": { + "d3-array": "1 - 2", + "d3-shape": "^1.2.0" + } }, - "node_modules/confbox": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", - "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", + "node_modules/d3-sankey/node_modules/d3-array": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.12.1.tgz", + "integrity": "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==", "dev": true, - "license": "MIT" + "license": "BSD-3-Clause", + "dependencies": { + "internmap": "^1.0.0" + } }, - "node_modules/consola": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", - "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", + "node_modules/d3-sankey/node_modules/d3-path": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz", + "integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==", "dev": true, - "license": "MIT", - "engines": { - "node": "^14.18.0 || >=16.10.0" + "license": "BSD-3-Clause" + }, + "node_modules/d3-sankey/node_modules/d3-shape": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz", + "integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "d3-path": "1" } }, - "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==", + "node_modules/d3-sankey/node_modules/internmap": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-1.0.1.tgz", + "integrity": "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==", "dev": true, - "license": "MIT" + "license": "ISC" }, - "node_modules/copy-anything": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-4.0.5.tgz", - "integrity": "sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==", + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==", "dev": true, - "license": "MIT", + "license": "ISC", "dependencies": { - "is-what": "^5.2.0" + "d3-color": "1 - 3", + "d3-interpolate": "1 - 3" }, "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/mesqueeb" + "node": ">=12" } }, - "node_modules/core-util-is": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/crc-32": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", - "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", "dev": true, - "license": "Apache-2.0", - "bin": { - "crc32": "bin/crc32.njs" - }, + "license": "ISC", "engines": { - "node": ">=0.8" + "node": ">=12" } }, - "node_modules/crc32-stream": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-6.0.0.tgz", - "integrity": "sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g==", + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", "dev": true, - "license": "MIT", + "license": "ISC", "dependencies": { - "crc-32": "^1.2.0", - "readable-stream": "^4.0.0" + "d3-path": "^3.1.0" }, "engines": { - "node": ">= 14" + "node": ">=12" } }, - "node_modules/cross-env": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-10.1.0.tgz", - "integrity": "sha512-GsYosgnACZTADcmEyJctkJIoqAhHjttw7RsFrVoJNXbsWWqaq6Ym+7kZjq6mS45O0jij6vtiReppKQEtqWy6Dw==", + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", "dev": true, - "license": "MIT", + "license": "ISC", "dependencies": { - "@epic-web/invariant": "^1.0.0", - "cross-spawn": "^7.0.6" - }, - "bin": { - "cross-env": "dist/bin/cross-env.js", - "cross-env-shell": "dist/bin/cross-env-shell.js" + "d3-array": "2 - 3" }, "engines": { - "node": ">=20" + "node": ">=12" } }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", "dev": true, - "license": "MIT", + "license": "ISC", "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" + "d3-time": "1 - 3" }, "engines": { - "node": ">= 8" + "node": ">=12" } }, - "node_modules/crypt": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", - "integrity": "sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==", + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", "dev": true, - "license": "BSD-3-Clause", + "license": "ISC", "engines": { - "node": "*" + "node": ">=12" } }, - "node_modules/css-shorthand-properties": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/css-shorthand-properties/-/css-shorthand-properties-1.1.2.tgz", - "integrity": "sha512-C2AugXIpRGQTxaCW0N7n5jD/p5irUmCrwl03TrnMFBHDbdq44CFWR2zO7rK9xPN4Eo3pUxC4vQzQgbIpzrD1PQ==", + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", "dev": true, - "license": "MIT" - }, - "node_modules/css-value": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/css-value/-/css-value-0.0.1.tgz", - "integrity": "sha512-FUV3xaJ63buRLgHrLQVlVgQnQdR4yqdLGaDu7g8CQcWjInDfM9plBTPI9FRfpahju1UBSaMckeb2/46ApS/V1Q==", - "dev": true + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } }, - "node_modules/css-what": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", - "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">= 6" + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" }, - "funding": { - "url": "https://github.com/sponsors/fb55" + "engines": { + "node": ">=12" } }, - "node_modules/csstype": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", - "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "node_modules/dagre-d3-es": { + "version": "7.0.14", + "resolved": "https://registry.npmjs.org/dagre-d3-es/-/dagre-d3-es-7.0.14.tgz", + "integrity": "sha512-P4rFMVq9ESWqmOgK+dlXvOtLwYg0i7u0HBGJER0LZDJT2VHIPAMZ/riPxqJceWMStH5+E61QxFra9kIS3AqdMg==", "dev": true, - "license": "MIT" + "license": "MIT", + "dependencies": { + "d3": "^7.9.0", + "lodash-es": "^4.17.21" + } }, "node_modules/data-uri-to-buffer": { "version": "6.0.2", @@ -4863,6 +5783,13 @@ "node": ">= 14" } }, + "node_modules/dayjs": { + "version": "1.11.21", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.21.tgz", + "integrity": "sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA==", + "dev": true, + "license": "MIT" + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -4894,19 +5821,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/deep-eql": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.4.tgz", - "integrity": "sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==", - "dev": true, - "license": "MIT", - "dependencies": { - "type-detect": "^4.0.0" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -4925,10 +5839,20 @@ } }, "node_modules/deepmerge-ts": { - "version": "7.1.5", - "resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-7.1.5.tgz", - "integrity": "sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==", + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-7.1.6.tgz", + "integrity": "sha512-gQhL1ksGBLQbHeAo47YU6cs2ahd3Pv+8PFYoWogNZbQnNwQyOh9Ad5kbeHFiWwbKdeWQJ5Z7Y7mwb9ew2hXBLw==", "dev": true, + "funding": [ + { + "type": "ko-fi", + "url": "https://ko-fi.com/rebeccastevens" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/deepmerge-ts" + } + ], "license": "BSD-3-Clause", "engines": { "node": ">=16.0.0" @@ -4985,6 +5909,16 @@ "node": ">= 14" } }, + "node_modules/delaunator": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.1.0.tgz", + "integrity": "sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "robust-predicates": "^3.0.2" + } + }, "node_modules/dequal": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", @@ -4995,16 +5929,6 @@ "node": ">=6" } }, - "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/devlop": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", @@ -5019,14 +5943,19 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/diff-sequences": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", - "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", "dev": true, "license": "MIT", - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" } }, "node_modules/domelementtype": { @@ -5042,6 +5971,47 @@ ], "license": "BSD-2-Clause" }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/dompurify": { + "version": "3.4.13", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.13.tgz", + "integrity": "sha512-2vmYIoqjze2d+kakP8S/nS5shfsl587kzwEjcGlTdiksUVgFHnFCsLYDVj/JNqJVOQZGSYBTmuycv0PodwmnMQ==", + "dev": true, + "license": "(MPL-2.0 OR Apache-2.0)", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -5170,9 +6140,9 @@ } }, "node_modules/entities": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", - "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", "dev": true, "license": "BSD-2-Clause", "engines": { @@ -5219,9 +6189,9 @@ } }, "node_modules/es-module-lexer": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", - "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.1.tgz", + "integrity": "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==", "dev": true, "license": "MIT" }, @@ -5238,10 +6208,22 @@ "node": ">= 0.4" } }, + "node_modules/es-toolkit": { + "version": "1.50.0", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.50.0.tgz", + "integrity": "sha512-OyZKhUVvEep9ITEiwHn8GKnMRQIVqoSIX7WnRbkWgJkllCujilqP2rD0u979tkl8wqyc8ICwlc1UBVv/Sl1G6w==", + "dev": true, + "license": "MIT", + "workspaces": [ + "docs", + "benchmarks", + "tests/types" + ] + }, "node_modules/esbuild": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", - "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", + "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -5252,32 +6234,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.28.1", - "@esbuild/android-arm": "0.28.1", - "@esbuild/android-arm64": "0.28.1", - "@esbuild/android-x64": "0.28.1", - "@esbuild/darwin-arm64": "0.28.1", - "@esbuild/darwin-x64": "0.28.1", - "@esbuild/freebsd-arm64": "0.28.1", - "@esbuild/freebsd-x64": "0.28.1", - "@esbuild/linux-arm": "0.28.1", - "@esbuild/linux-arm64": "0.28.1", - "@esbuild/linux-ia32": "0.28.1", - "@esbuild/linux-loong64": "0.28.1", - "@esbuild/linux-mips64el": "0.28.1", - "@esbuild/linux-ppc64": "0.28.1", - "@esbuild/linux-riscv64": "0.28.1", - "@esbuild/linux-s390x": "0.28.1", - "@esbuild/linux-x64": "0.28.1", - "@esbuild/netbsd-arm64": "0.28.1", - "@esbuild/netbsd-x64": "0.28.1", - "@esbuild/openbsd-arm64": "0.28.1", - "@esbuild/openbsd-x64": "0.28.1", - "@esbuild/openharmony-arm64": "0.28.1", - "@esbuild/sunos-x64": "0.28.1", - "@esbuild/win32-arm64": "0.28.1", - "@esbuild/win32-ia32": "0.28.1", - "@esbuild/win32-x64": "0.28.1" + "@esbuild/aix-ppc64": "0.28.2", + "@esbuild/android-arm": "0.28.2", + "@esbuild/android-arm64": "0.28.2", + "@esbuild/android-x64": "0.28.2", + "@esbuild/darwin-arm64": "0.28.2", + "@esbuild/darwin-x64": "0.28.2", + "@esbuild/freebsd-arm64": "0.28.2", + "@esbuild/freebsd-x64": "0.28.2", + "@esbuild/linux-arm": "0.28.2", + "@esbuild/linux-arm64": "0.28.2", + "@esbuild/linux-ia32": "0.28.2", + "@esbuild/linux-loong64": "0.28.2", + "@esbuild/linux-mips64el": "0.28.2", + "@esbuild/linux-ppc64": "0.28.2", + "@esbuild/linux-riscv64": "0.28.2", + "@esbuild/linux-s390x": "0.28.2", + "@esbuild/linux-x64": "0.28.2", + "@esbuild/netbsd-arm64": "0.28.2", + "@esbuild/netbsd-x64": "0.28.2", + "@esbuild/openbsd-arm64": "0.28.2", + "@esbuild/openbsd-x64": "0.28.2", + "@esbuild/openharmony-arm64": "0.28.2", + "@esbuild/sunos-x64": "0.28.2", + "@esbuild/win32-arm64": "0.28.2", + "@esbuild/win32-ia32": "0.28.2", + "@esbuild/win32-x64": "0.28.2" } }, "node_modules/escalade": { @@ -5312,6 +6294,17 @@ "source-map": "~0.6.1" } }, + "node_modules/escodegen/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/eslint-scope": { "version": "8.4.0", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", @@ -5398,88 +6391,38 @@ }, "node_modules/event-target-shim": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", - "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/events": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", - "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.x" - } - }, - "node_modules/events-universal": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", - "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "bare-events": "^2.7.0" - } - }, - "node_modules/execa": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", - "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^8.0.1", - "human-signals": "^5.0.0", - "is-stream": "^3.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^5.1.0", - "onetime": "^6.0.0", - "signal-exit": "^4.1.0", - "strip-final-newline": "^3.0.0" - }, - "engines": { - "node": ">=16.17" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - } - }, - "node_modules/execa/node_modules/get-stream": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", - "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", "dev": true, "license": "MIT", "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=6" } }, - "node_modules/execa/node_modules/is-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", - "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", "dev": true, "license": "MIT", "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=0.8.x" + } + }, + "node_modules/events-universal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", + "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.7.0" } }, "node_modules/expect-type": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", - "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -5529,9 +6472,9 @@ "license": "MIT" }, "node_modules/fast-xml-builder": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.2.0.tgz", - "integrity": "sha512-00aAWieqff+ZJhsXA4g1g7M8k+7AYoMUUHF+/zFb5U6Uv/P0Vl4QZo84/IcufzYalLuEj9928bXN9PbbFzMF0Q==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.3.0.tgz", + "integrity": "sha512-F74cZEdCvuw9P41GAC3rod4X04jjWGM1JPEv/GWSqFTWLsdyMSBMBMlm9Hk3GLBgLBbdBNY8yee0pQh2RBVESQ==", "dev": true, "funding": [ { @@ -5541,14 +6484,14 @@ ], "license": "MIT", "dependencies": { - "path-expression-matcher": "^1.5.0", - "xml-naming": "^0.1.0" + "path-expression-matcher": "^1.6.2", + "xml-naming": "^0.3.0" } }, "node_modules/fast-xml-parser": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.9.3.tgz", - "integrity": "sha512-brCNCeScma/kqa54J4PIDriSSSLssRkuYaUCpvHJulGc3HGI/xxKUCTDcYkAdqJsyb//ydpbxecjC3hB9+tb/g==", + "version": "5.10.1", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.10.1.tgz", + "integrity": "sha512-IEMIf7298kXuZSRFoGfMYrl7is8LpavODgbNz1cwIudv7KwVFnuU+UsMporfq6PD6aXSlawZlARiA3UywCTfMw==", "dev": true, "funding": [ { @@ -5558,12 +6501,12 @@ ], "license": "MIT", "dependencies": { - "@nodable/entities": "^2.2.0", + "@nodable/entities": "^3.0.0", "fast-xml-builder": "^1.2.0", - "is-unsafe": "^1.0.1", - "path-expression-matcher": "^1.5.0", + "is-unsafe": "^2.0.0", + "path-expression-matcher": "^1.6.2", "strnum": "^2.4.1", - "xml-naming": "^0.1.0" + "xml-naming": "^0.3.0" }, "bin": { "fxparser": "src/cli/cli.js" @@ -5616,10 +6559,20 @@ "rollup": "^4.34.8" } }, + "node_modules/fix-dts-default-cjs-exports/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", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, "node_modules/flatted": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", - "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz", + "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==", "dev": true, "license": "ISC" }, @@ -5692,9 +6645,9 @@ } }, "node_modules/geckodriver": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/geckodriver/-/geckodriver-6.1.0.tgz", - "integrity": "sha512-ZRXLa4ZaYTTgUO4Eefw+RsQCleugU2QLb1ME7qTYxxuRj51yAhfnXaItXNs5/vUzfIaDHuZ+YnSF005hfp07nQ==", + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/geckodriver/-/geckodriver-6.1.1.tgz", + "integrity": "sha512-/AcCyc9o9o6hUbudaSJM2iOtXbxSLqQPOb4GrPvEN40cjraUeaX/j5kH3mSgwiroyMn7qzx2wM61AQ2XY8j3sA==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -5704,7 +6657,7 @@ "decamelize": "^6.0.1", "http-proxy-agent": "^7.0.2", "https-proxy-agent": "^7.0.6", - "modern-tar": "^0.7.2" + "modern-tar": "^0.7.3" }, "bin": { "geckodriver": "bin/geckodriver.js" @@ -5733,16 +6686,6 @@ "node": "6.* || 8.* || >= 10.*" } }, - "node_modules/get-func-name": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", - "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "*" - } - }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -5875,6 +6818,13 @@ "dev": true, "license": "MIT" }, + "node_modules/hachure-fill": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/hachure-fill/-/hachure-fill-0.5.2.tgz", + "integrity": "sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==", + "dev": true, + "license": "MIT" + }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -6003,6 +6953,39 @@ "dev": true, "license": "MIT" }, + "node_modules/htmlparser2": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz", + "integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==", + "dev": true, + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "entities": "^7.0.1" + } + }, + "node_modules/htmlparser2/node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/http-proxy-agent": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", @@ -6031,16 +7014,6 @@ "node": ">= 14" } }, - "node_modules/human-signals": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", - "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=16.17.0" - } - }, "node_modules/iconv-lite": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", @@ -6100,6 +7073,16 @@ "dev": true, "license": "ISC" }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/inversify": { "version": "7.11.0", "resolved": "https://registry.npmjs.org/inversify/-/inversify-7.11.0.tgz", @@ -6113,9 +7096,9 @@ } }, "node_modules/ip-address": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", - "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.5.0.tgz", + "integrity": "sha512-R5SnVLJmgYYvf2F2ZgwSBnelz5G4q5AxIC277GDfUaNbrZKNANcBC7RHqYYePlszf4kBolVkJauG0ZjHHFh55g==", "dev": true, "license": "MIT", "engines": { @@ -6328,9 +7311,9 @@ } }, "node_modules/is-unsafe": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-unsafe/-/is-unsafe-1.0.1.tgz", - "integrity": "sha512-CLK2+VdgERgD96EYm5lUQssZYlRg2tkZnbsxZoacmSiRxiFJ4Nk4SzjCl+Ur+v3kXIY9dTIdb3IH22y1mZ56LA==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-unsafe/-/is-unsafe-2.0.0.tgz", + "integrity": "sha512-2LdV822R+wmI86unXA93WCFpL6g+av8ynWk0nrHyJqGop5VoocYsSLFgN8jrfalT6iGeLNM4KXuVSsULP53kEA==", "dev": true, "funding": [ { @@ -6384,13 +7367,13 @@ } }, "node_modules/javascript-obfuscator": { - "version": "5.4.3", - "resolved": "https://registry.npmjs.org/javascript-obfuscator/-/javascript-obfuscator-5.4.3.tgz", - "integrity": "sha512-G5WUgh84tJa5jtk49w+nQCxQpXIvp65xhLyaFeTAdQeYqWji+SXo89k96UvYIDqojyWZEWSDKRmaWKK6bBn/2A==", + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/javascript-obfuscator/-/javascript-obfuscator-5.5.0.tgz", + "integrity": "sha512-YBkZLadMb0ynLQKKMj8urk+NPsglWa4Upwm/kd6x65gyY6YkNkCJi40OOdGDQa1bVGixgRAmVINwODkLnmiJNw==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "@javascript-obfuscator/escodegen": "2.4.1", + "@javascript-obfuscator/escodegen": "2.4.2", "@javascript-obfuscator/estraverse": "5.4.0", "@vercel/blob": ">=0.23.0", "acorn": "8.15.0", @@ -6494,6 +7477,16 @@ "jiti": "lib/jiti-cli.mjs" } }, + "node_modules/jose": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/jose/-/jose-5.10.0.tgz", + "integrity": "sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/joycon": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", @@ -6514,13 +7507,6 @@ "node": ">= 0.8" } }, - "node_modules/js-tokens": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", - "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", - "dev": true, - "license": "MIT" - }, "node_modules/jsbi": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/jsbi/-/jsbi-4.3.2.tgz", @@ -6574,354 +7560,121 @@ "safe-buffer": "~5.1.0" } }, - "node_modules/lazystream": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", - "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", - "dev": true, - "license": "MIT", - "dependencies": { - "readable-stream": "^2.0.5" - }, - "engines": { - "node": ">= 0.6.3" - } - }, - "node_modules/lazystream/node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "dev": true, - "license": "MIT", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/lazystream/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true, - "license": "MIT" - }, - "node_modules/lazystream/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, - "node_modules/levn": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", - "integrity": "sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/libphonenumber-js": { - "version": "1.13.6", - "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.13.6.tgz", - "integrity": "sha512-NdB6O6QvlGMCoG003m0YIKG2+Xw7DjmCZhmc1RH+K6HncADUbRf8TZeLegxBBN1VFyPHcNpPTKpIhYLXzJVy1Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/lie": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", - "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "immediate": "~3.0.5" - } - }, - "node_modules/lightningcss": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", - "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", - "dev": true, - "license": "MPL-2.0", - "dependencies": { - "detect-libc": "^2.0.3" - }, - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - }, - "optionalDependencies": { - "lightningcss-android-arm64": "1.32.0", - "lightningcss-darwin-arm64": "1.32.0", - "lightningcss-darwin-x64": "1.32.0", - "lightningcss-freebsd-x64": "1.32.0", - "lightningcss-linux-arm-gnueabihf": "1.32.0", - "lightningcss-linux-arm64-gnu": "1.32.0", - "lightningcss-linux-arm64-musl": "1.32.0", - "lightningcss-linux-x64-gnu": "1.32.0", - "lightningcss-linux-x64-musl": "1.32.0", - "lightningcss-win32-arm64-msvc": "1.32.0", - "lightningcss-win32-x64-msvc": "1.32.0" - } - }, - "node_modules/lightningcss-android-arm64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", - "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-darwin-arm64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", - "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-darwin-x64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", - "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-freebsd-x64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", - "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", - "cpu": [ - "x64" - ], - "dev": true, - "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.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", - "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", - "cpu": [ - "arm" - ], + "node_modules/katex": { + "version": "0.16.47", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.47.tgz", + "integrity": "sha512-Eeo8Ys1doU1z+x8AZsPpQu+p/QcZBI5PeOo7QGQdy2x2m0MU/hYagBbGOmXwr5KVbEfVuWv9LpnQWeehogurjg==", "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" + "funding": [ + "https://opencollective.com/katex", + "https://github.com/sponsors/katex" ], - "engines": { - "node": ">= 12.0.0" + "license": "MIT", + "dependencies": { + "commander": "^8.3.0" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "bin": { + "katex": "cli.js" } }, - "node_modules/lightningcss-linux-arm64-gnu": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", - "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", - "cpu": [ - "arm64" - ], + "node_modules/katex/node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", "dev": true, - "libc": [ - "glibc" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], + "license": "MIT", "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "node": ">= 12" } }, - "node_modules/lightningcss-linux-arm64-musl": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", - "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", - "cpu": [ - "arm64" - ], + "node_modules/khroma": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/khroma/-/khroma-2.1.0.tgz", + "integrity": "sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==", + "dev": true + }, + "node_modules/layout-base": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-1.0.2.tgz", + "integrity": "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==", "dev": true, - "libc": [ - "musl" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" + "license": "MIT" + }, + "node_modules/lazystream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", + "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "readable-stream": "^2.0.5" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "engines": { + "node": ">= 0.6.3" } }, - "node_modules/lightningcss-linux-x64-gnu": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", - "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", - "cpu": [ - "x64" - ], + "node_modules/lazystream/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", "dev": true, - "libc": [ - "glibc" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" } }, - "node_modules/lightningcss-linux-x64-musl": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", - "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", - "cpu": [ - "x64" - ], + "node_modules/lazystream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "dev": true, - "libc": [ - "musl" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "license": "MIT" + }, + "node_modules/lazystream/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" } }, - "node_modules/lightningcss-win32-arm64-msvc": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", - "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", - "cpu": [ - "arm64" - ], + "node_modules/levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==", "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 12.0.0" + "license": "MIT", + "dependencies": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "engines": { + "node": ">= 0.8.0" } }, - "node_modules/lightningcss-win32-x64-msvc": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", - "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", - "cpu": [ - "x64" - ], + "node_modules/libphonenumber-js": { + "version": "1.13.10", + "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.13.10.tgz", + "integrity": "sha512-xJxrdqvbl2rtn2MaUJrUejz8J7/uZNC0V77oks2LxYrO/+ZtVpRmz+fEQMuu6VusnEB1fmpByiLS1WXecOnAnw==", "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "license": "MIT" + }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "immediate": "~3.0.5" } }, "node_modules/lilconfig": { @@ -6945,9 +7698,9 @@ "license": "MIT" }, "node_modules/linkify-it": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.1.tgz", - "integrity": "sha512-wVoTjP4Q6R0NW5hiZkVJaFZPWgtXfoGF+6LucL3/FtiNjmcHhYjEr5f1Kqjirc1nBW07J/ZuRFumqr2oqccEWg==", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.2.tgz", + "integrity": "sha512-ONTm2jCMAVZjgQa/Fy1kScXsuOoF5NPTsoFBdE1KVIZ2vAh/r9+Bqo+0jINCBYnavTPQZz38QzFTme79ENoN3Q==", "dev": true, "funding": [ { @@ -6974,23 +7727,6 @@ "node": "^12.20.0 || ^14.13.1 || >=16.0.0" } }, - "node_modules/local-pkg": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.5.1.tgz", - "integrity": "sha512-9rrA30MRRP3gBD3HTGnC6cDFpaE1kVDWxWgqWJUN0RvDNAo+Nz/9GxB+nHOH0ifbVFy0hSA1V6vFDvnx54lTEQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "mlly": "^1.7.3", - "pkg-types": "^1.2.1" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, "node_modules/locate-app": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/locate-app/-/locate-app-2.5.0.tgz", @@ -7013,6 +7749,19 @@ "userhome": "1.0.1" } }, + "node_modules/locate-app/node_modules/type-fest": { + "version": "4.26.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.26.0.tgz", + "integrity": "sha512-OduNjVJsFbifKb57UqZ2EMP1i4u64Xwow3NYXUtBbD4vIwJdQd4+xl8YDou1dlm4DVrtwT/7Ky8z8WyCULVfxw==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/lodash": { "version": "4.18.1", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", @@ -7020,6 +7769,13 @@ "dev": true, "license": "MIT" }, + "node_modules/lodash-es": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz", + "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==", + "dev": true, + "license": "MIT" + }, "node_modules/lodash.clonedeep": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", @@ -7055,16 +7811,6 @@ "dev": true, "license": "MIT" }, - "node_modules/loupe": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", - "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", - "dev": true, - "license": "MIT", - "dependencies": { - "get-func-name": "^2.0.1" - } - }, "node_modules/lru-cache": { "version": "10.4.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", @@ -7080,10 +7826,9 @@ "license": "MIT" }, "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, + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.1.1.tgz", + "integrity": "sha512-qFemKPzc3ttrYVaMmnSkGtGc5nE6Ncl4bj7c9IE6C9OUIRXjf6PzJ+UZ1xhVIjYc7dolHq3qKzpAJPZUbvNj+A==", "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" @@ -7097,9 +7842,9 @@ "license": "MIT" }, "node_modules/markdown-it": { - "version": "14.2.0", - "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.2.0.tgz", - "integrity": "sha512-1TGiQiJVRQ3NPmZH6sx5Cfnmg6GQm9jvC1ch4TK511NjSJvjzKLzn5pPfZRNZkRPZP0HqCioSndqH8v2nRaWVQ==", + "version": "14.3.0", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.3.0.tgz", + "integrity": "sha512-RCEsPjR+sr0x+AuYp601tKTkgFG4YEPLCzHST3cQ/fhlJkqAkz1L2/Qbp1j9qw5SBwQHFBoW8+hoN5xssOF0Tw==", "dev": true, "funding": [ { @@ -7114,8 +7859,8 @@ "license": "MIT", "dependencies": { "argparse": "^2.0.1", - "entities": "^4.4.0", - "linkify-it": "^5.0.1", + "entities": "^4.5.0", + "linkify-it": "^5.0.2", "mdurl": "^2.0.0", "punycode.js": "^2.3.1", "uc.micro": "^2.1.0" @@ -7135,17 +7880,17 @@ "mathxyjax3": "^0.8.3" } }, - "node_modules/markdown-it/node_modules/entities": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", - "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "node_modules/marked": { + "version": "16.4.2", + "resolved": "https://registry.npmjs.org/marked/-/marked-16.4.2.tgz", + "integrity": "sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA==", "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.12" + "license": "MIT", + "bin": { + "marked": "bin/marked.js" }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" + "engines": { + "node": ">= 20" } }, "node_modules/math-intrinsics": { @@ -7207,9 +7952,9 @@ } }, "node_modules/mdurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", - "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.1.0.tgz", + "integrity": "sha512-1+HBaOx0zi/dQWht8rNv9MYf9qqpqL/kxI0hXImU6Y547zM6Sni8BQibt7ifgMcYtQg41ao3Ivd6cnSM86inpg==", "dev": true, "license": "MIT" }, @@ -7220,6 +7965,36 @@ "dev": true, "license": "MIT" }, + "node_modules/mermaid": { + "version": "11.16.1", + "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-11.16.1.tgz", + "integrity": "sha512-TQsq6u22fAn3rek5VOubrhKPo1g5hwC3FXUN9hiyupTckcYiGuuKGkNQrKYwGJkXUxZdojwRG46gsSCFZMDp4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@braintree/sanitize-url": "^7.1.2", + "@iconify/utils": "^3.0.2", + "@mermaid-js/parser": "^1.2.0", + "@types/d3": "^7.4.3", + "@upsetjs/venn.js": "^2.0.0", + "cytoscape": "^3.33.3", + "cytoscape-cose-bilkent": "^4.1.0", + "cytoscape-fcose": "^2.2.0", + "d3": "^7.9.0", + "d3-sankey": "^0.12.3", + "dagre-d3-es": "7.0.14", + "dayjs": "^1.11.20", + "dompurify": "^3.3.3", + "es-toolkit": "^1.45.1", + "katex": "^0.16.45", + "khroma": "^2.1.0", + "marked": "^16.3.0", + "roughjs": "^4.6.6", + "stylis": "^4.3.6", + "ts-dedent": "^2.2.0", + "uuid": "^11.1.0 || ^12 || ^13 || ^14.0.0" + } + }, "node_modules/micromark-util-character": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", @@ -7314,19 +8089,6 @@ ], "license": "MIT" }, - "node_modules/mimic-fn": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", - "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/minimatch": { "version": "9.0.9", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", @@ -7381,9 +8143,9 @@ } }, "node_modules/modern-tar": { - "version": "0.7.6", - "resolved": "https://registry.npmjs.org/modern-tar/-/modern-tar-0.7.6.tgz", - "integrity": "sha512-sweCIVXzx1aIGTCdzcMlSZt1h8k5Tmk08VNAuRk3IU28XamGiOH5ypi11g6De2CH7PhYqSSnGy2A/EFhbWnVKg==", + "version": "0.7.7", + "resolved": "https://registry.npmjs.org/modern-tar/-/modern-tar-0.7.7.tgz", + "integrity": "sha512-t9VmxaqrmANnEOBhpSDI6HD192Ge48k8vmWqQQL7hSFEqHEYwZbbsu49+aKLWZeRvFs3j1pMhXOqqF4kPlvjkQ==", "dev": true, "license": "MIT", "engines": { @@ -7428,9 +8190,9 @@ } }, "node_modules/multimatch/node_modules/brace-expansion": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", - "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", "dev": true, "license": "MIT", "dependencies": { @@ -7464,9 +8226,9 @@ } }, "node_modules/nanoid": { - "version": "3.3.12", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", - "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", "dev": true, "funding": [ { @@ -7492,6 +8254,14 @@ "node": ">= 0.4.0" } }, + "node_modules/non-layered-tidy-tree-layout": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/non-layered-tidy-tree-layout/-/non-layered-tidy-tree-layout-2.0.2.tgz", + "integrity": "sha512-gkXMxRzUH+PB0ax9dUN0yYF0S25BqeAYqhgMaLUFmpXLEk7Fcu8f4emJuOAY0V8kjDICxROIKsTAKsV/v355xw==", + "dev": true, + "license": "MIT", + "optional": true + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -7502,35 +8272,6 @@ "node": ">=0.10.0" } }, - "node_modules/npm-run-path": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", - "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^4.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/npm-run-path/node_modules/path-key": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", - "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/nth-check": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", @@ -7603,9 +8344,9 @@ } }, "node_modules/obug": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.3.tgz", - "integrity": "sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.4.tgz", + "integrity": "sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==", "dev": true, "funding": [ "https://github.com/sponsors/sxzz", @@ -7620,26 +8361,10 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/onetime": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", - "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "mimic-fn": "^4.0.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" } }, "node_modules/oniguruma-to-es": { @@ -7672,20 +8397,14 @@ "node": ">= 0.8.0" } }, - "node_modules/p-limit": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-5.0.0.tgz", - "integrity": "sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==", + "node_modules/os-paths": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/os-paths/-/os-paths-4.4.0.tgz", + "integrity": "sha512-wrAwOeXp1RRMFfQY8Sy7VaGVmPocaLwSFOYCGKSyo8qmJ+/yaafCl5BCA1IQZWqFSRBrKDYFeR9d/VyQzfH/jg==", "dev": true, "license": "MIT", - "dependencies": { - "yocto-queue": "^1.0.0" - }, "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">= 6.0" } }, "node_modules/pac-proxy-agent": { @@ -7729,6 +8448,13 @@ "dev": true, "license": "BlueOak-1.0.0" }, + "node_modules/package-manager-detector": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.8.0.tgz", + "integrity": "sha512-yQA4H19AmPEoMUeavPMDIe1higySl/gH/yaQrkT/s07Qp+7pp2hYz30N3z2l5BkjVkF9Ow6o0wjJamm2y7Sn0A==", + "dev": true, + "license": "MIT" + }, "node_modules/pako": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", @@ -7736,6 +8462,33 @@ "dev": true, "license": "(MIT AND Zlib)" }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-htmlparser2-tree-adapter": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz", + "integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "domhandler": "^5.0.3", + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, "node_modules/parse5-parser-stream": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz", @@ -7749,7 +8502,7 @@ "url": "https://github.com/inikulin/parse5?sponsor=1" } }, - "node_modules/parse5-parser-stream/node_modules/entities": { + "node_modules/parse5/node_modules/entities": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", @@ -7762,23 +8515,17 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, - "node_modules/parse5-parser-stream/node_modules/parse5": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", - "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "node_modules/path-data-parser": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/path-data-parser/-/path-data-parser-0.1.0.tgz", + "integrity": "sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w==", "dev": true, - "license": "MIT", - "dependencies": { - "entities": "^6.0.0" - }, - "funding": { - "url": "https://github.com/inikulin/parse5?sponsor=1" - } + "license": "MIT" }, "node_modules/path-expression-matcher": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.6.0.tgz", - "integrity": "sha512-e5y7RCLHKjemsgQ4eqGJtPyr10ILz25HO7flzxhTV8bgvd5yHx98DGtCAtbVW9f2TqnYI/gEVZd+vz7snrdPTw==", + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.6.2.tgz", + "integrity": "sha512-enSlaiat05iasnzmgNxRj8reFdj3puY2QpNgP1aPIaVfT6nn9ICuPoFlKHk8EN22HcwewshO+mN2DGbkCEOtqQ==", "dev": true, "funding": [ { @@ -7832,16 +8579,6 @@ "dev": true, "license": "MIT" }, - "node_modules/pathval": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", - "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "*" - } - }, "node_modules/pend": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", @@ -7864,9 +8601,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", "dev": true, "license": "MIT", "engines": { @@ -7899,35 +8636,35 @@ } }, "node_modules/playwright": { - "version": "1.61.1", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.61.1.tgz", - "integrity": "sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ==", + "version": "1.62.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.62.1.tgz", + "integrity": "sha512-0M+L3LAD8/nm554LOla9Ayx0j0tmFZ0FBcoQ7F1VuVHpM/XpiC8RcDzBQB8W5+hA8L22THxELzeF+2WcUzvcLg==", "dev": true, "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.61.1" + "playwright-core": "1.62.1" }, "bin": { "playwright": "cli.js" }, "engines": { - "node": ">=18" + "node": ">=20" }, "optionalDependencies": { "fsevents": "2.3.2" } }, "node_modules/playwright-core": { - "version": "1.61.1", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.61.1.tgz", - "integrity": "sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==", + "version": "1.62.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.62.1.tgz", + "integrity": "sha512-wPYSwEBJY9GHraISXqyqtx0na0LpO3XEX7jNDhntbex7tzUS7kLnZsOlFruFJB4Hi/rhDMjXGqHewDZ68nYZVw==", "dev": true, "license": "Apache-2.0", "bin": { "playwright-core": "cli.js" }, "engines": { - "node": ">=18" + "node": ">=20" } }, "node_modules/pngjs": { @@ -7940,6 +8677,24 @@ "node": ">=14.19.0" } }, + "node_modules/points-on-curve": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/points-on-curve/-/points-on-curve-0.2.0.tgz", + "integrity": "sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A==", + "dev": true, + "license": "MIT" + }, + "node_modules/points-on-path": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/points-on-path/-/points-on-path-0.2.1.tgz", + "integrity": "sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-data-parser": "0.1.0", + "points-on-curve": "0.2.0" + } + }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", @@ -7951,9 +8706,9 @@ } }, "node_modules/postcss": { - "version": "8.5.19", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.19.tgz", - "integrity": "sha512-Mz8SaolMd8nB+G13WkORcxQKHZ/NE4xXevtkJHVuG+guo9/wYKlIMTKAqGdEmYOXR2ijPjTYNHssizdaVSUNdQ==", + "version": "8.5.26", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", + "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", "dev": true, "funding": [ { @@ -7971,7 +8726,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.12", + "nanoid": "^3.3.17", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -8023,14 +8778,22 @@ } }, "node_modules/preact": { - "version": "10.29.2", - "resolved": "https://registry.npmjs.org/preact/-/preact-10.29.2.tgz", - "integrity": "sha512-7tNmwg/7mzzAoB/8kSg6Hl37JraAZw3Z3A0JSY7VXlZwo82Xn0G7wKbNNs2qoF4ZEEsQGTwDAroNdqKs1ofJxQ==", + "version": "10.29.8", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.29.8.tgz", + "integrity": "sha512-ej2aVZ+vZ8WO7tvlQWRM9N63A0KzF9q4mWJfDUHgYaIofWY9hu74QdnQrjoPMmZi2/nZ5gN0bJCQF49xQqx09Q==", "dev": true, "license": "MIT", "funding": { "type": "opencollective", "url": "https://opencollective.com/preact" + }, + "peerDependencies": { + "preact-render-to-string": ">=5" + }, + "peerDependenciesMeta": { + "preact-render-to-string": { + "optional": true + } } }, "node_modules/prelude-ls": { @@ -8042,34 +8805,6 @@ "node": ">= 0.8.0" } }, - "node_modules/pretty-format": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", - "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/schemas": "^29.6.3", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/pretty-format/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, "node_modules/process": { "version": "0.11.10", "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", @@ -8108,36 +8843,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/proxy-agent": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz", - "integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==", - "dev": true, - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.2", - "debug": "^4.3.4", - "http-proxy-agent": "^7.0.1", - "https-proxy-agent": "^7.0.6", - "lru-cache": "^7.14.1", - "pac-proxy-agent": "^7.1.0", - "proxy-from-env": "^1.1.0", - "socks-proxy-agent": "^8.0.5" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/proxy-agent/node_modules/lru-cache": { - "version": "7.18.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", - "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=12" - } - }, "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", @@ -8173,13 +8878,6 @@ "dev": true, "license": "MIT" }, - "node_modules/react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "dev": true, - "license": "MIT" - }, "node_modules/readable-stream": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", @@ -8197,6 +8895,31 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/readable-stream/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, "node_modules/readdir-glob": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz", @@ -8354,44 +9077,17 @@ "dev": true, "license": "MIT" }, - "node_modules/rolldown": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.3.tgz", - "integrity": "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==", + "node_modules/robust-predicates": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.3.tgz", + "integrity": "sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==", "dev": true, - "license": "MIT", - "dependencies": { - "@oxc-project/types": "=0.133.0", - "@rolldown/pluginutils": "^1.0.0" - }, - "bin": { - "rolldown": "bin/cli.mjs" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "optionalDependencies": { - "@rolldown/binding-android-arm64": "1.0.3", - "@rolldown/binding-darwin-arm64": "1.0.3", - "@rolldown/binding-darwin-x64": "1.0.3", - "@rolldown/binding-freebsd-x64": "1.0.3", - "@rolldown/binding-linux-arm-gnueabihf": "1.0.3", - "@rolldown/binding-linux-arm64-gnu": "1.0.3", - "@rolldown/binding-linux-arm64-musl": "1.0.3", - "@rolldown/binding-linux-ppc64-gnu": "1.0.3", - "@rolldown/binding-linux-s390x-gnu": "1.0.3", - "@rolldown/binding-linux-x64-gnu": "1.0.3", - "@rolldown/binding-linux-x64-musl": "1.0.3", - "@rolldown/binding-openharmony-arm64": "1.0.3", - "@rolldown/binding-wasm32-wasi": "1.0.3", - "@rolldown/binding-win32-arm64-msvc": "1.0.3", - "@rolldown/binding-win32-x64-msvc": "1.0.3" - } + "license": "Unlicense" }, "node_modules/rollup": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", - "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.4.tgz", + "integrity": "sha512-RXOqwaPsBGjMNMa4sQjDjHieHEZDFoj/Rdr46l2MU5DfEs16wHJPC2RPTPHWhNl+M3aI472LLqFkFKut4SblOg==", "dev": true, "license": "MIT", "dependencies": { @@ -8405,34 +9101,55 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.62.2", - "@rollup/rollup-android-arm64": "4.62.2", - "@rollup/rollup-darwin-arm64": "4.62.2", - "@rollup/rollup-darwin-x64": "4.62.2", - "@rollup/rollup-freebsd-arm64": "4.62.2", - "@rollup/rollup-freebsd-x64": "4.62.2", - "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", - "@rollup/rollup-linux-arm-musleabihf": "4.62.2", - "@rollup/rollup-linux-arm64-gnu": "4.62.2", - "@rollup/rollup-linux-arm64-musl": "4.62.2", - "@rollup/rollup-linux-loong64-gnu": "4.62.2", - "@rollup/rollup-linux-loong64-musl": "4.62.2", - "@rollup/rollup-linux-ppc64-gnu": "4.62.2", - "@rollup/rollup-linux-ppc64-musl": "4.62.2", - "@rollup/rollup-linux-riscv64-gnu": "4.62.2", - "@rollup/rollup-linux-riscv64-musl": "4.62.2", - "@rollup/rollup-linux-s390x-gnu": "4.62.2", - "@rollup/rollup-linux-x64-gnu": "4.62.2", - "@rollup/rollup-linux-x64-musl": "4.62.2", - "@rollup/rollup-openbsd-x64": "4.62.2", - "@rollup/rollup-openharmony-arm64": "4.62.2", - "@rollup/rollup-win32-arm64-msvc": "4.62.2", - "@rollup/rollup-win32-ia32-msvc": "4.62.2", - "@rollup/rollup-win32-x64-gnu": "4.62.2", - "@rollup/rollup-win32-x64-msvc": "4.62.2", + "@napi-rs/lzma-linux-x64-gnu": "1.5.1", + "@rollup/rollup-android-arm-eabi": "4.62.4", + "@rollup/rollup-android-arm64": "4.62.4", + "@rollup/rollup-darwin-arm64": "4.62.4", + "@rollup/rollup-darwin-x64": "4.62.4", + "@rollup/rollup-freebsd-arm64": "4.62.4", + "@rollup/rollup-freebsd-x64": "4.62.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.4", + "@rollup/rollup-linux-arm-musleabihf": "4.62.4", + "@rollup/rollup-linux-arm64-gnu": "4.62.4", + "@rollup/rollup-linux-arm64-musl": "4.62.4", + "@rollup/rollup-linux-loong64-gnu": "4.62.4", + "@rollup/rollup-linux-loong64-musl": "4.62.4", + "@rollup/rollup-linux-ppc64-gnu": "4.62.4", + "@rollup/rollup-linux-ppc64-musl": "4.62.4", + "@rollup/rollup-linux-riscv64-gnu": "4.62.4", + "@rollup/rollup-linux-riscv64-musl": "4.62.4", + "@rollup/rollup-linux-s390x-gnu": "4.62.4", + "@rollup/rollup-linux-x64-gnu": "4.62.4", + "@rollup/rollup-linux-x64-musl": "4.62.4", + "@rollup/rollup-openbsd-x64": "4.62.4", + "@rollup/rollup-openharmony-arm64": "4.62.4", + "@rollup/rollup-win32-arm64-msvc": "4.62.4", + "@rollup/rollup-win32-ia32-msvc": "4.62.4", + "@rollup/rollup-win32-x64-gnu": "4.62.4", + "@rollup/rollup-win32-x64-msvc": "4.62.4", "fsevents": "~2.3.2" } }, + "node_modules/roughjs": { + "version": "4.6.6", + "resolved": "https://registry.npmjs.org/roughjs/-/roughjs-4.6.6.tgz", + "integrity": "sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "hachure-fill": "^0.5.2", + "path-data-parser": "^0.1.0", + "points-on-curve": "^0.2.0", + "points-on-path": "^0.2.1" + } + }, + "node_modules/rw": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", + "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==", + "dev": true, + "license": "BSD-3-Clause" + }, "node_modules/safaridriver": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/safaridriver/-/safaridriver-1.0.1.tgz", @@ -8512,6 +9229,14 @@ "dev": true, "license": "MIT" }, + "node_modules/search-insights": { + "version": "2.17.3", + "resolved": "https://registry.npmjs.org/search-insights/-/search-insights-2.17.3.tgz", + "integrity": "sha512-RQPdCYTa8A68uM2jwxoY842xDhvx3E5LFL1LxvxCNMev4o5mLuokczhzjAgGwUZBAmOKZknArSxLKmXtIi2AxQ==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/semver": { "version": "7.8.5", "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", @@ -8541,23 +9266,10 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/serialize-error/node_modules/type-fest": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", - "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/serialize-javascript": { - "version": "7.0.7", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-7.0.7.tgz", - "integrity": "sha512-YAy8Od6KV+uuwUuU50np8fGB/Aues6Y0nAhA9y/hId74PlKUcme4pXcBD46NWKr1Q4osN/iseZ17YqO1XfmI8g==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-7.1.0.tgz", + "integrity": "sha512-RNEqWOyhhUQYN9V1GfHwu9AR/g+NTciH6Z5u3/no6X3/w+04J2lVDL+svFQVXgXrEGBMG2puMVN3gq2SNGuTGw==", "dev": true, "license": "BSD-3-Clause", "engines": { @@ -8758,13 +9470,13 @@ } }, "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", "dev": true, "license": "BSD-3-Clause", "engines": { - "node": ">=0.10.0" + "node": ">= 12" } }, "node_modules/source-map-js": { @@ -8788,6 +9500,16 @@ "source-map": "^0.6.0" } }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/space-separated-tokens": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", @@ -8844,9 +9566,9 @@ "license": "MIT" }, "node_modules/std-env": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz", - "integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz", + "integrity": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==", "dev": true, "license": "MIT" }, @@ -9008,32 +9730,6 @@ "node": ">=8" } }, - "node_modules/strip-final-newline": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", - "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/strip-literal": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-2.1.1.tgz", - "integrity": "sha512-631UJ6O00eNGfMiWG78ck80dfBab8X6IVFB51jZK5Icd7XAs60Z5y7QdSd/wGIklnWvRbUNloVzhOKKmutxQ6Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "js-tokens": "^9.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, "node_modules/strnum": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.4.1.tgz", @@ -9050,6 +9746,13 @@ "anynum": "^1.0.1" } }, + "node_modules/stylis": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.4.0.tgz", + "integrity": "sha512-5Z9ZpRzfuH6l/UAvCPAPUo3665Nk2wLaZU3x+TLHKVzIz33+sbJqbtrYoC3KD4/uVOr2Zp+L0LySezP9OHV9yA==", + "dev": true, + "license": "MIT" + }, "node_modules/sucrase": { "version": "3.35.1", "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", @@ -9073,16 +9776,6 @@ "node": ">=16 || 14 >=14.17" } }, - "node_modules/sucrase/node_modules/commander": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", - "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, "node_modules/superjson": { "version": "2.2.6", "resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.6.tgz", @@ -9122,27 +9815,12 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/tabbable": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.4.0.tgz", - "integrity": "sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==", - "dev": true, - "license": "MIT" - }, - "node_modules/tar-fs": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.2.tgz", - "integrity": "sha512-QGxxTxxyleAdyM3kpFs14ymbYmNFrfY+pHj7Z8FgtbZ7w2//VAgLMac7sT6nRpIHjppXO2AwwEOg0bPFVRcmXw==", - "dev": true, - "license": "MIT", - "dependencies": { - "pump": "^3.0.0", - "tar-stream": "^3.1.5" - }, - "optionalDependencies": { - "bare-fs": "^4.0.1", - "bare-path": "^3.0.0" - } + "node_modules/tabbable": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.5.0.tgz", + "integrity": "sha512-wieBHXygIm7OyQOu5hQlkk62/WyCFYGlWg7L6/ZCUZwx0o398Zkn4pVmMyfYhfMG8kGrj/Krt8eIk6UKC6VzwA==", + "dev": true, + "license": "MIT" }, "node_modules/tar-stream": { "version": "3.2.0", @@ -9168,9 +9846,9 @@ } }, "node_modules/terser": { - "version": "5.49.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.49.0.tgz", - "integrity": "sha512-SNiDnXyHSrxVcIOtVbULzcTmniUiwcV7Nwdyj1twVubeTmbjoa8p69KKDpfkdoOavuM4/GRm1+ykI8qqnavHoA==", + "version": "5.50.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.50.0.tgz", + "integrity": "sha512-CN9BVxWhgS/hRxtUMjtC2uRWSTcSfQFHMDWma6sKKfIivCD91sM+FOPfvwoaRMqCSrUpe1nv3jDamd9eEQ4y+w==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -9247,14 +9925,11 @@ "license": "MIT" }, "node_modules/tinyexec": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", - "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } + "license": "MIT" }, "node_modules/tinyglobby": { "version": "0.2.17", @@ -9273,30 +9948,10 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, - "node_modules/tinypool": { - "version": "0.8.4", - "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.8.4.tgz", - "integrity": "sha512-i11VH5gS6IFeLY3gMBQ00/MmLncVP7JLXOw1vlgkytLmJK7QnEr7NXf0LBdxfmNPAeyetukOk0bOYrJrFGjYJQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, "node_modules/tinyrainbow": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", - "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/tinyspy": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.2.1.tgz", - "integrity": "sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.1.tgz", + "integrity": "sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==", "dev": true, "license": "MIT", "engines": { @@ -9334,6 +9989,16 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/ts-dedent": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/ts-dedent/-/ts-dedent-2.3.0.tgz", + "integrity": "sha512-JfJeIHke7y2egdGGgRAvpCwYFUsHlM2gPcrVOxFkznt/4uzQ7HFmvE63iFHVLBJNDuyDOQgijDK/tXH/f6Msjg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.10" + } + }, "node_modules/ts-interface-checker": { "version": "0.1.13", "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", @@ -9400,27 +10065,10 @@ } } }, - "node_modules/tsup/node_modules/source-map": { - "version": "0.7.6", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", - "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">= 12" - } - }, - "node_modules/tsup/node_modules/tinyexec": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", - "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", - "dev": true, - "license": "MIT" - }, "node_modules/tsx": { - "version": "4.22.4", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.22.4.tgz", - "integrity": "sha512-X8EX+XV4QR5xCsrgxaED954zTDfY8KqlDtskKEL0cHhyS/P8b4IFOvGDQpsC9Q1XnLq915wEfwwY/zzskCtmhg==", + "version": "4.23.12", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.12.tgz", + "integrity": "sha512-FDf4L4sYzKtzWYhU/Xm0AQFdTjdIxNo9ElTf2mxXM6k8YMHXzYUe4yODVaXP4V9uMFbVg8c0qyBccK2OOxb45Q==", "dev": true, "license": "MIT", "dependencies": { @@ -9464,20 +10112,10 @@ "node": ">= 0.8.0" } }, - "node_modules/type-detect": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz", - "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/type-fest": { - "version": "4.26.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.26.0.tgz", - "integrity": "sha512-OduNjVJsFbifKb57UqZ2EMP1i4u64Xwow3NYXUtBbD4vIwJdQd4+xl8YDou1dlm4DVrtwT/7Ky8z8WyCULVfxw==", + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", "dev": true, "license": "(MIT OR CC0-1.0)", "engines": { @@ -9488,17 +10126,17 @@ } }, "node_modules/typedoc": { - "version": "0.28.19", - "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.28.19.tgz", - "integrity": "sha512-wKh+lhdmMFivMlc6vRRcMGXeGEHGU2g8a2CkPTJjJlwRf1iXbimWIPcFolCqe4E0d/FRtGszpIrsp3WLpDB8Pw==", + "version": "0.28.20", + "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.28.20.tgz", + "integrity": "sha512-uSKqkh8Cr48vllnEy+jdaAgOeR6Y+QCBW7usgUsKj7gJEfR7stw9U/fE49LBnj2tPRKPY0c0EBJSWe9Appmplg==", "dev": true, "license": "Apache-2.0", "dependencies": { "@gerrit0/mini-shiki": "^3.23.0", "lunr": "^2.3.9", - "markdown-it": "^14.1.1", + "markdown-it": "^14.3.0", "minimatch": "^10.2.5", - "yaml": "^2.8.3" + "yaml": "^2.9.0" }, "bin": { "typedoc": "bin/typedoc" @@ -9522,26 +10160,26 @@ } }, "node_modules/typedoc/node_modules/brace-expansion": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", - "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" }, "engines": { - "node": "18 || 20 || >=22" + "node": "20 || >=22" } }, "node_modules/typedoc/node_modules/minimatch": { - "version": "10.2.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", - "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^5.0.5" + "brace-expansion": "^5.0.8" }, "engines": { "node": "18 || 20 || >=22" @@ -9615,13 +10253,13 @@ "license": "MIT" }, "node_modules/undici": { - "version": "6.27.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-6.27.0.tgz", - "integrity": "sha512-YmfV3YnEDzXRC5lZ2jWtWWHKGUm1zIt8AhesR1tens+HTNv+YZlN/dp6G727LOvMJ8xjP9Be7Y2Sdr96LDm+pg==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.29.0.tgz", + "integrity": "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==", "dev": true, "license": "MIT", "engines": { - "node": ">=18.17" + "node": ">=20.18.1" } }, "node_modules/undici-types": { @@ -9676,439 +10314,176 @@ "node_modules/unist-util-visit": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz", - "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0", - "unist-util-is": "^6.0.0", - "unist-util-visit-parents": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-visit-parents": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", - "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0", - "unist-util-is": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unplugin": { - "version": "2.3.11", - "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-2.3.11.tgz", - "integrity": "sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/remapping": "^2.3.5", - "acorn": "^8.15.0", - "picomatch": "^4.0.3", - "webpack-virtual-modules": "^0.6.2" - }, - "engines": { - "node": ">=18.12.0" - } - }, - "node_modules/unplugin-swc": { - "version": "1.5.9", - "resolved": "https://registry.npmjs.org/unplugin-swc/-/unplugin-swc-1.5.9.tgz", - "integrity": "sha512-RKwK3yf0M+MN17xZfF14bdKqfx0zMXYdtOdxLiE6jHAoidupKq3jGdJYANyIM1X/VmABhh1WpdO+/f4+Ol89+g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@rollup/pluginutils": "^5.3.0", - "load-tsconfig": "^0.2.5", - "unplugin": "^2.3.11" - }, - "peerDependencies": { - "@swc/core": "^1.2.108" - } - }, - "node_modules/urlpattern-polyfill": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/urlpattern-polyfill/-/urlpattern-polyfill-10.1.0.tgz", - "integrity": "sha512-IGjKp/o0NL3Bso1PymYURCJxMPNAf/ILOpendP9f5B6e1rTJgdgiOvgfoT8VxCAdY+Wisb9uhGaJJf3yZ2V9nw==", - "dev": true, - "license": "MIT" - }, - "node_modules/userhome": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/userhome/-/userhome-1.0.1.tgz", - "integrity": "sha512-5cnLm4gseXjAclKowC4IjByaGsjtAoV6PrOQOljplNB54ReUYJP8HdAFq2muHinSDAh09PPX/uXDPfdxRHvuSA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/util": { - "version": "0.12.5", - "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", - "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", - "dev": true, - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "is-arguments": "^1.0.4", - "is-generator-function": "^1.0.7", - "is-typed-array": "^1.1.3", - "which-typed-array": "^1.1.2" - } - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true, - "license": "MIT" - }, - "node_modules/validator": { - "version": "13.15.35", - "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.35.tgz", - "integrity": "sha512-TQ5pAGhd5whStmqWvYF4OjQROlmv9SMFVt37qoCBdqRffuuklWYQlCNnEs2ZaIBD1kZRNnikiZOS1eqgkar0iw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/vfile": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", - "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0", - "vfile-message": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/vfile-message": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", - "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0", - "unist-util-stringify-position": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/vite": { - "version": "8.0.16", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.16.tgz", - "integrity": "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==", - "dev": true, - "license": "MIT", - "dependencies": { - "lightningcss": "^1.32.0", - "picomatch": "^4.0.4", - "postcss": "^8.5.15", - "rolldown": "1.0.3", - "tinyglobby": "^0.2.17" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^20.19.0 || >=22.12.0", - "@vitejs/devtools": "^0.1.18", - "esbuild": "^0.27.0 || ^0.28.0", - "jiti": ">=1.21.0", - "less": "^4.0.0", - "sass": "^1.70.0", - "sass-embedded": "^1.70.0", - "stylus": ">=0.54.8", - "sugarss": "^5.0.0", - "terser": "^5.16.0", - "tsx": "^4.8.1", - "yaml": "^2.4.2" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "@vitejs/devtools": { - "optional": true - }, - "esbuild": { - "optional": true - }, - "jiti": { - "optional": true - }, - "less": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true - } + "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/vite-node": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.6.1.tgz", - "integrity": "sha512-YAXkfvGtuTzwWbDSACdJSg4A4DZiAqckWe90Zapc/sEX3XvHcw1NdurM/6od8J207tSDqNbSsgdCacBgvJKFuA==", + "node_modules/unist-util-visit-parents": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", + "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", "dev": true, "license": "MIT", "dependencies": { - "cac": "^6.7.14", - "debug": "^4.3.4", - "pathe": "^1.1.1", - "picocolors": "^1.0.0", - "vite": "^5.0.0" - }, - "bin": { - "vite-node": "vite-node.mjs" - }, - "engines": { - "node": "^18.0.0 || >=20.0.0" + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" }, "funding": { - "url": "https://opencollective.com/vitest" + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/vite-node/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/unplugin": { + "version": "2.3.11", + "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-2.3.11.tgz", + "integrity": "sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww==", "dev": true, - "hasInstallScript": true, "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "acorn": "^8.15.0", + "picomatch": "^4.0.3", + "webpack-virtual-modules": "^0.6.2" + }, "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + "node": ">=18.12.0" } }, - "node_modules/vite-node/node_modules/pathe": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", - "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/vite-node/node_modules/vite": { - "version": "5.4.21", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", - "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "node_modules/unplugin-swc": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/unplugin-swc/-/unplugin-swc-1.5.10.tgz", + "integrity": "sha512-aNvKH601KYzc/fXyDQJPAk8obNPzA4GdGqx+qSjQoyweMLDMLs5VRbunjL4skIv/hyyAnSgrbxMwXHdRTzhmew==", "dev": true, "license": "MIT", "dependencies": { - "esbuild": "^0.21.3", - "postcss": "^8.4.43", - "rollup": "^4.20.0" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^18.0.0 || >=20.0.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" + "@rollup/pluginutils": "^5.3.0", + "load-tsconfig": "^0.2.5", + "unplugin": "^2.3.11" }, "peerDependencies": { - "@types/node": "^18.0.0 || >=20.0.0", - "less": "*", - "lightningcss": "^1.21.0", - "sass": "*", - "sass-embedded": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.4.0" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - } + "@swc/core": "^1.2.108" } }, - "node_modules/vite/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/urlpattern-polyfill": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/urlpattern-polyfill/-/urlpattern-polyfill-10.1.0.tgz", + "integrity": "sha512-IGjKp/o0NL3Bso1PymYURCJxMPNAf/ILOpendP9f5B6e1rTJgdgiOvgfoT8VxCAdY+Wisb9uhGaJJf3yZ2V9nw==", + "dev": true, + "license": "MIT" + }, + "node_modules/userhome": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/userhome/-/userhome-1.0.1.tgz", + "integrity": "sha512-5cnLm4gseXjAclKowC4IjByaGsjtAoV6PrOQOljplNB54ReUYJP8HdAFq2muHinSDAh09PPX/uXDPfdxRHvuSA==", "dev": true, - "hasInstallScript": true, "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + "node": ">= 0.8.0" } }, - "node_modules/vitepress": { - "version": "1.6.4", - "resolved": "https://registry.npmjs.org/vitepress/-/vitepress-1.6.4.tgz", - "integrity": "sha512-+2ym1/+0VVrbhNyRoFFesVvBvHAVMZMK0rw60E3X/5349M1GuVdKeazuksqopEdvkKwKGs21Q729jX81/bkBJg==", + "node_modules/util": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", + "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", "dev": true, "license": "MIT", "dependencies": { - "@docsearch/css": "3.8.2", - "@docsearch/js": "3.8.2", - "@iconify-json/simple-icons": "^1.2.21", - "@shikijs/core": "^2.1.0", - "@shikijs/transformers": "^2.1.0", - "@shikijs/types": "^2.1.0", - "@types/markdown-it": "^14.1.2", - "@vitejs/plugin-vue": "^5.2.1", - "@vue/devtools-api": "^7.7.0", - "@vue/shared": "^3.5.13", - "@vueuse/core": "^12.4.0", - "@vueuse/integrations": "^12.4.0", - "focus-trap": "^7.6.4", - "mark.js": "8.11.1", - "minisearch": "^7.1.1", - "shiki": "^2.1.0", - "vite": "^5.4.14", - "vue": "^3.5.13" - }, - "bin": { - "vitepress": "bin/vitepress.js" - }, - "peerDependencies": { - "markdown-it-mathjax3": "^4", - "postcss": "^8" - }, - "peerDependenciesMeta": { - "markdown-it-mathjax3": { - "optional": true - }, - "postcss": { - "optional": true - } + "inherits": "^2.0.3", + "is-arguments": "^1.0.4", + "is-generator-function": "^1.0.7", + "is-typed-array": "^1.1.3", + "which-typed-array": "^1.1.2" } }, - "node_modules/vitepress/node_modules/@shikijs/types": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-2.5.0.tgz", - "integrity": "sha512-ygl5yhxki9ZLNuNpPitBWvcy9fsSKKaRuO4BAlMyagszQidxcpLAr0qiW/q43DtSIDxO6hEbtYLiFZNXO/hdGw==", + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/uuid": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-14.0.1.tgz", + "integrity": "sha512-6ZxzVpzDXDa3bJWaHilVayA+BH/1zmxCJoVgvmqJnid/gPoKHxUrS/aC/T6LGQtNHT+XHG9fXPJB4d+IrU30Ew==", "dev": true, + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], "license": "MIT", - "dependencies": { - "@shikijs/vscode-textmate": "^10.0.2", - "@types/hast": "^3.0.4" + "bin": { + "uuid": "dist-node/bin/uuid" } }, - "node_modules/vitepress/node_modules/@vitejs/plugin-vue": { - "version": "5.2.4", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz", - "integrity": "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==", + "node_modules/validator": { + "version": "13.15.35", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.35.tgz", + "integrity": "sha512-TQ5pAGhd5whStmqWvYF4OjQROlmv9SMFVt37qoCBdqRffuuklWYQlCNnEs2ZaIBD1kZRNnikiZOS1eqgkar0iw==", "dev": true, "license": "MIT", "engines": { - "node": "^18.0.0 || >=20.0.0" + "node": ">= 0.10" + } + }, + "node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" }, - "peerDependencies": { - "vite": "^5.0.0 || ^6.0.0", - "vue": "^3.2.25" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/vitepress/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/vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", "dev": true, - "hasInstallScript": true, "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/vitepress/node_modules/vite": { - "version": "5.4.21", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", - "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "node_modules/vite": { + "version": "6.4.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz", + "integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==", "dev": true, "license": "MIT", "dependencies": { - "esbuild": "^0.21.3", - "postcss": "^8.4.43", - "rollup": "^4.20.0" + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" }, "bin": { "vite": "bin/vite.js" }, "engines": { - "node": "^18.0.0 || >=20.0.0" + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" }, "funding": { "url": "https://github.com/vitejs/vite?sponsor=1" @@ -10117,19 +10492,25 @@ "fsevents": "~2.3.3" }, "peerDependencies": { - "@types/node": "^18.0.0 || >=20.0.0", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", - "terser": "^5.4.0" + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" }, "peerDependenciesMeta": { "@types/node": { "optional": true }, + "jiti": { + "optional": true + }, "less": { "optional": true }, @@ -10150,23 +10531,44 @@ }, "terser": { "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true } } }, + "node_modules/vite/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/vitest": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.9.tgz", - "integrity": "sha512-nE3/LEyc0z87uHYLZebqCUOaJr2hdtuPp7BQ4BosVFnfltxgAvMG08NyrSGlPpOUWvR27c5flSmYFTNr78L9GQ==", + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.10.tgz", + "integrity": "sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/expect": "4.1.9", - "@vitest/mocker": "4.1.9", - "@vitest/pretty-format": "4.1.9", - "@vitest/runner": "4.1.9", - "@vitest/snapshot": "4.1.9", - "@vitest/spy": "4.1.9", - "@vitest/utils": "4.1.9", + "@vitest/expect": "4.1.10", + "@vitest/mocker": "4.1.10", + "@vitest/pretty-format": "4.1.10", + "@vitest/runner": "4.1.10", + "@vitest/snapshot": "4.1.10", + "@vitest/spy": "4.1.10", + "@vitest/utils": "4.1.10", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", @@ -10194,12 +10596,12 @@ "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", - "@vitest/browser-playwright": "4.1.9", - "@vitest/browser-preview": "4.1.9", - "@vitest/browser-webdriverio": "4.1.9", - "@vitest/coverage-istanbul": "4.1.9", - "@vitest/coverage-v8": "4.1.9", - "@vitest/ui": "4.1.9", + "@vitest/browser-playwright": "4.1.10", + "@vitest/browser-preview": "4.1.10", + "@vitest/browser-webdriverio": "4.1.10", + "@vitest/coverage-istanbul": "4.1.10", + "@vitest/coverage-v8": "4.1.10", + "@vitest/ui": "4.1.10", "happy-dom": "*", "jsdom": "*", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" @@ -10243,18 +10645,38 @@ } } }, + "node_modules/vitest/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", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/vitest/node_modules/tinyexec": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.0.tgz", + "integrity": "sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/vue": { - "version": "3.5.40", - "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.40.tgz", - "integrity": "sha512-+8PJ4SJXdn/cHGImF4CKdxlWHIN5Dkt7DoufRREM6h6uVCx2m7QxgcEQmmzyOK8A9mcafg7sFbJFYsdFVubTig==", + "version": "3.5.41", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.41.tgz", + "integrity": "sha512-2laE0p+aK+/AOPG/XL/WepOs/GlK755LJ1XECi9kDUrz1FKNw8rb2Xzlw9JS1rqEV55nb0ttsKxVlTCcd+R5cg==", "dev": true, "license": "MIT", "dependencies": { - "@vue/compiler-dom": "3.5.40", - "@vue/compiler-sfc": "3.5.40", - "@vue/runtime-dom": "3.5.40", - "@vue/server-renderer": "3.5.40", - "@vue/shared": "3.5.40" + "@vue/compiler-dom": "3.5.41", + "@vue/compiler-sfc": "3.5.41", + "@vue/runtime-dom": "3.5.41", + "@vue/server-renderer": "3.5.41", + "@vue/shared": "3.5.41" }, "peerDependencies": { "typescript": "*" @@ -10316,24 +10738,34 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/wait-port/node_modules/commander": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", + "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || >=14" + } + }, "node_modules/webdriver": { - "version": "9.29.0", - "resolved": "https://registry.npmjs.org/webdriver/-/webdriver-9.29.0.tgz", - "integrity": "sha512-RzD+aleJeVqG4aGhp4QGQNriaBYnBV0pyC+v1AAZKoxBK+nb0W5iVWTwkTFGGMDdJGWR7s+M8Ozm6lXihpCBww==", + "version": "9.30.1", + "resolved": "https://registry.npmjs.org/webdriver/-/webdriver-9.30.1.tgz", + "integrity": "sha512-qvtOsFiS0v3rrDPp+saWI8WQdOotT7nsPIFaML0T/uRUwr4QPjaFNbY+ZQwfzv07BQdpVGwiUEeLws5piVlEgA==", "dev": true, "license": "MIT", "dependencies": { "@types/node": "^20.1.0", "@types/ws": "^8.5.3", - "@wdio/config": "9.29.0", - "@wdio/logger": "9.18.0", - "@wdio/protocols": "9.29.0", - "@wdio/types": "9.29.0", - "@wdio/utils": "9.29.0", + "@wdio/config": "9.30.1", + "@wdio/logger": "9.29.1", + "@wdio/protocols": "9.30.1", + "@wdio/types": "9.30.1", + "@wdio/utils": "9.30.1", "deepmerge-ts": "^7.0.3", "https-proxy-agent": "^7.0.6", - "undici": "^6.21.3", - "ws": "^8.8.0" + "undici": "^6.27.0", + "ws": "^8.21.0" }, "engines": { "node": ">=18.20.0" @@ -10346,272 +10778,79 @@ "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~6.21.0" - } - }, - "node_modules/webdriver/node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/webdriverio": { - "version": "9.29.0", - "resolved": "https://registry.npmjs.org/webdriverio/-/webdriverio-9.29.0.tgz", - "integrity": "sha512-OoKlyOHhqUGIT1kL5mYbkjDI3bMf5xkBl4FsBwElOkU3aeUAKRe4aW8LzoQ1aNfut8POpjqcadQ4L5Lc4tnUwg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "^20.11.30", - "@types/sinonjs__fake-timers": "^8.1.5", - "@wdio/config": "9.29.0", - "@wdio/logger": "9.18.0", - "@wdio/protocols": "9.29.0", - "@wdio/repl": "9.16.2", - "@wdio/types": "9.29.0", - "@wdio/utils": "9.29.0", - "archiver": "^7.0.1", - "aria-query": "^5.3.0", - "cheerio": "^1.0.0-rc.12", - "css-shorthand-properties": "^1.1.1", - "css-value": "^0.0.1", - "grapheme-splitter": "^1.0.4", - "htmlfy": "^0.8.1", - "is-plain-obj": "^4.1.0", - "jszip": "^3.10.1", - "lodash.clonedeep": "^4.5.0", - "lodash.zip": "^4.2.0", - "query-selector-shadow-dom": "^1.0.1", - "resq": "^1.11.0", - "rgb2hex": "0.2.5", - "serialize-error": "^12.0.0", - "urlpattern-polyfill": "^10.0.0", - "webdriver": "9.29.0" - }, - "engines": { - "node": ">=18.20.0" - }, - "peerDependencies": { - "puppeteer-core": ">=22.x || <=24.x" - }, - "peerDependenciesMeta": { - "puppeteer-core": { - "optional": true - } - } - }, - "node_modules/webdriverio/node_modules/@types/node": { - "version": "20.19.43", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz", - "integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.21.0" - } - }, - "node_modules/webdriverio/node_modules/cheerio": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.2.0.tgz", - "integrity": "sha512-WDrybc/gKFpTYQutKIK6UvfcuxijIZfMfXaYm8NMsPQxSYvf+13fXUJ4rztGGbJcBQ/GF55gvrZ0Bc0bj/mqvg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cheerio-select": "^2.1.0", - "dom-serializer": "^2.0.0", - "domhandler": "^5.0.3", - "domutils": "^3.2.2", - "encoding-sniffer": "^0.2.1", - "htmlparser2": "^10.1.0", - "parse5": "^7.3.0", - "parse5-htmlparser2-tree-adapter": "^7.1.0", - "parse5-parser-stream": "^7.1.2", - "undici": "^7.19.0", - "whatwg-mimetype": "^4.0.0" - }, - "engines": { - "node": ">=20.18.1" - }, - "funding": { - "url": "https://github.com/cheeriojs/cheerio?sponsor=1" - } - }, - "node_modules/webdriverio/node_modules/cheerio-select": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", - "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "boolbase": "^1.0.0", - "css-select": "^5.1.0", - "css-what": "^6.1.0", - "domelementtype": "^2.3.0", - "domhandler": "^5.0.3", - "domutils": "^3.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, - "node_modules/webdriverio/node_modules/css-select": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", - "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "boolbase": "^1.0.0", - "css-what": "^6.1.0", - "domhandler": "^5.0.2", - "domutils": "^3.0.1", - "nth-check": "^2.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, - "node_modules/webdriverio/node_modules/dom-serializer": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", - "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", - "dev": true, - "license": "MIT", - "dependencies": { - "domelementtype": "^2.3.0", - "domhandler": "^5.0.2", - "entities": "^4.2.0" - }, - "funding": { - "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" - } - }, - "node_modules/webdriverio/node_modules/domhandler": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", - "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "domelementtype": "^2.3.0" - }, - "engines": { - "node": ">= 4" - }, - "funding": { - "url": "https://github.com/fb55/domhandler?sponsor=1" - } - }, - "node_modules/webdriverio/node_modules/domutils": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", - "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "dom-serializer": "^2.0.0", - "domelementtype": "^2.3.0", - "domhandler": "^5.0.3" - }, - "funding": { - "url": "https://github.com/fb55/domutils?sponsor=1" - } - }, - "node_modules/webdriverio/node_modules/entities": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", - "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/webdriverio/node_modules/htmlparser2": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz", - "integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==", - "dev": true, - "funding": [ - "https://github.com/fb55/htmlparser2?sponsor=1", - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ], - "license": "MIT", - "dependencies": { - "domelementtype": "^2.3.0", - "domhandler": "^5.0.3", - "domutils": "^3.2.2", - "entities": "^7.0.1" - } - }, - "node_modules/webdriverio/node_modules/htmlparser2/node_modules/entities": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", - "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/webdriverio/node_modules/parse5": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", - "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", - "dev": true, - "license": "MIT", - "dependencies": { - "entities": "^6.0.0" - }, - "funding": { - "url": "https://github.com/inikulin/parse5?sponsor=1" + "undici-types": "~6.21.0" } }, - "node_modules/webdriverio/node_modules/parse5-htmlparser2-tree-adapter": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz", - "integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==", + "node_modules/webdriver/node_modules/undici": { + "version": "6.28.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.28.0.tgz", + "integrity": "sha512-LIY910g9TI13YS95lrMFrs8Rm/u/irgHeTWoKCoteeJ04CUJ92eEfj0rVn+7VKMPBpUPiUoBKfhNyLI23EE/KA==", "dev": true, "license": "MIT", - "dependencies": { - "domhandler": "^5.0.3", - "parse5": "^7.0.0" - }, - "funding": { - "url": "https://github.com/inikulin/parse5?sponsor=1" + "engines": { + "node": ">=18.17" } }, - "node_modules/webdriverio/node_modules/parse5/node_modules/entities": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", - "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "node_modules/webdriver/node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "dev": true, - "license": "BSD-2-Clause", + "license": "MIT" + }, + "node_modules/webdriverio": { + "version": "9.30.1", + "resolved": "https://registry.npmjs.org/webdriverio/-/webdriverio-9.30.1.tgz", + "integrity": "sha512-HvoMHVPj1Ky04h0kEELKilWuzAi0Ctpmfw5V7LStQmOzbXkzfxxMutSB3SezJAYCWCnZ6zsEw7TdDWubWGY7qA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "^20.11.30", + "@types/sinonjs__fake-timers": "^8.1.5", + "@wdio/config": "9.30.1", + "@wdio/logger": "9.29.1", + "@wdio/protocols": "9.30.1", + "@wdio/repl": "9.16.2", + "@wdio/types": "9.30.1", + "@wdio/utils": "9.30.1", + "archiver": "^7.0.1", + "aria-query": "^5.3.0", + "cheerio": "^1.0.0-rc.12", + "css-shorthand-properties": "^1.1.1", + "css-value": "^0.0.1", + "grapheme-splitter": "^1.0.4", + "htmlfy": "^0.8.1", + "is-plain-obj": "^4.1.0", + "jszip": "^3.10.1", + "lodash.clonedeep": "^4.5.0", + "lodash.zip": "^4.2.0", + "query-selector-shadow-dom": "^1.0.1", + "resq": "^1.11.0", + "rgb2hex": "0.2.5", + "serialize-error": "^12.0.0", + "urlpattern-polyfill": "^10.0.0", + "webdriver": "9.30.1" + }, "engines": { - "node": ">=0.12" + "node": ">=18.20.0" }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" + "peerDependencies": { + "puppeteer-core": ">=22.x || <=24.x" + }, + "peerDependenciesMeta": { + "puppeteer-core": { + "optional": true + } } }, - "node_modules/webdriverio/node_modules/undici": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.28.0.tgz", - "integrity": "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==", + "node_modules/webdriverio/node_modules/@types/node": { + "version": "20.19.43", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz", + "integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==", "dev": true, "license": "MIT", - "engines": { - "node": ">=20.18.1" + "dependencies": { + "undici-types": "~6.21.0" } }, "node_modules/webdriverio/node_modules/undici-types": { @@ -10823,9 +11062,9 @@ "license": "ISC" }, "node_modules/ws": { - "version": "8.21.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", - "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz", + "integrity": "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==", "dev": true, "license": "MIT", "engines": { @@ -10844,10 +11083,37 @@ } } }, + "node_modules/xdg-app-paths": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/xdg-app-paths/-/xdg-app-paths-5.5.1.tgz", + "integrity": "sha512-hI3flOB4PLZIy5prbtTpirobtPE2ZtZ52szO+2mM9Efp6ErM398La+C1lIpNWDfNoQk+6Lsi6nMcCwVB7pxeMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "os-paths": "^4.0.1", + "xdg-portable": "^7.2.0" + }, + "engines": { + "node": ">= 6.0" + } + }, + "node_modules/xdg-portable": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/xdg-portable/-/xdg-portable-7.3.0.tgz", + "integrity": "sha512-sqMMuL1rc0FmMBOzCpd0yuy9trqF2yTTVe+E9ogwCSWQCdDEtQUwrZPT6AxqtsFGRNxycgncbP/xmOOSPw5ZUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "os-paths": "^4.0.1" + }, + "engines": { + "node": ">= 6.0" + } + }, "node_modules/xml-naming": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/xml-naming/-/xml-naming-0.1.0.tgz", - "integrity": "sha512-k8KO9hrMyNk6tUWqUfkTEZbezRRpONVOzUTnc97VnCvyj6Tf9lyUR9EDAIeiVLv56jsMcoXEwjW8Kv5yPY52lw==", + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/xml-naming/-/xml-naming-0.3.0.tgz", + "integrity": "sha512-ghig2TBE/H11aOVgmahA3MhimvkBr6JIYknH/Dhdk10nXwdbIqBJsbfMxpvFPG8bAw77gN29aQWvKpmVoPlvPQ==", "dev": true, "funding": [ { @@ -10886,25 +11152,6 @@ "url": "https://github.com/sponsors/eemeli" } }, - "node_modules/yargs": { - "version": "17.7.3", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", - "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", - "dev": true, - "license": "MIT", - "dependencies": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/yargs-parser": { "version": "21.1.1", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", @@ -10915,137 +11162,221 @@ "node": ">=12" } }, - "node_modules/yargs/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==", + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", "dev": true, "license": "MIT", - "engines": { - "node": ">=8" + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" } }, - "node_modules/yargs/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/yargs/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==", + "node_modules/zip-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-6.0.1.tgz", + "integrity": "sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==", "dev": true, "license": "MIT", "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" + "archiver-utils": "^5.0.0", + "compress-commons": "^6.0.2", + "readable-stream": "^4.0.0" }, "engines": { - "node": ">=8" + "node": ">= 14" } }, - "node_modules/yargs/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==", + "node_modules/zod": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.11.tgz", + "integrity": "sha512-WPsqwxITS2tzx1bzhIKsEs19ABD5vmCVa4xBo2tq/SrV4RNZtfws1EnCWQXM6yh8bD08a1idvkB5MZSBiZsjwg==", "dev": true, "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "packages/functions": { + "name": "@magmacomputing/tempo-fns", + "version": "0.1.3", + "license": "MIT", + "devDependencies": { + "@rollup/plugin-node-resolve": "^16.0.3", + "rollup": "^4.62.2", + "vitepress": "^1.6.4", + "vue": "^3.5.39" }, - "engines": { - "node": ">=8" + "peerDependencies": { + "@js-temporal/polyfill": "^0.5.1", + "@magmacomputing/tempo": "^3.7.0" + }, + "peerDependenciesMeta": { + "@js-temporal/polyfill": { + "optional": true + }, + "@magmacomputing/tempo": { + "optional": true + } } }, - "node_modules/yauzl": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", - "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "packages/functions/node_modules/@shikijs/types": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-2.5.0.tgz", + "integrity": "sha512-ygl5yhxki9ZLNuNpPitBWvcy9fsSKKaRuO4BAlMyagszQidxcpLAr0qiW/q43DtSIDxO6hEbtYLiFZNXO/hdGw==", "dev": true, "license": "MIT", "dependencies": { - "buffer-crc32": "~0.2.3", - "fd-slicer": "~1.1.0" + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" } }, - "node_modules/yauzl/node_modules/buffer-crc32": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "packages/functions/node_modules/@vitejs/plugin-vue": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz", + "integrity": "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==", "dev": true, "license": "MIT", "engines": { - "node": "*" + "node": "^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0", + "vue": "^3.2.25" } }, - "node_modules/yocto-queue": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.2.tgz", - "integrity": "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==", + "packages/functions/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" + } + }, + "packages/functions/node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", "dev": true, "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, "engines": { - "node": ">=12.20" + "node": "^18.0.0 || >=20.0.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } } }, - "node_modules/zip-stream": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-6.0.1.tgz", - "integrity": "sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==", + "packages/functions/node_modules/vitepress": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/vitepress/-/vitepress-1.6.4.tgz", + "integrity": "sha512-+2ym1/+0VVrbhNyRoFFesVvBvHAVMZMK0rw60E3X/5349M1GuVdKeazuksqopEdvkKwKGs21Q729jX81/bkBJg==", "dev": true, "license": "MIT", "dependencies": { - "archiver-utils": "^5.0.0", - "compress-commons": "^6.0.2", - "readable-stream": "^4.0.0" + "@docsearch/css": "3.8.2", + "@docsearch/js": "3.8.2", + "@iconify-json/simple-icons": "^1.2.21", + "@shikijs/core": "^2.1.0", + "@shikijs/transformers": "^2.1.0", + "@shikijs/types": "^2.1.0", + "@types/markdown-it": "^14.1.2", + "@vitejs/plugin-vue": "^5.2.1", + "@vue/devtools-api": "^7.7.0", + "@vue/shared": "^3.5.13", + "@vueuse/core": "^12.4.0", + "@vueuse/integrations": "^12.4.0", + "focus-trap": "^7.6.4", + "mark.js": "8.11.1", + "minisearch": "^7.1.1", + "shiki": "^2.1.0", + "vite": "^5.4.14", + "vue": "^3.5.13" }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/zwitch": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", - "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", - "dev": true, - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "packages/functions": { - "name": "@magmacomputing/tempo-fns", - "version": "0.1.3", - "license": "MIT", - "devDependencies": { - "@rollup/plugin-node-resolve": "^16.0.3", - "rollup": "^4.62.2", - "vitepress": "^1.6.4", - "vue": "^3.5.39" + "bin": { + "vitepress": "bin/vitepress.js" }, "peerDependencies": { - "@js-temporal/polyfill": "^0.5.1", - "@magmacomputing/tempo": "^3.7.0" + "markdown-it-mathjax3": "^4", + "postcss": "^8" }, "peerDependenciesMeta": { - "@js-temporal/polyfill": { + "markdown-it-mathjax3": { "optional": true }, - "@magmacomputing/tempo": { + "postcss": { "optional": true } } }, "packages/library": { "name": "@magmacomputing/library", - "version": "3.11.0", + "version": "3.11.1", "license": "MIT", "dependencies": { "tslib": "^2.8.1" @@ -11063,13 +11394,13 @@ }, "packages/plugins/ai": { "name": "@magmacomputing/tempo-plugin-ai", - "version": "0.2.0", + "version": "0.3.0", "license": "MIT", "devDependencies": { "@js-temporal/polyfill": "^0.5.1" }, "peerDependencies": { - "@magmacomputing/tempo": "^3.10.3" + "@magmacomputing/tempo": "^3.11.1" } }, "packages/plugins/astro": { @@ -11098,126 +11429,84 @@ "license": "MIT", "devDependencies": { "@magmacomputing/tempo": "^3.9.0", - "vitest": "^1.0.0" + "vitest": "^4.1.10" }, "peerDependencies": { "@magmacomputing/tempo": "^3.9.0" } }, - "packages/plugins/finance/node_modules/@vitest/expect": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.6.1.tgz", - "integrity": "sha512-jXL+9+ZNIJKruofqXuuTClf44eSpcHlgj3CiuNihUF3Ioujtmc0zIa3UJOW5RjDK1YLBJZnWBlPuqhYycLioog==", - "dev": true, + "packages/plugins/snap": { + "name": "@magmacomputing/tempo-plugin-snap", + "version": "1.3.2", "license": "MIT", - "dependencies": { - "@vitest/spy": "1.6.1", - "@vitest/utils": "1.6.1", - "chai": "^4.3.10" - }, - "funding": { - "url": "https://opencollective.com/vitest" + "devDependencies": {}, + "peerDependencies": { + "@magmacomputing/tempo": "^3.6.1" } }, - "packages/plugins/finance/node_modules/@vitest/runner": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.6.1.tgz", - "integrity": "sha512-3nSnYXkVkf3mXFfE7vVyPmi3Sazhb/2cfZGGs0JRzFsPFvAMBEcrweV1V1GsrstdXeKCTXlJbvnQwGWgEIHmOA==", - "dev": true, + "packages/plugins/sync": { + "name": "@magmacomputing/tempo-plugin-sync", + "version": "1.0.3", "license": "MIT", - "dependencies": { - "@vitest/utils": "1.6.1", - "p-limit": "^5.0.0", - "pathe": "^1.1.1" - }, - "funding": { - "url": "https://opencollective.com/vitest" + "devDependencies": {}, + "peerDependencies": { + "@magmacomputing/tempo": "^3.6.1" } }, - "packages/plugins/finance/node_modules/@vitest/snapshot": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.6.1.tgz", - "integrity": "sha512-WvidQuWAzU2p95u8GAKlRMqMyN1yOJkGHnx3M1PL9Raf7AQ1kwLKg04ADlCa3+OXUZE7BceOhVZiuWAbzCKcUQ==", - "dev": true, + "packages/tempo": { + "name": "@magmacomputing/tempo", + "version": "3.11.1", "license": "MIT", "dependencies": { - "magic-string": "^0.30.5", - "pathe": "^1.1.1", - "pretty-format": "^29.7.0" + "tslib": "^2.8.1" }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "packages/plugins/finance/node_modules/@vitest/spy": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.6.1.tgz", - "integrity": "sha512-MGcMmpGkZebsMZhbQKkAf9CX5zGvjkBTqf8Zx3ApYWXr3wG+QvEu2eXWfnIIWYSJExIp4V9FCKDEeygzkYrXMw==", - "dev": true, - "license": "MIT", - "dependencies": { - "tinyspy": "^2.2.0" + "bin": { + "tempo": "bin/tempo.js" }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "packages/plugins/finance/node_modules/@vitest/utils": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.6.1.tgz", - "integrity": "sha512-jOrrUvXM4Av9ZWiG1EajNto0u96kWAhJ1LmPmJhXXQx/32MecEKd10pOLYgS2BQx1TgkGhloPU1ArDW2vvaY6g==", - "dev": true, - "license": "MIT", - "dependencies": { - "diff-sequences": "^29.6.3", - "estree-walker": "^3.0.3", - "loupe": "^2.3.7", - "pretty-format": "^29.7.0" + "devDependencies": { + "@js-temporal/polyfill": "^0.5.1", + "@magmacomputing/library": "*", + "@magmacomputing/tempo-plugin-ticker": "^2.2.3", + "@rollup/plugin-alias": "^6.0.0", + "@rollup/plugin-terser": "^1.0.0", + "@rollup/plugin-typescript": "^12.3.0", + "javascript-obfuscator": "^5.4.3", + "magic-string": "^1.1.1", + "mermaid": "^11.16.1", + "typedoc": "^0.28.19", + "vitepress": "^1.6.4", + "vitepress-plugin-mermaid": "^2.0.17" }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "packages/plugins/finance/node_modules/assertion-error": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", - "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", - "dev": true, - "license": "MIT", "engines": { - "node": "*" + "node": ">=20.0.0" } }, - "packages/plugins/finance/node_modules/chai": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/chai/-/chai-4.5.0.tgz", - "integrity": "sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw==", + "packages/tempo/node_modules/@shikijs/types": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-2.5.0.tgz", + "integrity": "sha512-ygl5yhxki9ZLNuNpPitBWvcy9fsSKKaRuO4BAlMyagszQidxcpLAr0qiW/q43DtSIDxO6hEbtYLiFZNXO/hdGw==", "dev": true, "license": "MIT", "dependencies": { - "assertion-error": "^1.1.0", - "check-error": "^1.0.3", - "deep-eql": "^4.1.3", - "get-func-name": "^2.0.2", - "loupe": "^2.3.6", - "pathval": "^1.1.1", - "type-detect": "^4.1.0" - }, - "engines": { - "node": ">=4" + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" } }, - "packages/plugins/finance/node_modules/estree-walker": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", - "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "packages/tempo/node_modules/@vitejs/plugin-vue": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz", + "integrity": "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==", "dev": true, "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0" + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0", + "vue": "^3.2.25" } }, - "packages/plugins/finance/node_modules/fsevents": { + "packages/tempo/node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", @@ -11232,21 +11521,7 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, - "packages/plugins/finance/node_modules/pathe": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", - "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", - "dev": true, - "license": "MIT" - }, - "packages/plugins/finance/node_modules/std-env": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", - "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", - "dev": true, - "license": "MIT" - }, - "packages/plugins/finance/node_modules/vite": { + "packages/tempo/node_modules/vite": { "version": "5.4.21", "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", @@ -11306,114 +11581,60 @@ } } }, - "packages/plugins/finance/node_modules/vitest": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.6.1.tgz", - "integrity": "sha512-Ljb1cnSJSivGN0LqXd/zmDbWEM0RNNg2t1QW/XUhYl/qPqyu7CsqeWtqQXHVaJsecLPuDoak2oJcZN2QoRIOag==", + "packages/tempo/node_modules/vitepress": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/vitepress/-/vitepress-1.6.4.tgz", + "integrity": "sha512-+2ym1/+0VVrbhNyRoFFesVvBvHAVMZMK0rw60E3X/5349M1GuVdKeazuksqopEdvkKwKGs21Q729jX81/bkBJg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/expect": "1.6.1", - "@vitest/runner": "1.6.1", - "@vitest/snapshot": "1.6.1", - "@vitest/spy": "1.6.1", - "@vitest/utils": "1.6.1", - "acorn-walk": "^8.3.2", - "chai": "^4.3.10", - "debug": "^4.3.4", - "execa": "^8.0.1", - "local-pkg": "^0.5.0", - "magic-string": "^0.30.5", - "pathe": "^1.1.1", - "picocolors": "^1.0.0", - "std-env": "^3.5.0", - "strip-literal": "^2.0.0", - "tinybench": "^2.5.1", - "tinypool": "^0.8.3", - "vite": "^5.0.0", - "vite-node": "1.6.1", - "why-is-node-running": "^2.2.2" + "@docsearch/css": "3.8.2", + "@docsearch/js": "3.8.2", + "@iconify-json/simple-icons": "^1.2.21", + "@shikijs/core": "^2.1.0", + "@shikijs/transformers": "^2.1.0", + "@shikijs/types": "^2.1.0", + "@types/markdown-it": "^14.1.2", + "@vitejs/plugin-vue": "^5.2.1", + "@vue/devtools-api": "^7.7.0", + "@vue/shared": "^3.5.13", + "@vueuse/core": "^12.4.0", + "@vueuse/integrations": "^12.4.0", + "focus-trap": "^7.6.4", + "mark.js": "8.11.1", + "minisearch": "^7.1.1", + "shiki": "^2.1.0", + "vite": "^5.4.14", + "vue": "^3.5.13" }, "bin": { - "vitest": "vitest.mjs" - }, - "engines": { - "node": "^18.0.0 || >=20.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" + "vitepress": "bin/vitepress.js" }, "peerDependencies": { - "@edge-runtime/vm": "*", - "@types/node": "^18.0.0 || >=20.0.0", - "@vitest/browser": "1.6.1", - "@vitest/ui": "1.6.1", - "happy-dom": "*", - "jsdom": "*" + "markdown-it-mathjax3": "^4", + "postcss": "^8" }, "peerDependenciesMeta": { - "@edge-runtime/vm": { - "optional": true - }, - "@types/node": { - "optional": true - }, - "@vitest/browser": { - "optional": true - }, - "@vitest/ui": { - "optional": true - }, - "happy-dom": { + "markdown-it-mathjax3": { "optional": true }, - "jsdom": { + "postcss": { "optional": true } } }, - "packages/plugins/snap": { - "name": "@magmacomputing/tempo-plugin-snap", - "version": "1.3.2", - "license": "MIT", - "devDependencies": {}, - "peerDependencies": { - "@magmacomputing/tempo": "^3.6.1" - } - }, - "packages/plugins/sync": { - "name": "@magmacomputing/tempo-plugin-sync", - "version": "1.0.3", - "license": "MIT", - "devDependencies": {}, - "peerDependencies": { - "@magmacomputing/tempo": "^3.6.1" - } - }, - "packages/tempo": { - "name": "@magmacomputing/tempo", - "version": "3.11.0", + "packages/tempo/node_modules/vitepress-plugin-mermaid": { + "version": "2.0.17", + "resolved": "https://registry.npmjs.org/vitepress-plugin-mermaid/-/vitepress-plugin-mermaid-2.0.17.tgz", + "integrity": "sha512-IUzYpwf61GC6k0XzfmAmNrLvMi9TRrVRMsUyCA8KNXhg/mQ1VqWnO0/tBVPiX5UoKF1mDUwqn5QV4qAJl6JnUg==", + "dev": true, "license": "MIT", - "dependencies": { - "tslib": "^2.8.1" - }, - "bin": { - "tempo": "bin/tempo.js" - }, - "devDependencies": { - "@js-temporal/polyfill": "^0.5.1", - "@magmacomputing/library": "*", - "@magmacomputing/tempo-plugin-ticker": "^2.2.3", - "@rollup/plugin-alias": "^6.0.0", - "@rollup/plugin-terser": "^1.0.0", - "@rollup/plugin-typescript": "^12.3.0", - "javascript-obfuscator": "^5.4.3", - "magic-string": "^0.30.21", - "typedoc": "^0.28.19", - "vitepress": "^1.6.4" + "optionalDependencies": { + "@mermaid-js/mermaid-mindmap": "^9.3.0" }, - "engines": { - "node": ">=20.0.0" + "peerDependencies": { + "mermaid": "10 || 11", + "vitepress": "^1.0.0 || ^1.0.0-alpha" } } } diff --git a/package.json b/package.json index a960cd8a..2361dc82 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tempo-monorepo", - "version": "3.11.0", + "version": "3.11.1", "private": true, "engines": { "node": ">=20.0.0" @@ -54,20 +54,20 @@ "typescript": "^6.0.3", "typescript-7": "npm:typescript@^7.0.2", "unplugin-swc": "^1.5.9", - "vitest": "^4.1.7", - "webdriverio": "^9.29.0" + "vitest": "4.1.10", + "webdriverio": "^9.30.1" }, "overrides": { "esbuild": "^0.28.0" }, "allowScripts": { "edgedriver@6.3.0": true, - "geckodriver@6.1.0": true, - "esbuild@0.28.1": true, - "esbuild@0.21.5": true, - "@swc/core@1.15.43": true + "@swc/core@1.15.47": true, + "esbuild@0.28.2": true, + "geckodriver@6.1.1": true }, "dependencies": { + "magic-string": "^1.1.1", "typescript-7": "npm:typescript@^7.0.2" } } diff --git a/packages/library/CHANGELOG.md b/packages/library/CHANGELOG.md index 1282f066..b3ac0840 100644 --- a/packages/library/CHANGELOG.md +++ b/packages/library/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [3.11.1] - 2026-08-06 + +### Added +- **Calendar & Time Math (`calendar.library`)**: Added standalone date/calendar constants and helpers (`ISO_WEEKDAY_NAMES`, `DAY_MAP`, `MONTH_MAP`, `getDaysInMonth`, `getUtcParts`, `DayKey`, `MonthKey`, `IsoWeekdayNames`) in `#library/calendar.library.js`. +- **Recurrence Engine (`recurrence.library`)**: Added standalone zero-dependency RFC 5545 recurrence rule utilities (`isRRuleString`, `isFiniteRRule`, `parseRRule`, `getNextRRuleEpoch`, `expandRRuleEpochs`, `ParsedRRule`) to `#library/recurrence.library.js`. + ## [3.10.2] - 2026-07-25 ### Fixed diff --git a/packages/library/README.md b/packages/library/README.md index 57fb95a3..ce00846f 100644 --- a/packages/library/README.md +++ b/packages/library/README.md @@ -1,4 +1,4 @@ -# Magma Library (Internal Reference) +# Tempo Library Logo Magma Library (Internal Reference) > [!NOTE] > **Internal Reference Package**: `packages/library` is an internal monorepo utility suite used across Tempo packages. It is **not** published as a standalone package on npm, and is provided in the documentation as a reference guide for internal architectural utilities and shared routines. @@ -25,6 +25,7 @@ The library is organized into specialized modules, each designed for maximum eff | **Pledge** | A robust wrapper for native Promises with settled-state tracking and timeout support. | | **Reflection** | Clean access to own-properties, values, and entries without prototype pollution. | | **Temporal** | Lightweight helpers and polyfill integration for the native `Temporal` API. | +| **Recurrence** | Zero-dependency RFC 5545 recurrence rule parsing, expansion, and finiteness evaluation. | --- diff --git a/packages/library/img/library-logo.svg b/packages/library/img/library-logo.svg new file mode 100644 index 00000000..ec7fd7c4 --- /dev/null +++ b/packages/library/img/library-logo.svg @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/library/package.json b/packages/library/package.json index 312b3090..9ecbafb8 100644 --- a/packages/library/package.json +++ b/packages/library/package.json @@ -1,6 +1,6 @@ { "name": "@magmacomputing/library", - "version": "3.11.0", + "version": "3.11.1", "description": "Shared utility library for Tempo", "author": "Magma Computing Solutions", "license": "MIT", @@ -26,7 +26,8 @@ ".": { "types": "./dist/common.index.d.ts", "development": "./src/common.index.ts", - "import": "./dist/common.index.js" + "import": "./dist/common.index.js", + "default": "./dist/common.index.js" }, "./browser": { "types": "./dist/browser.index.d.ts", diff --git a/packages/library/src/common.index.ts b/packages/library/src/common.index.ts index 262b00d7..44d73521 100644 --- a/packages/library/src/common.index.ts +++ b/packages/library/src/common.index.ts @@ -26,5 +26,7 @@ export * from './common/symbol.library.js'; export * from './common/type.library.js'; export * from './common/temporal.polyfill.js'; export * from './common/temporal.library.js'; +export * from './common/calendar.library.js'; +export * from './common/recurrence.library.js'; export * from './common/utility.library.js'; export * from './common/webtoken.library.js'; diff --git a/packages/library/src/common/assertion.library.ts b/packages/library/src/common/assertion.library.ts index 0c56d622..8ffab521 100644 --- a/packages/library/src/common/assertion.library.ts +++ b/packages/library/src/common/assertion.library.ts @@ -24,6 +24,9 @@ export const isString = (obj: unknown): obj is string => isType(obj, 'St export const isNumber = (obj: unknown): obj is number => isType(obj, 'Number'); export const isFiniteNumber = (obj: unknown): obj is number => isType(obj, 'Number') && isFinite(obj as number); +const RE_BIGINT_LITERAL = /^[+-]?[0-9]+n$/; +const RE_REGEXP_LITERAL = /^\/.*\/$/; + /** * Tests if a value can be safely converted to a numeric value. * Handles strings, numbers, and BigInts, verifying finite properties and valid formats. @@ -44,13 +47,13 @@ export function isNumeric(str?: any): boolean { case 'string': { const val = str.trim(); if (val.length === 0) return false; - return /^-?[0-9]+n$/.test(val) || (!isNaN(parseFloat(val)) && isFinite(Number(val))); + return RE_BIGINT_LITERAL.test(val) || (!isNaN(parseFloat(val)) && isFinite(Number(val))); } default: return false; } } export const isInteger = (obj: unknown): obj is bigint => isType(obj, 'BigInt'); -export const isIntegerLike = (obj: unknown): obj is string => isType(obj, 'String') && /^-?[0-9]+n$/.test(obj as string); +export const isIntegerLike = (obj: unknown): obj is string => isType(obj, 'String') && RE_BIGINT_LITERAL.test((obj as string).trim()); export const isDigit = (obj: unknown): obj is number | bigint => isType(obj, 'Number', 'BigInt'); export const isBoolean = (obj: unknown): obj is boolean => isType(obj, 'Boolean'); export const isArray = (obj: unknown): obj is T[] => isType(obj, 'Array'); @@ -58,7 +61,7 @@ export const isArrayLike = (obj: any): obj is ArrayLike => protoType export const isObject = (obj: unknown): obj is Property => isType>(obj, 'Object'); export const isDate = (obj: unknown): obj is Date => isType(obj, 'Date'); export const isRegExp = (obj: unknown): obj is RegExp => isType(obj, 'RegExp'); -export const isRegExpLike = (obj: unknown): obj is string => isType(obj, 'String') && /^\/.*\/$/.test(obj as string); +export const isRegExpLike = (obj: unknown): obj is string => isType(obj, 'String') && RE_REGEXP_LITERAL.test(obj as string); export const isSymbol = (obj: unknown): obj is symbol => isType(obj, 'Symbol'); export const isSymbolFor = (obj: unknown): obj is symbol => isType(obj, 'Symbol') && Symbol.keyFor(obj as symbol) !== undefined; export const isPropertyKey = (obj: unknown): obj is PropertyKey => isType(obj, 'String', 'Number', 'Symbol'); diff --git a/packages/library/src/common/calendar.library.ts b/packages/library/src/common/calendar.library.ts new file mode 100644 index 00000000..24f08cbc --- /dev/null +++ b/packages/library/src/common/calendar.library.ts @@ -0,0 +1,198 @@ +/** + * Number of days in a standard week. + */ +export const DAYS_IN_WEEK = 7; + +/** + * Mapping of 2-letter and 3-letter ISO day abbreviations to 1-indexed weekday numbers (1..7, Monday=1, Sunday=7). + */ +export const DAY_MAP = Object.freeze({ + MO: 1, TU: 2, WE: 3, TH: 4, FR: 5, SA: 6, SU: 7, + MON: 1, TUE: 2, WED: 3, THU: 4, FRI: 5, SAT: 6, SUN: 7, +} as const); + +/** Type representing the DAY_MAP mapping structure. */ +export type DayMap = typeof DAY_MAP; + +/** Union type of all valid 2-letter and 3-letter day abbreviation keys ('MO' | 'TU' | ... | 'SUN'). */ +export type DayKey = keyof DayMap; + +/** Union type of all valid ISO weekday numeric values (1..7). */ +export type DayValue = DayMap[DayKey]; + +/** + * Mapping of 3-letter month abbreviations (JAN..DEC) to 1-indexed month numbers (1..12). + */ +export const MONTH_MAP = Object.freeze({ + JAN: 1, FEB: 2, MAR: 3, APR: 4, MAY: 5, JUN: 6, + JUL: 7, AUG: 8, SEP: 9, OCT: 10, NOV: 11, DEC: 12, +} as const); + +/** Type representing the MONTH_MAP mapping structure. */ +export type MonthMap = typeof MONTH_MAP; + +/** Union type of all valid 3-letter month abbreviation keys ('JAN' | 'FEB' | ... | 'DEC'). */ +export type MonthKey = keyof MonthMap; + +/** Union type of all valid 1-indexed month numeric values (1..12). */ +export type MonthValue = MonthMap[MonthKey]; + +/** + * Mapping of 1-indexed ISO weekday numbers (1..7, Monday=1, Sunday=7) to full English weekday names. + */ +export const ISO_WEEKDAY_NAMES = Object.freeze({ + 1: 'Monday', + 2: 'Tuesday', + 3: 'Wednesday', + 4: 'Thursday', + 5: 'Friday', + 6: 'Saturday', + 7: 'Sunday', +} as const); + +/** Type representing the ISO_WEEKDAY_NAMES mapping structure. */ +export type IsoWeekdayNames = typeof ISO_WEEKDAY_NAMES; + +/** Union type of all valid ISO weekday numbers (1 | 2 | 3 | 4 | 5 | 6 | 7). */ +export type IsoWeekdayNumber = keyof IsoWeekdayNames; + +/** Union type of all full English weekday names ('Monday' | 'Tuesday' | ... | 'Sunday'). */ +export type IsoWeekdayName = IsoWeekdayNames[IsoWeekdayNumber]; + +/** + * Structured UTC calendar and clock components. + */ +export interface UtcParts { + year: number; + month: number; // 1-indexed (1..12) + day: number; // 1-indexed (1..31) + weekday: number; // 1-indexed ISO weekday (1..7, Monday=1, Sunday=7) + hours: number; // 0..23 + minutes: number; // 0..59 + seconds: number; // 0..59 + milliseconds: number; // 0..999 +} + +/** + * Extracts all UTC calendar and clock components from a Date instance as a structured object. + * Month and weekday are 1-indexed (1 for January, 1 for Monday through 7 for Sunday). + * Defaults to current timestamp (`new Date()`) if no date is provided. + * + * @param date - Optional Date instance to inspect (defaults to `new Date()`) + * @returns An object containing `{ year, month, day, weekday, hours, minutes, seconds, milliseconds }` + */ +export function getUtcParts(date: Date = new Date()): UtcParts { + return { + year: date.getUTCFullYear(), + month: date.getUTCMonth() + 1, + day: date.getUTCDate(), + weekday: date.getUTCDay() || DAY_MAP.SUN, + hours: date.getUTCHours(), + minutes: date.getUTCMinutes(), + seconds: date.getUTCSeconds(), + milliseconds: date.getUTCMilliseconds(), + } +} + +/** + * Returns the total number of days in the specified month of a given year (1-indexed month, 1..12). + * + * @param year - The full calendar year (e.g. 2026) + * @param month - The 1-indexed month number (1 for January through 12 for December) + * @returns Total number of days in that month (28..31) + */ +export function getDaysInMonth(year: number, month: number): number { + return new Date(Date.UTC(year, month, 0)).getUTCDate(); +} + +/** + * Determines whether a given calendar year is a leap year in the Gregorian calendar. + * + * @param year - The full calendar year to test (e.g. 2024) + * @returns `true` if the year is a leap year, otherwise `false` + */ +export function isLeapYear(year: number): boolean { + return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0; +} + +/** + * Structured calendar and clock options for creating a UTC Date instance. + */ +export interface UtcPartsOptions { + year: number; + month: number; // 1-indexed (1..12) + day: number; // 1-indexed (1..31) + hours?: number; // 0..23 (default: 0) + minutes?: number; // 0..59 (default: 0) + seconds?: number; // 0..59 (default: 0) + milliseconds?: number; // 0..999 (default: 0) +} + +/** Type alias for UtcPartsOptions. */ +export type UtcDateOptions = UtcPartsOptions; + +/** + * Creates a UTC Date instance from structured 1-indexed calendar components and optional clock values. + * Inverse of `getUtcParts`. + * + * @param parts - Structured calendar components containing year, month, day, and optional clock components + * @returns A new UTC Date instance + */ +export function fromUtcParts(parts: UtcPartsOptions): Date { + const { + year, + month, + day, + hours = 0, + minutes = 0, + seconds = 0, + milliseconds = 0, + } = parts; + const d = new Date(0); + d.setUTCFullYear(year, month - 1, day); + d.setUTCHours(hours, minutes, seconds, milliseconds); + return d; +} + +/** + * Validates whether a given year, 1-indexed month, and day constitute a valid calendar date in the Gregorian calendar. + * + * @param year - The full calendar year (e.g. 2026) + * @param month - The 1-indexed month number (1..12) + * @param day - The 1-indexed day of the month (1..31) + * @returns `true` if the year, month, and day represent a valid date, otherwise `false` + */ +export function isValidDate(year: number, month: number, day: number): boolean { + if (!Number.isInteger(year) || !Number.isInteger(month) || !Number.isInteger(day)) return false; + if (month < 1 || month > 12 || day < 1) return false; + const d = new Date(0); + d.setUTCFullYear(year, month - 1, day); + return d.getUTCFullYear() === year && d.getUTCMonth() === month - 1 && d.getUTCDate() === day; +} + +/** + * Adds an integer number of days to a Date in UTC, returning a new Date instance. + * + * @param date - The baseline Date instance + * @param days - Number of days to add (can be negative) + * @returns A new Date instance offset by the specified number of days + */ +export function addUtcDays(date: Date, days: number): Date { + const result = new Date(date.getTime()); + result.setUTCDate(result.getUTCDate() + days); + return result; +} + +/** + * Returns a new Date instance with updated UTC calendar or clock fields applied to the given date. + * Non-mutating (pure copy-on-write). + * + * @param date - The source Date instance + * @param parts - Partial UTC parts to overwrite on the source date + * @returns A new UTC Date instance with the specified parts updated + */ +export function withUtcParts(date: Date, parts: Partial): Date { + const current = getUtcParts(date); + return fromUtcParts({ ...current, ...parts }); +} + diff --git a/packages/library/src/common/cipher.library.ts b/packages/library/src/common/cipher.library.ts index 56381814..dc6472ed 100644 --- a/packages/library/src/common/cipher.library.ts +++ b/packages/library/src/common/cipher.library.ts @@ -148,6 +148,8 @@ export const verify = async (signature: Promise | ArrayBuffer | Uin return subtle.verify(keys.SignKey, keypair.publicKey, buffer as BufferSource, encodeBuffer(doc)); } +const RE_WHITESPACE = /\s+/g; + /** * Imports a PEM-formatted public key string into a native Web Crypto API CryptoKey object. * @@ -159,7 +161,7 @@ export const importPublicKey = async (pem: string): Promise => { const pemFooter = '-----END PUBLIC KEY-----'; const pemContents = pem .substring(pem.indexOf(pemHeader) + pemHeader.length, pem.indexOf(pemFooter)) - .replace(/\s+/g, ''); + .replace(RE_WHITESPACE, ''); const binaryDerString = atob(pemContents); const binaryDer = new Uint8Array(binaryDerString.length); diff --git a/packages/library/src/common/coercion.library.ts b/packages/library/src/common/coercion.library.ts index b71586b8..539c7def 100644 --- a/packages/library/src/common/coercion.library.ts +++ b/packages/library/src/common/coercion.library.ts @@ -57,6 +57,48 @@ export function asNumber(str?: string | number | bigint) { return parseFloat(str?.toString() ?? 'NaN'); } +const RE_INTEGER = /^[+-]?[0-9]+$/; +const RE_SIGN_START = /^[+-]/; +const MAX_SAFE_INTEGER_BIGINT = BigInt(Number.MAX_SAFE_INTEGER); +const MIN_SAFE_INTEGER_BIGINT = BigInt(Number.MIN_SAFE_INTEGER); + +/** + * @param str - The value with optional plus|minus prefix + * @returns The string value without the sign prefix + */ +function removeSign(str: string | number | bigint) { + return String(str).trim().replace(RE_SIGN_START, ''); +} + +/** + * Parses any `string | number | bigint` to a canonical signed `bigint`. + * Handles optional sign prefix, trailing `n` suffix (BigInt literal), and float truncation. + * Does **not** apply a safe-integer boundary check — that is `toBounded`'s responsibility. + */ +function parseBigInt(str: string | number | bigint): bigint { + if (isInteger(str)) return str; // already a bigint + if (isNumber(str)) return BigInt(Math.trunc(str)); // truncate float, no string round-trip + + const raw = String(str).trim(); + const clean = removeSign(str); + if (clean.length === 0) return BigInt(raw); + + const sign = raw.startsWith('-') ? -1n : 1n; // preserve sign before stripping + const trim = isIntegerLike(clean) ? -1 : undefined; // strip trailing 'n' (undefined = keep full string) + return sign * BigInt(clean.slice(0, trim)); +} + +/** + * Applies the safe-integer boundary check to a `bigint`. + * Returns a `number` when the value fits within `Number.MAX_SAFE_INTEGER` bounds, + * otherwise preserves the `bigint` to avoid precision loss. + */ +function toBounded(big: bigint): number | bigint { + return (big > MAX_SAFE_INTEGER_BIGINT || big < MIN_SAFE_INTEGER_BIGINT) + ? big + : Number(big); +} + /** * Coerces a String or Number to a BigInt. * @@ -76,9 +118,7 @@ export function asInteger(str?: T) { case 'Number': return BigInt(Math.trunc(arg.value)); // cast as BigInt case 'String': - return (isIntegerLike(arg.value)) // String representation of a BigInt - ? BigInt(arg.value.slice(0, -1)) // get rid of trailing 'n' - : BigInt(arg.value); + return parseBigInt(arg.value); // normalise sign + optional trailing 'n' default: return str as Exclude; } @@ -96,28 +136,17 @@ export function asInteger(str?: T) { * ``` */ export const ifNumeric = (str: string | number | bigint, stripZero = false) => { - switch (true) { - case isInteger(str): { - const big = str as bigint; - if (big > BigInt(Number.MAX_SAFE_INTEGER) || big < BigInt(Number.MIN_SAFE_INTEGER)) return big; - return Number(big); - } - - case isNumber(str): - return str; - - case isNumeric(str) && (!str?.toString().startsWith('0') || stripZero): { - const numStr = String(str); - if (/^-?[0-9]+$/.test(numStr)) { - const big = BigInt(numStr); - if (big > BigInt(Number.MAX_SAFE_INTEGER) || big < BigInt(Number.MIN_SAFE_INTEGER)) return big; - } - return asNumber(str); - } + if (isInteger(str) || (isNumber(str) && Number.isFinite(str)))// native BigInt or finite number → boundary-check + return toBounded(parseBigInt(str)); - default: - return str as string; // non-numeric String → as-is - } + const value = removeSign(str); // only reached for string input + + if (isNumeric(str) && (!value.startsWith('0') || stripZero)) + return (isIntegerLike(value) || RE_INTEGER.test(value)) // BigInt literal or plain integer string + ? toBounded(parseBigInt(str as string)) + : asNumber(str); // floating-point string + + return str; // non-numeric or leading-zero string → as-is } /** diff --git a/packages/library/src/common/international.library.ts b/packages/library/src/common/international.library.ts index 62e6bb92..7b079c50 100644 --- a/packages/library/src/common/international.library.ts +++ b/packages/library/src/common/international.library.ts @@ -61,6 +61,8 @@ export function getDateTimeFormat() { return getDTF().resolvedOptions(); } +const RE_UNDERSCORE = /_/g; + /** * Returns the canonicalized locale string, or undefined if the locale is invalid. * Uses `Intl.getCanonicalLocales` for strict validation. @@ -74,7 +76,7 @@ export function getDateTimeFormat() { */ export function canonicalLocale(locale: string): string | undefined { try { - return Intl.getCanonicalLocales(locale.replace(/_/g, '-'))[0]; + return Intl.getCanonicalLocales(locale.replace(RE_UNDERSCORE, '-'))[0]; } catch (e) { console.warn(`[Tempo] dropping invalid locale: '${locale}'`, e); return undefined; diff --git a/packages/library/src/common/object.library.ts b/packages/library/src/common/object.library.ts index 4e082186..d4810464 100644 --- a/packages/library/src/common/object.library.ts +++ b/packages/library/src/common/object.library.ts @@ -3,6 +3,9 @@ import { isObject, isArray, isFunction, isDefined, isNullish, isMap, isSet } fro import { getType } from '#library/type.library.js'; import type { Extend, Property } from '#library/type.library.js'; +const RE_UNQUOTE_PROP = /"([^"]+)":/g; +const RE_COMMA = /,/g; + /** * Serializes an object to JSON and removes quotes around property names. * Useful for generating loosely formatted string representations of objects. @@ -16,8 +19,8 @@ import type { Extend, Property } from '#library/type.library.js'; */ export const unQuoteObj = (obj: any) => { return JSON.stringify(obj) - ?.replace(/"([^"]+)":/g, '$1: ') - ?.replace(/,/g, ', ') + ?.replace(RE_UNQUOTE_PROP, '$1: ') + ?.replace(RE_COMMA, ', ') } /** diff --git a/packages/library/src/common/primitive.library.ts b/packages/library/src/common/primitive.library.ts index 4f7a2d3b..3963e35f 100644 --- a/packages/library/src/common/primitive.library.ts +++ b/packages/library/src/common/primitive.library.ts @@ -121,6 +121,8 @@ export function ownValues(json: T, all = false): ValueOf[] { return ownEntries(json, all).map(([_, value]) => value as ValueOf); } +const RE_BRACKET_PATH = /\[([^\[\]]*)\]/g; + /** * Gets a nested value from an object using dot or bracket notation. * @@ -139,7 +141,7 @@ export function extract(obj: any, path: string | number, dflt?: T): T { return path .toString() - .replace(/\[([^\[\]]*)\]/g, '.$1.') + .replace(RE_BRACKET_PATH, '.$1.') .split('.') .filter(field => field.length > 0) .reduce((acc, field) => acc?.[field] ?? null, obj) ?? dflt; diff --git a/packages/library/src/common/proxy.library.ts b/packages/library/src/common/proxy.library.ts index cc4259c5..49f2936b 100644 --- a/packages/library/src/common/proxy.library.ts +++ b/packages/library/src/common/proxy.library.ts @@ -124,7 +124,7 @@ function factory(target: T, options: ProxyOptions = {}): T { } const val = Reflect.get(t, k, r); - if (bind && isFunction(val)) { + if (bind && k !== 'constructor' && isFunction(val)) { const desc = Object.getOwnPropertyDescriptor(t, k); if (desc && !desc.configurable && !desc.writable) return val; let perTargetCache = boundMethodCache.get(val); diff --git a/packages/library/src/common/recurrence.library.ts b/packages/library/src/common/recurrence.library.ts new file mode 100644 index 00000000..5490a54a --- /dev/null +++ b/packages/library/src/common/recurrence.library.ts @@ -0,0 +1,418 @@ +import { isDefined } from './assertion.library.js'; +import { + DAYS_IN_WEEK, + DAY_MAP, + MONTH_MAP, + getDaysInMonth, + getUtcParts, + fromUtcParts, + isValidDate, + addUtcDays, + withUtcParts, + type DayKey, + type MonthKey, +} from './calendar.library.js'; + +const RE_RRULE_FREQ = /^(RRULE:)?FREQ=(DAILY|WEEKLY|MONTHLY|YEARLY)(;|$)/i; +const RE_FINITE_RRULE = /(?:^|;)(?:RRULE:)?(UNTIL|COUNT)=/i; +const RE_RRULE_PREFIX = /^RRULE:/i; +const RE_DATE_8DIGIT = /^\d{8}$/; +const RE_UNTIL_TIMESTAMP = /^(\d{4})(\d{2})(\d{2})T?(\d{2})?(\d{2})?(\d{2})?Z?$/; +const RE_BYDAY_PART = /^([+-]?\d+)?([A-Z]+)$/i; + +/** + * Tests whether a string is a valid RFC 5545 Recurrence Rule (RRULE). + * + * @param input - The candidate string to inspect + * @returns `true` if the string matches an RRULE pattern starting with FREQ=, otherwise `false`. + */ +export function isRRuleString(input: string): boolean { + const trimmed = input.trim(); + return RE_RRULE_FREQ.test(trimmed); +} + +/** + * Checks whether an RRULE string represents a finite (bounded) series. + * + * @param rrule - The RFC 5545 RRULE string + * @returns `true` if the rule contains an UNTIL or COUNT boundary clause, otherwise `false`. + */ +export function isFiniteRRule(rrule: string): boolean { + return RE_FINITE_RRULE.test(rrule); +} + +/** + * Parsed structure of an RFC 5545 Recurrence Rule string. + */ +export interface ParsedRRule { + /** Frequency unit: DAILY, WEEKLY, MONTHLY, or YEARLY */ + freq: string; + /** Inter-occurrence interval count (default: 1) */ + interval: number; + /** Maximum number of occurrences to generate, if bounded by COUNT */ + count?: number | undefined; + /** Bounded end timestamp in epoch milliseconds, if bounded by UNTIL */ + untilMs?: number | undefined; + /** Filter months (1..12) from BYMONTH */ + byMonth?: number[] | undefined; + /** Day-of-week specifications from BYDAY with optional nth occurrences */ + byDay?: Array<{ nth?: number | undefined; day: string }> | undefined; + /** Hour filters (0..23) from BYHOUR */ + byHour?: number[] | undefined; + /** Minute filters (0..59) from BYMINUTE */ + byMinute?: number[] | undefined; + /** Set position selectors from BYSETPOS */ + bySetPos?: number[] | undefined; +} + +/** + * Parses an RFC 5545 RRULE string into a structured {@link ParsedRRule} object. + * + * @param rrule - The raw RFC 5545 RRULE string to parse + * @returns Structured representation of the recurrence parameters + */ +export function parseRRule(rrule: string): ParsedRRule { + const cleanRRule = rrule.trim().replace(RE_RRULE_PREFIX, ''); + const parts = cleanRRule.split(';'); + let freq = 'DAILY'; + let interval = 1; + let count: number | undefined; + let untilMs: number | undefined; + let byMonth: number[] | undefined; + let byDay: Array<{ nth?: number | undefined; day: string }> | undefined; + let byHour: number[] | undefined; + let byMinute: number[] | undefined; + let bySetPos: number[] | undefined; + + for (const part of parts) { + const [key, val] = part.split('='); + if (!key || !val) continue; + const k = key.toUpperCase(); + const trimmedVal = val.trim(); + + switch (k) { + case 'FREQ': + freq = trimmedVal.toUpperCase(); + break; + case 'INTERVAL': { + const parsed = parseInt(trimmedVal, 10); + interval = !isNaN(parsed) && parsed > 0 ? parsed : 1; + break; + } + case 'COUNT': { + const parsed = parseInt(trimmedVal, 10); + count = !isNaN(parsed) && parsed > 0 ? parsed : undefined; + break; + } + case 'UNTIL': { + if (RE_DATE_8DIGIT.test(trimmedVal)) { + const year = parseInt(trimmedVal.slice(0, 4), 10); + const month = parseInt(trimmedVal.slice(4, 6), 10); + const day = parseInt(trimmedVal.slice(6, 8), 10); + if (isValidDate(year, month, day)) { + untilMs = fromUtcParts({ year, month, day, hours: 23, minutes: 59, seconds: 59, milliseconds: 999 }).getTime(); + } + } else { + const uStr = trimmedVal.replace(RE_UNTIL_TIMESTAMP, '$1-$2-$3T$4:$5:$6Z'); + const parsedDate = new Date(uStr); + untilMs = !isNaN(parsedDate.getTime()) ? parsedDate.getTime() : undefined; + } + break; + } + case 'BYMONTH': { + const items = trimmedVal.split(',').map(v => { + const trimmed = v.trim(); + const num = parseInt(trimmed, 10); + if (!isNaN(num) && num >= 1 && num <= 12) return num; + const prefix = trimmed.slice(0, 3).toUpperCase(); + return prefix in MONTH_MAP ? MONTH_MAP[prefix as MonthKey] : undefined; + }).filter((v): v is number => isDefined(v)); + if (items.length > 0) byMonth = items; + break; + } + case 'BYDAY': { + const items = trimmedVal.split(',').map(item => { + const m = item.match(RE_BYDAY_PART); + const nthVal = m && m[1] ? parseInt(m[1], 10) : undefined; + const rawDay = m ? m[2] : item; + const canonicalDay = rawDay.slice(0, 2).toUpperCase(); + return { nth: isDefined(nthVal) && !isNaN(nthVal) ? nthVal : undefined, day: canonicalDay }; + }).filter(d => d.day in DAY_MAP); + if (items.length > 0) byDay = items; + break; + } + case 'BYHOUR': { + const items = trimmedVal.split(',').map(v => parseInt(v, 10)).filter(v => !isNaN(v) && v >= 0 && v <= 23); + if (items.length > 0) byHour = items; + break; + } + case 'BYMINUTE': { + const items = trimmedVal.split(',').map(v => parseInt(v, 10)).filter(v => !isNaN(v) && v >= 0 && v <= 59); + if (items.length > 0) byMinute = items; + break; + } + case 'BYSETPOS': { + const items = trimmedVal.split(',').map(v => parseInt(v, 10)).filter(v => !isNaN(v)); + if (items.length > 0) bySetPos = items; + break; + } + default: + // Safely ignore unrecognized or extra RRULE parameters (e.g. WKST, EXDATE) + break; + } + } + + return { freq, interval, count, untilMs, byMonth, byDay, byHour, byMinute, bySetPos }; +} + +/** + * Expands occurrences of an RFC 5545 RRULE string into epoch millisecond numbers. + * Pure function operating strictly on primitive timestamps and UTC Date calculations. + * + * @param rruleStr - The RFC 5545 recurrence rule string to expand + * @param anchorEpochMs - Anchor timestamp in epoch milliseconds to evaluate occurrences from + * @param options - Optional bounds and count controls for the evaluation + * @returns Array of occurrence timestamps in epoch milliseconds + */ +export function expandRRuleEpochs( + rruleStr: string, + anchorEpochMs: number, + options?: { count?: number | undefined; afterMs?: number | undefined; beforeMs?: number | undefined } +): number[] { + const rule = parseRRule(rruleStr); + const anchorDate = new Date(anchorEpochMs); + const results: number[] = []; + const maxToFetch = isDefined(rule.count) && isDefined(options?.count) + ? Math.min(rule.count, options.count) + : (rule.count ?? options?.count ?? 100); + + let totalGeneratedFromAnchor = 0; + let resultsCount = 0; + let step = 0; + const MAX_STEPS = 1000; + + const { + year: anchorYear, + month: anchorMonth, + day: anchorDay, + hours: anchorHours, + minutes: anchorMinutes, + seconds: anchorSeconds, + milliseconds: anchorMs, + } = getUtcParts(anchorDate); + + while (resultsCount < maxToFetch && step < MAX_STEPS) { + let periodBases: Date[] = []; + + switch (rule.freq) { + case 'WEEKLY': { + const steppedDate = addUtcDays(anchorDate, step * rule.interval * DAYS_IN_WEEK); + if (rule.byDay && rule.byDay.length > 0) { + periodBases = rule.byDay.map(bd => { + const targetDay = bd.day in DAY_MAP ? DAY_MAP[bd.day as DayKey] : 1; + const currentDow = getUtcParts(steppedDate).weekday; + const diff = (targetDay - currentDow + DAYS_IN_WEEK) % DAYS_IN_WEEK; + return addUtcDays(steppedDate, diff); + }); + } else { + periodBases = [steppedDate]; + } + break; + } + + case 'MONTHLY': { + const totalMonths = (anchorYear * 12 + (anchorMonth - 1)) + step * rule.interval; + const targetYear = Math.floor(totalMonths / 12); + const targetMonth = (totalMonths % 12) + 1; + + if (rule.byDay && rule.byDay.length > 0) { + const daysInMonth = getDaysInMonth(targetYear, targetMonth); + const candidateDays: Date[] = []; + for (const bd of rule.byDay) { + const targetDow = bd.day in DAY_MAP ? DAY_MAP[bd.day as DayKey] : 1; + const matchingDates: Date[] = []; + for (let dayNum = 1; dayNum <= daysInMonth; dayNum++) { + const d = fromUtcParts({ + year: targetYear, + month: targetMonth, + day: dayNum, + hours: anchorHours, + minutes: anchorMinutes, + seconds: anchorSeconds, + milliseconds: anchorMs, + }); + const dow = getUtcParts(d).weekday; + if (dow === targetDow) matchingDates.push(d); + } + + if (isDefined(bd.nth)) { + if (bd.nth > 0 && bd.nth <= matchingDates.length) { + candidateDays.push(matchingDates[bd.nth - 1]); + } else if (bd.nth < 0 && Math.abs(bd.nth) <= matchingDates.length) { + candidateDays.push(matchingDates[matchingDates.length + bd.nth]); + } + } else { + candidateDays.push(...matchingDates); + } + } + periodBases = candidateDays; + } else { + if (isValidDate(targetYear, targetMonth, anchorDay)) { + periodBases = [fromUtcParts({ + year: targetYear, + month: targetMonth, + day: anchorDay, + hours: anchorHours, + minutes: anchorMinutes, + seconds: anchorSeconds, + milliseconds: anchorMs, + })]; + } + } + break; + } + + case 'YEARLY': { + const targetYear = anchorYear + step * rule.interval; + const months = rule.byMonth && rule.byMonth.length > 0 ? rule.byMonth : [anchorMonth]; + const candidateDays: Date[] = []; + + for (const m of months) { + if (rule.byDay && rule.byDay.length > 0) { + const daysInMonth = getDaysInMonth(targetYear, m); + for (const bd of rule.byDay) { + const targetDow = bd.day in DAY_MAP ? DAY_MAP[bd.day as DayKey] : 1; + const matchingDates: Date[] = []; + for (let dayNum = 1; dayNum <= daysInMonth; dayNum++) { + const d = fromUtcParts({ + year: targetYear, + month: m, + day: dayNum, + hours: anchorHours, + minutes: anchorMinutes, + seconds: anchorSeconds, + milliseconds: anchorMs, + }); + const dow = getUtcParts(d).weekday; + if (dow === targetDow) matchingDates.push(d); + } + + if (isDefined(bd.nth)) { + if (bd.nth > 0 && bd.nth <= matchingDates.length) { + candidateDays.push(matchingDates[bd.nth - 1]); + } else if (bd.nth < 0 && Math.abs(bd.nth) <= matchingDates.length) { + candidateDays.push(matchingDates[matchingDates.length + bd.nth]); + } + } else { + candidateDays.push(...matchingDates); + } + } + } else { + if (isValidDate(targetYear, m, anchorDay)) { + candidateDays.push(fromUtcParts({ + year: targetYear, + month: m, + day: anchorDay, + hours: anchorHours, + minutes: anchorMinutes, + seconds: anchorSeconds, + milliseconds: anchorMs, + })); + } + } + } + periodBases = candidateDays; + break; + } + + case 'DAILY': + default: { + periodBases = [addUtcDays(anchorDate, step * rule.interval)]; + break; + } + } + + if (rule.byMonth && rule.byMonth.length > 0 && rule.freq !== 'YEARLY') + periodBases = periodBases.filter(b => rule.byMonth!.includes(b.getUTCMonth() + 1)); + + const periodCandidates: Date[] = []; + for (const base of periodBases) { + const { hours: baseHours, minutes: baseMinutes } = getUtcParts(base); + const hours = rule.byHour && rule.byHour.length > 0 ? rule.byHour : [baseHours]; + const minutes = rule.byMinute && rule.byMinute.length > 0 ? rule.byMinute : [baseMinutes]; + + for (const h of hours) { + for (const m of minutes) { + periodCandidates.push(withUtcParts(base, { + hours: h, + minutes: m, + seconds: anchorSeconds, + milliseconds: anchorMs, + })); + } + } + } + + periodCandidates.sort((a, b) => a.getTime() - b.getTime()); + + let finalPeriodCandidates = periodCandidates; + if (rule.bySetPos && rule.bySetPos.length > 0 && periodCandidates.length > 0) { + finalPeriodCandidates = []; + for (const pos of rule.bySetPos) { + if (pos > 0 && pos <= periodCandidates.length) { + finalPeriodCandidates.push(periodCandidates[pos - 1]); + } else if (pos < 0 && Math.abs(pos) <= periodCandidates.length) { + finalPeriodCandidates.push(periodCandidates[periodCandidates.length + pos]); + } + } + } + + step++; + + let stopSeries = false; + for (const cand of finalPeriodCandidates) { + totalGeneratedFromAnchor++; + const candMs = cand.getTime(); + + if (isDefined(rule.untilMs) && candMs > rule.untilMs) { + stopSeries = true; + break; + } + if (isDefined(rule.count) && totalGeneratedFromAnchor > rule.count) { + stopSeries = true; + break; + } + if (isDefined(options?.beforeMs) && candMs > options.beforeMs) { + stopSeries = true; + break; + } + if (isDefined(options?.afterMs) && candMs <= options.afterMs) { + continue; + } + + results.push(candMs); + resultsCount++; + + if (resultsCount >= maxToFetch) { + stopSeries = true; + break; + } + } + + if (stopSeries) break; + } + + return results; +} + +/** + * Computes the single next RRULE occurrence epoch millisecond timestamp after `fromEpochMs`. + * + * @param rruleStr - The RFC 5545 recurrence rule string + * @param fromEpochMs - The baseline timestamp in epoch milliseconds + * @returns Epoch millisecond timestamp of the next occurrence, or null if no further occurrences exist + */ +export function getNextRRuleEpoch(rruleStr: string, fromEpochMs: number): number | null { + const expanded = expandRRuleEpochs(rruleStr, fromEpochMs, { count: 1, afterMs: fromEpochMs }); + return expanded.length > 0 ? expanded[0] : null; +} diff --git a/packages/library/src/common/request.library.ts b/packages/library/src/common/request.library.ts index 09d94b66..963de46d 100644 --- a/packages/library/src/common/request.library.ts +++ b/packages/library/src/common/request.library.ts @@ -36,6 +36,8 @@ export class HttpError extends Error { } } +const RE_TRAILING_CLOSURE = /\);?$/; + /** * Performs an HTTP fetch request with built-in timeout, JSON parsing, and custom prefix handling. * Automatically throws an `HttpError` if the response is not `ok`. @@ -66,7 +68,7 @@ export const fetchRequest = (url: string | URL, init = {} as RequestInit, con if (config.prefix) { const rawPrefixText = await res.text(); // read raw text first const json = rawPrefixText.startsWith(config.prefix) // if it starts with the specified prefix - ? rawPrefixText.substring(config.prefix.length).replace(/\);?$/, '') // then strip the prefix AND any trailing closure + ? rawPrefixText.substring(config.prefix.length).replace(RE_TRAILING_CLOSURE, '') // then strip the prefix AND any trailing closure : rawPrefixText; return JSON.parse(json) as T; // parse the unwrapped string diff --git a/packages/library/src/common/serialize.library.ts b/packages/library/src/common/serialize.library.ts index 96c402c8..178d80a9 100644 --- a/packages/library/src/common/serialize.library.ts +++ b/packages/library/src/common/serialize.library.ts @@ -101,20 +101,18 @@ function replacer(key: string, obj: any): any { return isEmpty(key) ? obj : stri function reviver(_key: string, val: any): any { return decode(val) } // safe-characters [sp " ; < > [ ] ^ { | }] -const safeList = ['20', '22', '3B', '3C', '3E', '5B', '5D', '5E', '7B', '7C', '7D']; +const SAFE_URI_MAP: Record = { + '%20': ' ', '%22': '"', '%3B': ';', '%3C': '<', '%3E': '>', + '%5B': '[', '%5D': ']', '%5E': '^', '%7B': '{', '%7C': '|', '%7D': '}', +} as const; +const RE_SAFE_URI_CODES = new RegExp(Object.keys(SAFE_URI_MAP).join('|'), 'gi'); /** encode control characters, then replace a safe-subset back to text-string */ function encode(val: string) { let enc = encodeURI(val); - if (enc.includes('%')) { // if an encoded URI might be in string - safeList.forEach(code => { - const uri = '%' + code; - const reg = new RegExp(uri, 'g'); - - enc = enc.replace(reg, decodeURI(uri)); - }) - } + if (enc.includes('%')) + enc = enc.replace(RE_SAFE_URI_CODES, match => SAFE_URI_MAP[match.toUpperCase()] ?? decodeURI(match)); return enc; } diff --git a/packages/library/src/common/string.library.ts b/packages/library/src/common/string.library.ts index f2338322..e1afa486 100644 --- a/packages/library/src/common/string.library.ts +++ b/packages/library/src/common/string.library.ts @@ -1,8 +1,16 @@ import { asNumber, asString, nullishToValue } from '#library/coercion.library.js'; import { stringify } from '#library/serialize.library.js'; import { isString, isObject, isNumeric, assertCondition, assertString } from '#library/assertion.library.js'; +import type { SingularUnit } from '#library/type.library.js'; -// General functions +const RE_TAB = /\t/g; +const RE_NEWLINE = /(\r\n|\n|\r)/g; +const RE_MULTI_SPACE = /\s{2,}/g; +const RE_WORD_START = /\w\S*/g; +const RE_SPACE = / /g; +const RE_PARAM_MARKER = /\$\{(\d+)\}/g; +const RE_FORMAT_SPECIFIER = /%[sj]/g; +const RE_TEMPLATE_PLACEHOLDER = /\${(.*?)}/g; // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // This section needs to be Function declarations so that they are hoisted @@ -24,9 +32,9 @@ export function trimAll(str: string | number, pat?: RegExp) { return str .toString() // coerce to String .replace(pat!, '') // remove regexp, if supplied - .replace(/\t/g, ' ') // replace with - .replace(/(\r\n|\n|\r)/g, ' ') // replace & - .replace(/\s{2,}/g, ' ') // trim multiple + .replace(RE_TAB, ' ') // replace with + .replace(RE_NEWLINE, ' ') // replace & + .replace(RE_MULTI_SPACE, ' ') // trim multiple .trim() // leading/trailing } @@ -43,7 +51,7 @@ export function trimAll(str: string | number, pat?: RegExp) { export function toProperCase(...str: T[]) { return str .flat() // in case {str} was already an array - .map(text => text.replace(/\w\S*/g, + .map(text => text.replace(RE_WORD_START, word => word.charAt(0).toUpperCase() + word.substring(1).toLowerCase())) .join(' ') as T } @@ -89,7 +97,7 @@ export const toCamelCase = (sentence: T) => { rest.splice(0, 1); } - return (sentence.startsWith('_') ? '_' : '') + word.toLocaleLowerCase() + toProperCase(...rest).replace(/ /g, '') as T; + return (sentence.startsWith('_') ? '_' : '') + word.toLocaleLowerCase() + toProperCase(...rest).replace(RE_SPACE, '') as T; } const HEX = 16; @@ -125,29 +133,26 @@ export const randomString = (len = 36) => { * sprintf('Hello %s', 'World'); // 'Hello World' * ``` */ -export function sprintf(fmt: string, ...msg: any[]): string; // either a format-string, followed by arguments -export function sprintf(...msg: any[]): string; // or just an array of arguments +export function sprintf(fmt: string, ...msg: any[]): string;// either a format-string, followed by arguments +export function sprintf(...msg: any[]): string; // or just an array of arguments export function sprintf(fmt: {}, ...msg: any[]) { - const regexp = /\$\{(\d)\}/g; // pattern to find "${digit}" parameter markers - let sfmt = asString(fmt); // avoid mutate fmt + let sfmt = isString(fmt) ? (fmt as string) : ''; - if (!isString(fmt)) { // might be an Object - msg.unshift(JSON.stringify(fmt)); // push to start of msg[] - sfmt = ''; // reset the string-format - } + if (!isString(fmt)) // might be an Object + msg.unshift(JSON.stringify(fmt)); // push to start of msg[] let cnt = 0; // if the format does not contain a corresponding '${digit}' then re-construct the parameters - sfmt = sfmt.replace(/%[sj]/g, _ => `\${${cnt++}}`); // flip all the %s or %j to a ${digit} parameter + sfmt = sfmt.replace(RE_FORMAT_SPECIFIER, _ => `\${${cnt++}}`); // flip all the %s or %j to a ${digit} parameter - const params = Array.from(sfmt.matchAll(regexp)) - .map(match => Number(match[1])) // which parameters are in the fmt + const params = Array.from(sfmt.matchAll(RE_PARAM_MARKER)) + .map(match => Number(match[1])) // which parameters are in the fmt msg.forEach((_, idx) => { if (!params.includes(idx)) // if more args than params sfmt += `${sfmt.length === 0 ? '' : sfmt.endsWith(':') ? ' ' : ', '}\${${idx}}` // append a dummy params to fmt }) // 2024-02-21 some Objects do not have a .toString method - return sfmt.replace(regexp, (_, idx) => msg[idx]?.toString?.() || stringify(msg[idx])); + return sfmt.replace(RE_PARAM_MARKER, (_, idx) => msg[idx]?.toString?.() || stringify(msg[idx])); } /** @@ -171,12 +176,6 @@ export const plural = (val: string | number | Record, word: stri : _plural(val, word, plural) } -type SingularUnit = T extends `${infer S}s` - ? T extends `${string}${string}${string}${string}` - ? S - : T - : T; - /** * Strips a plural 's' suffix from a string if it ends with 's' and is longer than 3 characters. * @@ -207,7 +206,7 @@ export const singular = (val: T): SingularUnit => */ export const makeTemplate = (templateString: any) => (templateData: any) => - String(templateString).replace(/\${(.*?)}/g, (_, key) => { + String(templateString).replace(RE_TEMPLATE_PLACEHOLDER, (_, key) => { const val = (templateData as any)[key.trim()]; return val !== undefined ? String(val) : ''; }); diff --git a/packages/library/src/common/temporal.library.ts b/packages/library/src/common/temporal.library.ts index ddc8cd95..9ac0ca62 100644 --- a/packages/library/src/common/temporal.library.ts +++ b/packages/library/src/common/temporal.library.ts @@ -112,6 +112,13 @@ export function normaliseFractionalDurations(payload: Record) { // import a polyfill directly. // ───────────────────────────────────────────────── +const RE_ISO_DATE_TIME_SPACE = /^(\d{4}-\d{2}-\d{2})\s+(?=\d{2}:\d{2})/; +const RE_SPACE_BEFORE_ZONE = /\s+(?=[Zz]|[+-]\d{2}|\[)/; +const RE_CALENDAR_BRACKET = /\[!?u-ca=[^\]]+\]/i; +const RE_TZ_BRACKET = /\[(?!!?u-ca=)[^\]]+\]/i; +const RE_OFFSET_SUFFIX = /Z$|[+-]\d{2}(:?\d{2})?$/i; +const RE_UTC_OFFSET = /^UTC([+-])(\d{1,2})(?::(\d{2}))?$/i; + /** * Creates a `Temporal.ZonedDateTime` from a property-bag or ISO string. * Automatically injects the specified timezone if missing from the string. @@ -121,12 +128,23 @@ export function normaliseFractionalDurations(payload: Record) { * @returns The created Temporal.ZonedDateTime */ export function toZonedDateTime(bag: Temporal.ZonedDateTimeLike | string, tz: Temporal.TimeZoneLike = 'UTC'): Temporal.ZonedDateTime { - if (isString(bag)) { - // Detect existing zone designator: bracketed IANA zone ([...]) or numeric offset (±HH:MM, Z) - const hasZone = /\[[^\]]+\]|([+-]\d{2}(:?\d{2})?|Z)$/.test(bag); - return Temporal.ZonedDateTime.from(hasZone ? bag : `${bag}[${tz}]`); - } - return Temporal.ZonedDateTime.from(bag); + if (!isString(bag)) + return Temporal.ZonedDateTime.from(bag); + + const str = bag + .trim() + .replace(RE_ISO_DATE_TIME_SPACE, '$1T') + .replace(RE_SPACE_BEFORE_ZONE, ''); + + if (RE_TZ_BRACKET.test(str)) + return Temporal.ZonedDateTime.from(str); + + if (RE_CALENDAR_BRACKET.test(str)) + return Temporal.ZonedDateTime.from(str.replace(RE_CALENDAR_BRACKET, `[${tz}]$&`)); + + return (str.includes('T') && RE_OFFSET_SUFFIX.test(str)) + ? Temporal.Instant.from(str).toZonedDateTimeISO(tz) + : Temporal.ZonedDateTime.from(`${str}[${tz}]`); } /** @@ -201,7 +219,7 @@ export function getTemporalIds(tzOrZdt: any, cal?: any): [string, string] { * @returns The normalized offset string */ export function normalizeUtcOffset(zone: string): string { - const match = /^UTC([+-])(\d{1,2})(?::(\d{2}))?$/i.exec(zone); + const match = RE_UTC_OFFSET.exec(zone); if (!match) return zone; const [, sign, hours, minutes] = match; diff --git a/packages/library/src/common/type.library.ts b/packages/library/src/common/type.library.ts index 08c10bd2..93b43411 100644 --- a/packages/library/src/common/type.library.ts +++ b/packages/library/src/common/type.library.ts @@ -198,6 +198,15 @@ export type OneKey = export type Prettify = { [K in keyof T]: T[K]; } & {} export type ParseInt = T extends `${infer N extends number}` ? N : never export type Plural = `${T}s`; +export type Singular = T extends `${infer S}s` + ? T extends MinLength + ? S + : T + : T; +export type SingularUnit = Singular; + +// Compile-time test cases to verify singular behavior +export type AssertEqual = [A] extends [B] ? ([B] extends [A] ? true : false) : false; type toName = T extends string ? "String" : diff --git a/packages/library/src/common/webtoken.library.ts b/packages/library/src/common/webtoken.library.ts index 2b9fef57..fe5eba3c 100644 --- a/packages/library/src/common/webtoken.library.ts +++ b/packages/library/src/common/webtoken.library.ts @@ -4,7 +4,16 @@ import { keys } from './cipher.library.js'; const logger = new Logger('WebToken'); -const formatBase64Url = (base64: string) => base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, ''); +const RE_PLUS = /\+/g; +const RE_SLASH = /\//g; +const RE_EQUALS = /=/g; +const RE_DASH = /-/g; +const RE_UNDERSCORE = /_/g; + +const formatBase64Url = (base64: string) => base64 + .replace(RE_PLUS, '-') + .replace(RE_SLASH, '_') + .replace(RE_EQUALS, ''); const toBase64Url = (str: string) => formatBase64Url(bufferToBase64(encodeBuffer(str))); const bufToBase64Url = (buf: Uint8Array) => formatBase64Url(bufferToBase64(buf)); @@ -27,7 +36,10 @@ export const decodeJWT = (jwt: string): T | null => { if (!part) return null; // 🛡️ Base64URL Normalization: replace -/_ with +/ and add padding - const base64 = part.replace(/-/g, '+').replace(/_/g, '/').padEnd(part.length + (4 - part.length % 4) % 4, '='); + const base64 = part + .replace(RE_DASH, '+') + .replace(RE_UNDERSCORE, '/') + .padEnd(part.length + (4 - part.length % 4) % 4, '='); const bytes = base64ToBuffer(base64); const payload = decodeBuffer(bytes); @@ -52,8 +64,8 @@ export const verifyJWS = async (token: string, publicKey: CryptoKey): Promise { + test('DAYS_IN_WEEK constant is 7', () => { + expect(DAYS_IN_WEEK).toBe(7); + }); + + test('DAY_MAP maps 2-letter and 3-letter weekday abbreviations to ISO 1..7 indices', () => { + expect(DAY_MAP.MO).toBe(1); + expect(DAY_MAP.TU).toBe(2); + expect(DAY_MAP.WE).toBe(3); + expect(DAY_MAP.TH).toBe(4); + expect(DAY_MAP.FR).toBe(5); + expect(DAY_MAP.SA).toBe(6); + expect(DAY_MAP.SU).toBe(7); + + expect(DAY_MAP.MON).toBe(1); + expect(DAY_MAP.TUE).toBe(2); + expect(DAY_MAP.WED).toBe(3); + expect(DAY_MAP.THU).toBe(4); + expect(DAY_MAP.FRI).toBe(5); + expect(DAY_MAP.SAT).toBe(6); + expect(DAY_MAP.SUN).toBe(7); + + // Type assertions + const sampleDayKey: DayKey = 'MON'; + const sampleDayVal: DayValue = DAY_MAP[sampleDayKey]; + expect(sampleDayVal).toBe(1); + }); + + test('MONTH_MAP maps 3-letter month abbreviations to 1..12 indices', () => { + expect(MONTH_MAP.JAN).toBe(1); + expect(MONTH_MAP.FEB).toBe(2); + expect(MONTH_MAP.MAR).toBe(3); + expect(MONTH_MAP.APR).toBe(4); + expect(MONTH_MAP.MAY).toBe(5); + expect(MONTH_MAP.JUN).toBe(6); + expect(MONTH_MAP.JUL).toBe(7); + expect(MONTH_MAP.AUG).toBe(8); + expect(MONTH_MAP.SEP).toBe(9); + expect(MONTH_MAP.OCT).toBe(10); + expect(MONTH_MAP.NOV).toBe(11); + expect(MONTH_MAP.DEC).toBe(12); + + // Type assertions + const sampleMonthKey: MonthKey = 'AUG'; + const sampleMonthVal: MonthValue = MONTH_MAP[sampleMonthKey]; + expect(sampleMonthVal).toBe(8); + }); + + test('ISO_WEEKDAY_NAMES maps ISO indices 1..7 to full English weekday names', () => { + expect(ISO_WEEKDAY_NAMES[1]).toBe('Monday'); + expect(ISO_WEEKDAY_NAMES[2]).toBe('Tuesday'); + expect(ISO_WEEKDAY_NAMES[3]).toBe('Wednesday'); + expect(ISO_WEEKDAY_NAMES[4]).toBe('Thursday'); + expect(ISO_WEEKDAY_NAMES[5]).toBe('Friday'); + expect(ISO_WEEKDAY_NAMES[6]).toBe('Saturday'); + expect(ISO_WEEKDAY_NAMES[7]).toBe('Sunday'); + + // Type assertions + const sampleNum: IsoWeekdayNumber = 7; + const sampleName: IsoWeekdayName = ISO_WEEKDAY_NAMES[sampleNum]; + expect(sampleName).toBe('Sunday'); + }); + + test('getUtcParts extracts all components with 1-indexed month and weekday', () => { + // Sunday 2026-08-09T14:35:45.123Z + const date = new Date(Date.UTC(2026, 7, 9, 14, 35, 45, 123)); + const parts = getUtcParts(date); + + expect(parts.year).toBe(2026); + expect(parts.month).toBe(8); + expect(parts.day).toBe(9); + expect(parts.weekday).toBe(7); + expect(parts.hours).toBe(14); + expect(parts.minutes).toBe(35); + expect(parts.seconds).toBe(45); + expect(parts.milliseconds).toBe(123); + }); + + test('getUtcParts defaults to current date when no argument is provided', () => { + const now = new Date(); + const parts = getUtcParts(); + + expect(parts.year).toBe(now.getUTCFullYear()); + expect(parts.month).toBe(now.getUTCMonth() + 1); + expect(parts.day).toBe(now.getUTCDate()); + expect(parts.weekday).toBeGreaterThanOrEqual(1); + expect(parts.weekday).toBeLessThanOrEqual(7); + expect(typeof parts.hours).toBe('number'); + expect(typeof parts.minutes).toBe('number'); + expect(typeof parts.seconds).toBe('number'); + expect(typeof parts.milliseconds).toBe('number'); + }); + + test('getUtcParts.weekday converts JS Date Sunday (0) to ISO Sunday (7)', () => { + // Sunday Aug 9, 2026 + const sunday = new Date(Date.UTC(2026, 7, 9)); + expect(sunday.getUTCDay()).toBe(0); + expect(getUtcParts(sunday).weekday).toBe(7); + + // Monday Aug 10, 2026 + const monday = new Date(Date.UTC(2026, 7, 10)); + expect(monday.getUTCDay()).toBe(1); + expect(getUtcParts(monday).weekday).toBe(1); + + // Friday Aug 7, 2026 + const friday = new Date(Date.UTC(2026, 7, 7)); + expect(friday.getUTCDay()).toBe(5); + expect(getUtcParts(friday).weekday).toBe(5); + + // Saturday Aug 8, 2026 + const saturday = new Date(Date.UTC(2026, 7, 8)); + expect(saturday.getUTCDay()).toBe(6); + expect(getUtcParts(saturday).weekday).toBe(6); + }); + + test('getDaysInMonth returns correct days for each month including leap year February', () => { + // Non-leap year 2026 + expect(getDaysInMonth(2026, 1)).toBe(31); // Jan + expect(getDaysInMonth(2026, 2)).toBe(28); // Feb + expect(getDaysInMonth(2026, 3)).toBe(31); // Mar + expect(getDaysInMonth(2026, 4)).toBe(30); // Apr + expect(getDaysInMonth(2026, 5)).toBe(31); // May + expect(getDaysInMonth(2026, 6)).toBe(30); // Jun + expect(getDaysInMonth(2026, 7)).toBe(31); // Jul + expect(getDaysInMonth(2026, 8)).toBe(31); // Aug + expect(getDaysInMonth(2026, 9)).toBe(30); // Sep + expect(getDaysInMonth(2026, 10)).toBe(31); // Oct + expect(getDaysInMonth(2026, 11)).toBe(30); // Nov + expect(getDaysInMonth(2026, 12)).toBe(31); // Dec + + // Leap year 2024 + expect(getDaysInMonth(2024, 2)).toBe(29); // Feb leap + }); + + test('isLeapYear correctly determines leap years', () => { + expect(isLeapYear(2024)).toBe(true); + expect(isLeapYear(2000)).toBe(true); + expect(isLeapYear(2026)).toBe(false); + expect(isLeapYear(1900)).toBe(false); + expect(isLeapYear(2100)).toBe(false); + }); + + test('fromUtcParts creates Date instances with 1-indexed month and clock values', () => { + const options: UtcPartsOptions = { + year: 2026, + month: 8, + day: 9, + hours: 10, + minutes: 30, + seconds: 45, + milliseconds: 500, + } + const date1 = fromUtcParts(options); + expect(date1.toISOString()).toBe('2026-08-09T10:30:45.500Z'); + + // Default clock values are 0 + const date2 = fromUtcParts({ year: 2026, month: 1, day: 15 }); + expect(date2.toISOString()).toBe('2026-01-15T00:00:00.000Z'); + + // Round-trip symmetry with getUtcParts + const original = new Date('2026-11-25T17:45:30.250Z'); + const parts = getUtcParts(original); + const roundtrip = fromUtcParts(parts); + expect(roundtrip.getTime()).toBe(original.getTime()); + }); + + test('isValidDate checks calendar validity without date allocation or overflow', () => { + expect(isValidDate(2026, 1, 31)).toBe(true); + expect(isValidDate(2026, 2, 28)).toBe(true); + expect(isValidDate(2026, 2, 29)).toBe(false); // Non-leap year + expect(isValidDate(2024, 2, 29)).toBe(true); // Leap year + expect(isValidDate(2026, 4, 30)).toBe(true); + expect(isValidDate(2026, 4, 31)).toBe(false); // April only has 30 days + expect(isValidDate(2026, 0, 15)).toBe(false); // Month < 1 + expect(isValidDate(2026, 13, 15)).toBe(false); // Month > 12 + expect(isValidDate(2026, 5, 0)).toBe(false); // Day < 1 + }); + + test('addUtcDays adds or subtracts days without mutating original date', () => { + const base = new Date('2026-02-28T12:00:00.000Z'); + const next = addUtcDays(base, 1); + expect(next.toISOString()).toBe('2026-03-01T12:00:00.000Z'); + expect(base.toISOString()).toBe('2026-02-28T12:00:00.000Z'); // Unmutated + + const prev = addUtcDays(base, -7); + expect(prev.toISOString()).toBe('2026-02-21T12:00:00.000Z'); + }); + + test('withUtcParts updates specific calendar or clock parts while preserving others', () => { + const base = new Date('2026-05-15T14:30:45.123Z'); + + // Date update + const updatedDate = withUtcParts(base, { month: 10, day: 31 }); + expect(updatedDate.toISOString()).toBe('2026-10-31T14:30:45.123Z'); + expect(base.toISOString()).toBe('2026-05-15T14:30:45.123Z'); // Unmutated + + // Clock update + const updatedTime = withUtcParts(base, { hours: 9, minutes: 0 }); + expect(updatedTime.toISOString()).toBe('2026-05-15T09:00:45.123Z'); + + // Combined update + const fullUpdate = withUtcParts(base, { year: 2028, hours: 23, minutes: 59, seconds: 59, milliseconds: 999 }); + expect(fullUpdate.toISOString()).toBe('2028-05-15T23:59:59.999Z'); + }); +}); diff --git a/packages/library/test/common/decorator.immutable-secure.test.ts b/packages/library/test/common/class.library.test.ts similarity index 100% rename from packages/library/test/common/decorator.immutable-secure.test.ts rename to packages/library/test/common/class.library.test.ts diff --git a/packages/library/test/common/coercion.library.test.ts b/packages/library/test/common/coercion.library.test.ts new file mode 100644 index 00000000..f4ea8e9d --- /dev/null +++ b/packages/library/test/common/coercion.library.test.ts @@ -0,0 +1,44 @@ +import { ifNumeric, asInteger } from '../../src/common/coercion.library.js'; + +describe('Coercion Library', () => { + describe('ifNumeric', () => { + it('should parse signed integer strings beyond max safe integer precisely as bigint', () => { + expect(ifNumeric('+9007199254740993')).toBe(9007199254740993n); + expect(ifNumeric('-9007199254740993')).toBe(-9007199254740993n); + }); + + it('should parse padded equivalents and trim strings correctly', () => { + expect(ifNumeric(' +9007199254740993 ')).toBe(9007199254740993n); + expect(ifNumeric(' +123n ')).toBe(123); + expect(ifNumeric(' -123n ')).toBe(-123); + expect(ifNumeric(' +123 ')).toBe(123); + expect(ifNumeric(' -123 ')).toBe(-123); + }); + + it('should handle zero-padded strings correctly depending on stripZero', () => { + expect(ifNumeric(' 012 ')).toBe(' 012 '); + expect(ifNumeric(' 012 ', true)).toBe(12); + }); + + it('should handle NaN and infinities by passing through', () => { + expect(ifNumeric(NaN)).toBeNaN(); + expect(ifNumeric(Infinity)).toBe(Infinity); + expect(ifNumeric(-Infinity)).toBe(-Infinity); + }); + }); + + describe('asInteger', () => { + it('should coerce strings with optional plus sign and padding to BigInt', () => { + expect(asInteger('+9007199254740993')).toBe(9007199254740993n); + expect(asInteger(' +9007199254740993n ')).toBe(9007199254740993n); + expect(asInteger(' +123 ')).toBe(123n); + expect(asInteger(' +123n ')).toBe(123n); + }); + + it('should throw SyntaxError for sign-only strings like + and -', () => { + expect(() => asInteger('+')).toThrow(SyntaxError); + expect(() => asInteger('-')).toThrow(SyntaxError); + expect(() => asInteger(' + ')).toThrow(SyntaxError); + }); + }); +}); diff --git a/packages/library/test/common/number_library.test.ts b/packages/library/test/common/number.library.test.ts similarity index 100% rename from packages/library/test/common/number_library.test.ts rename to packages/library/test/common/number.library.test.ts diff --git a/packages/library/test/common/proxy.library.test.ts b/packages/library/test/common/proxy.library.test.ts index a1b5f2ca..ca2998f9 100644 --- a/packages/library/test/common/proxy.library.test.ts +++ b/packages/library/test/common/proxy.library.test.ts @@ -45,6 +45,24 @@ describe('proxy method binding', () => { expect(proxyA.getValue()).toBe(3); expect(proxyB.getValue()).toBe(9); }); + + test('preserves original constructor identity without binding', () => { + class CustomTarget { + value = 42; + getValue() { return this.value; } + } + + const instance = new CustomTarget(); + const proxy = proxify(instance, false, false); + + expect(proxy.constructor).toBe(CustomTarget); + expect(proxy.constructor === CustomTarget).toBe(true); + + const plain = { a: 1 }; + const plainProxy = proxify(plain, false, false); + expect(plainProxy.constructor).toBe(Object); + expect(plainProxy.constructor === Object).toBe(true); + }); }); describe('delegator', () => { diff --git a/packages/library/test/common/recurrence.library.test.ts b/packages/library/test/common/recurrence.library.test.ts new file mode 100644 index 00000000..c41b6826 --- /dev/null +++ b/packages/library/test/common/recurrence.library.test.ts @@ -0,0 +1,220 @@ +import { + isRRuleString, + isFiniteRRule, + parseRRule, + expandRRuleEpochs, + getNextRRuleEpoch, +} from '../../src/common/recurrence.library.js'; +import { DAY_MAP } from '../../src/common/calendar.library.js'; + +describe('recurrence.library', () => { + test('isRRuleString identifies valid RRULE patterns', () => { + expect(isRRuleString('FREQ=DAILY')).toBe(true); + expect(isRRuleString('RRULE:FREQ=WEEKLY;BYDAY=MO')).toBe(true); + expect(isRRuleString('FREQ=MONTHLY;BYDAY=1MO,3MO')).toBe(true); + expect(isRRuleString('hello world')).toBe(false); + expect(isRRuleString('2026-08-07')).toBe(false); + }); + + test('isFiniteRRule identifies bounded vs infinite series', () => { + expect(isFiniteRRule('FREQ=DAILY;COUNT=5')).toBe(true); + expect(isFiniteRRule('FREQ=WEEKLY;UNTIL=20261231T235959Z')).toBe(true); + expect(isFiniteRRule('FREQ=MONTHLY;UNTIL=20261231')).toBe(true); + expect(isFiniteRRule('FREQ=DAILY')).toBe(false); + expect(isFiniteRRule('FREQ=WEEKLY;BYDAY=MO,WE,FR')).toBe(false); + }); + + test('DAY_MAP provides correct weekday mapping and Sunday aliases', () => { + expect(DAY_MAP.MO).toBe(1); + expect(DAY_MAP.TU).toBe(2); + expect(DAY_MAP.WE).toBe(3); + expect(DAY_MAP.TH).toBe(4); + expect(DAY_MAP.FR).toBe(5); + expect(DAY_MAP.SA).toBe(6); + expect(DAY_MAP.SU).toBe(7); + expect(DAY_MAP.SUN).toBe(7); + }); + + test('parseRRule correctly parses RRULE components', () => { + const parsed = parseRRule('FREQ=WEEKLY;INTERVAL=2;COUNT=5;BYDAY=1MO,-1FR;BYHOUR=9,17;BYMINUTE=30'); + expect(parsed.freq).toBe('WEEKLY'); + expect(parsed.interval).toBe(2); + expect(parsed.count).toBe(5); + expect(parsed.byDay).toEqual([ + { nth: 1, day: 'MO' }, + { nth: -1, day: 'FR' } + ]); + expect(parsed.byHour).toEqual([9, 17]); + expect(parsed.byMinute).toEqual([30]); + }); + + test('parseRRule correctly handles RRULE: prefix', () => { + const parsed = parseRRule('RRULE:FREQ=WEEKLY;INTERVAL=1;BYDAY=MO'); + expect(parsed.freq).toBe('WEEKLY'); + expect(parsed.interval).toBe(1); + expect(parsed.byDay).toEqual([{ nth: undefined, day: 'MO' }]); + }); + + test('parseRRule supports 2-letter, 3-letter, and full weekday names and normalizes to standard RFC 2-letter codes', () => { + const parsed = parseRRule('FREQ=WEEKLY;BYDAY=Monday,FRI,Wed,2Thursday,Sunday'); + expect(parsed.byDay).toEqual([ + { nth: undefined, day: 'MO' }, + { nth: undefined, day: 'FR' }, + { nth: undefined, day: 'WE' }, + { nth: 2, day: 'TH' }, + { nth: undefined, day: 'SU' } + ]); + }); + + test('parseRRule supports numeric, 3-letter, and full month names in BYMONTH', () => { + const parsed = parseRRule('FREQ=YEARLY;BYMONTH=1,Jan,December,AUG'); + expect(parsed.byMonth).toEqual([1, 1, 12, 8]); + }); + + test('expandRRuleEpochs generates correct daily occurrence timestamps', () => { + // 2026-08-07T00:00:00.000Z is Friday + const anchor = Date.UTC(2026, 7, 7, 0, 0, 0, 0); + const epochs = expandRRuleEpochs('FREQ=DAILY;INTERVAL=1', anchor, { count: 3 }); + + expect(epochs.length).toBe(3); + expect(new Date(epochs[0]).toISOString()).toBe('2026-08-07T00:00:00.000Z'); + expect(new Date(epochs[1]).toISOString()).toBe('2026-08-08T00:00:00.000Z'); + expect(new Date(epochs[2]).toISOString()).toBe('2026-08-09T00:00:00.000Z'); + }); + + test('expandRRuleEpochs handles Sunday BYDAY correctly for WEEKLY', () => { + // Friday, Aug 7, 2026 + const anchor = Date.UTC(2026, 7, 7, 10, 0, 0, 0); + const epochs = expandRRuleEpochs('FREQ=WEEKLY;BYDAY=SU', anchor, { count: 2 }); + + expect(epochs.length).toBe(2); + // First Sunday on or after Friday Aug 7 is Sunday Aug 9 + expect(new Date(epochs[0]).toISOString()).toBe('2026-08-09T10:00:00.000Z'); + // Next Sunday is Aug 16 + expect(new Date(epochs[1]).toISOString()).toBe('2026-08-16T10:00:00.000Z'); + }); + + test('expandRRuleEpochs handles Sunday and positive/negative nth selectors for MONTHLY', () => { + // August 2026: 1st is Saturday, 2nd is Sunday, last day is 31st (Monday), last Sunday is 30th + const anchor = Date.UTC(2026, 7, 1, 9, 0, 0, 0); + + // 1st Sunday of August 2026 -> Aug 2 + const firstSunday = expandRRuleEpochs('FREQ=MONTHLY;BYDAY=1SU', anchor, { count: 1 }); + expect(new Date(firstSunday[0]).toISOString()).toBe('2026-08-02T09:00:00.000Z'); + + // Last Sunday of August 2026 -> Aug 30 + const lastSunday = expandRRuleEpochs('FREQ=MONTHLY;BYDAY=-1SU', anchor, { count: 1 }); + expect(new Date(lastSunday[0]).toISOString()).toBe('2026-08-30T09:00:00.000Z'); + + // 2nd Tuesday of August 2026 -> Aug 11 + const secondTuesday = expandRRuleEpochs('FREQ=MONTHLY;BYDAY=2TU', anchor, { count: 1 }); + expect(new Date(secondTuesday[0]).toISOString()).toBe('2026-08-11T09:00:00.000Z'); + }); + + test('expandRRuleEpochs handles YEARLY with BYMONTH', () => { + const anchor = Date.UTC(2026, 7, 7, 12, 0, 0, 0); + const epochs = expandRRuleEpochs('FREQ=YEARLY;BYMONTH=12', anchor, { count: 2 }); + + expect(epochs.length).toBe(2); + expect(new Date(epochs[0]).toISOString()).toBe('2026-12-07T12:00:00.000Z'); + expect(new Date(epochs[1]).toISOString()).toBe('2027-12-07T12:00:00.000Z'); + + // BYMONTH=1 produces January candidates + const janEpochs = expandRRuleEpochs('FREQ=YEARLY;BYMONTH=1', anchor, { count: 2 }); + expect(janEpochs.length).toBe(2); + expect(new Date(janEpochs[0]).toISOString()).toBe('2026-01-07T12:00:00.000Z'); + expect(new Date(janEpochs[1]).toISOString()).toBe('2027-01-07T12:00:00.000Z'); + + // BYMONTH with BYDAY (e.g. 4th Thursday of November) + // November 2026 Thursdays: Nov 5, 12, 19, 26 (4th is Nov 26) + const thanksgivingEpochs = expandRRuleEpochs('FREQ=YEARLY;BYMONTH=11;BYDAY=4TH', anchor, { count: 1 }); + expect(thanksgivingEpochs.length).toBe(1); + expect(new Date(thanksgivingEpochs[0]).toISOString()).toBe('2026-11-26T12:00:00.000Z'); + }); + + test('expandRRuleEpochs terminates correctly with UNTIL and COUNT clauses', () => { + const anchor = Date.UTC(2026, 7, 1, 0, 0, 0, 0); + + // COUNT=3 + const countEpochs = expandRRuleEpochs('FREQ=DAILY;COUNT=3', anchor); + expect(countEpochs.length).toBe(3); + + // UNTIL=20260804 + const untilEpochs = expandRRuleEpochs('FREQ=DAILY;UNTIL=20260804', anchor); + expect(untilEpochs.length).toBe(4); + expect(new Date(untilEpochs[3]).toISOString()).toBe('2026-08-04T00:00:00.000Z'); + }); + + test('expandRRuleEpochs filters correctly with BYSETPOS', () => { + // August 2026 weekdays: Aug 3 (1st weekday), Aug 31 (last weekday) + const anchor = Date.UTC(2026, 7, 1, 8, 0, 0, 0); + + // 1st weekday of the month + const firstWeekday = expandRRuleEpochs('FREQ=MONTHLY;BYDAY=MO,TU,WE,TH,FR;BYSETPOS=1', anchor, { count: 1 }); + expect(new Date(firstWeekday[0]).toISOString()).toBe('2026-08-03T08:00:00.000Z'); + + // Last weekday of the month + const lastWeekday = expandRRuleEpochs('FREQ=MONTHLY;BYDAY=MO,TU,WE,TH,FR;BYSETPOS=-1', anchor, { count: 1 }); + expect(new Date(lastWeekday[0]).toISOString()).toBe('2026-08-31T08:00:00.000Z'); + }); + + test('getNextRRuleEpoch computes next occurrence including Sunday handling', () => { + const anchor = Date.UTC(2026, 7, 7, 0, 0, 0, 0); // Friday Aug 7 + const nextDaily = getNextRRuleEpoch('FREQ=DAILY;INTERVAL=1', anchor); + expect(nextDaily).not.toBeNull(); + expect(new Date(nextDaily!).toISOString()).toBe('2026-08-08T00:00:00.000Z'); + + // Next occurrence after Friday Aug 7 for Sunday rule is Sunday Aug 9 + const nextSunday = getNextRRuleEpoch('FREQ=WEEKLY;BYDAY=SU', anchor); + expect(nextSunday).not.toBeNull(); + expect(new Date(nextSunday!).toISOString()).toBe('2026-08-09T00:00:00.000Z'); + }); + + test('expandRRuleEpochs respects smaller of rule.count and options.count', () => { + const anchor = Date.UTC(2026, 7, 7, 0, 0, 0, 0); + // rule.count = 10, options.count = 2 -> yields 2 + const epochs1 = expandRRuleEpochs('FREQ=DAILY;COUNT=10', anchor, { count: 2 }); + expect(epochs1.length).toBe(2); + + // rule.count = 2, options.count = 10 -> yields 2 + const epochs2 = expandRRuleEpochs('FREQ=DAILY;COUNT=2', anchor, { count: 10 }); + expect(epochs2.length).toBe(2); + + // neither count provided -> defaults to up to 100 + const epochs3 = expandRRuleEpochs('FREQ=DAILY;INTERVAL=1', anchor); + expect(epochs3.length).toBe(100); + }); + + test('expandRRuleEpochs skips months that do not have the anchor day for MONTHLY recurrence without BYDAY', () => { + // Jan 31, 2026 -> Feb has 28 days (skipped), Mar has 31 days (included), Apr has 30 days (skipped), May has 31 days (included) + const anchor = Date.UTC(2026, 0, 31, 10, 0, 0, 0); + const epochs = expandRRuleEpochs('FREQ=MONTHLY;INTERVAL=1', anchor, { count: 3 }); + + expect(epochs.length).toBe(3); + expect(new Date(epochs[0]).toISOString()).toBe('2026-01-31T10:00:00.000Z'); + expect(new Date(epochs[1]).toISOString()).toBe('2026-03-31T10:00:00.000Z'); + expect(new Date(epochs[2]).toISOString()).toBe('2026-05-31T10:00:00.000Z'); + }); + + test('isRRuleString and isFiniteRRule strictly validate token boundaries', () => { + expect(isRRuleString('FREQ=DAILYXYZ')).toBe(false); + expect(isRRuleString('FREQ=DAILY;INTERVAL=1')).toBe(true); + expect(isFiniteRRule('FREQ=DAILY;XCOUNT=5')).toBe(false); + expect(isFiniteRRule('FREQ=DAILY;COUNT=5')).toBe(true); + expect(isFiniteRRule('RRULE:COUNT=5')).toBe(true); + }); + + test('parseRRule rejects invalid calendar dates in 8-digit UNTIL clause', () => { + const parsed = parseRRule('FREQ=DAILY;UNTIL=20260230'); + expect(parsed.untilMs).toBeUndefined(); + + const validParsed = parseRRule('FREQ=DAILY;UNTIL=20260228'); + expect(validParsed.untilMs).toBe(Date.UTC(2026, 1, 28, 23, 59, 59, 999)); + }); + + test('getNextRRuleEpoch returns null when no occurrences exist', () => { + const anchor = Date.UTC(2026, 7, 7, 0, 0, 0, 0); + const result = getNextRRuleEpoch('FREQ=DAILY;UNTIL=20260801', anchor); + expect(result).toBeNull(); + }); +}); diff --git a/packages/library/test/common/reflection_library.test.ts b/packages/library/test/common/reflection.library.test.ts similarity index 100% rename from packages/library/test/common/reflection_library.test.ts rename to packages/library/test/common/reflection.library.test.ts diff --git a/packages/library/test/common/serialize_symbol.test.ts b/packages/library/test/common/serialize.library.test.ts similarity index 100% rename from packages/library/test/common/serialize_symbol.test.ts rename to packages/library/test/common/serialize.library.test.ts diff --git a/packages/library/test/common/string_library.test.ts b/packages/library/test/common/string.library.test.ts similarity index 58% rename from packages/library/test/common/string_library.test.ts rename to packages/library/test/common/string.library.test.ts index 655cef48..d59f657c 100644 --- a/packages/library/test/common/string_library.test.ts +++ b/packages/library/test/common/string.library.test.ts @@ -1,4 +1,4 @@ -import { makeTemplate } from '#library/string.library.js'; +import { makeTemplate, sprintf } from '#library/string.library.js'; describe('String Library', () => { describe('makeTemplate', () => { @@ -38,4 +38,19 @@ describe('String Library', () => { expect(result).toBe('12345'); }); }); + + describe('sprintf', () => { + it('should substitute multi-digit parameter markers like ${10} when 11 or more arguments are passed', () => { + const args = ['zero', 'one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine', 'ten', 'eleven']; + const format = '%s %s %s %s %s %s %s %s %s %s %s %s'; + const result = sprintf(format, ...args); + expect(result).toBe('zero one two three four five six seven eight nine ten eleven'); + + const explicitMarkerResult = sprintf('${10} and ${0}', ...args); + expect(explicitMarkerResult).toBe('ten and zero, one, two, three, four, five, six, seven, eight, nine, eleven'); + + const allExplicitResult = sprintf('${0} - ${10}', 'arg0', 'arg1', 'arg2', 'arg3', 'arg4', 'arg5', 'arg6', 'arg7', 'arg8', 'arg9', 'arg10'); + expect(allExplicitResult).toBe('arg0 - arg10, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9'); + }); + }); }); diff --git a/packages/library/test/common/temporal.library.test.ts b/packages/library/test/common/temporal.library.test.ts new file mode 100644 index 00000000..9c4da3aa --- /dev/null +++ b/packages/library/test/common/temporal.library.test.ts @@ -0,0 +1,65 @@ +import { toZonedDateTime } from '#library/temporal.library.js'; + +describe('Temporal Library Helpers', () => { + describe('toZonedDateTime', () => { + it('should append [tz] to plain ISO strings', () => { + const zdt = toZonedDateTime('2024-01-01T12:00:00', 'Australia/Sydney'); + expect(zdt.timeZoneId).toBe('Australia/Sydney'); + expect(zdt.toString()).toContain('[Australia/Sydney]'); + }); + + it('should NOT append [tz] to strings that already have a bracketed zone', () => { + const zdt = toZonedDateTime('2024-01-01T12:00:00[UTC]', 'Australia/Sydney'); + expect(zdt.timeZoneId).toBe('UTC'); + expect(zdt.toString()).not.toContain('[Australia/Sydney]'); + }); + + it('should NOT append [tz] to strings that have both an offset and a bracket', () => { + const input = '2024-01-01T12:00:00+11:00[Australia/Sydney]'; + const zdt = toZonedDateTime(input, 'UTC'); + expect(zdt.timeZoneId).toBe('Australia/Sydney'); + expect(zdt.toString()).not.toContain('[UTC]'); + }); + + it('should convert "Z" designated strings to the requested timezone preserving instant', () => { + const bag = '2024-01-01T12:00:00Z'; + const zdt = toZonedDateTime(bag, 'Australia/Sydney'); + expect(zdt.timeZoneId).toBe('Australia/Sydney'); + expect(zdt.epochNanoseconds).toBe(Temporal.Instant.from(bag).epochNanoseconds); + expect(zdt.hour).toBe(23); + expect(zdt.day).toBe(1); + expect(zdt.toString()).toBe('2024-01-01T23:00:00+11:00[Australia/Sydney]'); + }); + + it('should normalize space-separated date-time and offset strings', () => { + const zdtSpace = toZonedDateTime('2024-01-01 12:00:00', 'Australia/Sydney'); + expect(zdtSpace.timeZoneId).toBe('Australia/Sydney'); + expect(zdtSpace.hour).toBe(12); + + const zdtSpacedZone = toZonedDateTime('2024-01-01 12:00:00 [Australia/Sydney]', 'UTC'); + expect(zdtSpacedZone.timeZoneId).toBe('Australia/Sydney'); + + const zdtSpacedZ = toZonedDateTime('2024-01-01 12:00:00 Z', 'Australia/Sydney'); + expect(zdtSpacedZ.hour).toBe(23); + }); + + it('should apply fallback timezone to calendar-only bracket annotations', () => { + const zdt = toZonedDateTime('2024-01-01T12:00:00[u-ca=iso8601]', 'Australia/Sydney'); + expect(zdt.timeZoneId).toBe('Australia/Sydney'); + expect(zdt.hour).toBe(12); + expect(zdt.calendarId).toBe('iso8601'); + expect(zdt.toString()).toContain('[Australia/Sydney]'); + }); + + it('should parse date-only values using the fallback timezone', () => { + const zdt = toZonedDateTime('2026-08-10', 'Australia/Sydney'); + expect(zdt.timeZoneId).toBe('Australia/Sydney'); + expect(zdt.year).toBe(2026); + expect(zdt.month).toBe(8); + expect(zdt.day).toBe(10); + expect(zdt.hour).toBe(0); + expect(zdt.minute).toBe(0); + expect(zdt.toString()).toContain('[Australia/Sydney]'); + }); + }); +}); diff --git a/packages/library/test/common/temporal_library.test.ts b/packages/library/test/common/temporal_library.test.ts deleted file mode 100644 index 5b08f5ec..00000000 --- a/packages/library/test/common/temporal_library.test.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { toZonedDateTime } from '#library/temporal.library.js'; - -describe('Temporal Library Helpers', () => { - describe('toZonedDateTime', () => { - it('should append [tz] to plain ISO strings', () => { - const zdt = toZonedDateTime('2024-01-01T12:00:00', 'Australia/Sydney'); - expect(zdt.timeZoneId).toBe('Australia/Sydney'); - expect(zdt.toString()).toContain('[Australia/Sydney]'); - }); - - it('should NOT append [tz] to strings that already have a bracketed zone', () => { - const zdt = toZonedDateTime('2024-01-01T12:00:00[UTC]', 'Australia/Sydney'); - expect(zdt.timeZoneId).toBe('UTC'); - expect(zdt.toString()).not.toContain('[Australia/Sydney]'); - }); - - it('should NOT append [tz] to strings that have both an offset and a bracket', () => { - const input = '2024-01-01T12:00:00+11:00[Australia/Sydney]'; - const zdt = toZonedDateTime(input, 'UTC'); - expect(zdt.timeZoneId).toBe('Australia/Sydney'); - expect(zdt.toString()).not.toContain('[UTC]'); - }); - - it('should handle "Z" as a zone designator and pass it through (even if ZonedDateTime.from throws without a bracket)', () => { - const bag = '2024-01-01T12:00:00Z'; - expect(() => toZonedDateTime(bag, 'Australia/Sydney')).toThrow(/time\s?zone/i); - }); - }); -}); diff --git a/packages/library/test/common/type.library.test.ts b/packages/library/test/common/type.library.test.ts new file mode 100644 index 00000000..2f401597 --- /dev/null +++ b/packages/library/test/common/type.library.test.ts @@ -0,0 +1,13 @@ +import type { Singular, AssertEqual } from '#library/type.library.js'; + +describe('Type Library (Compile-Time)', () => { + it('should correctly resolve Singular types at compile-time', () => { + const testCats: AssertEqual, 'cat'> = true; + const testBus: AssertEqual, 'bus'> = true; + const testS: AssertEqual, 's'> = true; + + expect(testCats).toBe(true); + expect(testBus).toBe(true); + expect(testS).toBe(true); + }); +}); diff --git a/packages/plugins/.bin/check-branch-diff.sh b/packages/plugins/.bin/check-branch-diff.sh new file mode 100755 index 00000000..7b19673d --- /dev/null +++ b/packages/plugins/.bin/check-branch-diff.sh @@ -0,0 +1,77 @@ +#!/usr/bin/env bash +# Compare plugin files in current branch against main branch to verify version bumps + +set -e + +MAIN_BRANCH="${1:-main}" +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)" + +printf "%-25s | %-13s | %-14s | %-14s | %-30s\n" "Plugin Package" "Files Changed" "Main Version" "Branch Version" "Version Bump Status" +printf "%-25s-+-%-13s-+-%-14s-+-%-14s-+-%-30s\n" "-------------------------" "-------------" "--------------" "--------------" "------------------------------" + +has_error=0 + +for plugin_dir in "${REPO_ROOT}/packages/plugins"/*; do + if [ -d "${plugin_dir}" ] && [ -f "${plugin_dir}/package.json" ]; then + plugin_name=$(basename "${plugin_dir}") + rel_path="packages/plugins/${plugin_name}" + + # Count changed files in this plugin (excluding package.json) + changed_count=$(git diff --name-only "${MAIN_BRANCH}...HEAD" -- "${rel_path}" 2>/dev/null | grep -v "package.json" | wc -l || echo "0") + + # Read version from main branch + main_version=$(git show "${MAIN_BRANCH}:${rel_path}/package.json" 2>/dev/null | node --input-type=module -e "import fs from 'fs'; console.log(JSON.parse(fs.readFileSync(process.stdin.fd, 'utf8')).version)" 2>/dev/null || echo "[NEW]") + + # Read version from current working branch + branch_version=$(node --input-type=module -e "import fs from 'fs'; console.log(JSON.parse(fs.readFileSync(process.argv[2], 'utf8')).version)" dummy "${plugin_dir}/package.json" 2>/dev/null || echo "unknown") + + # Determine status + status="Clean (Unchanged)" + if [ "${main_version}" = "[NEW]" ]; then + status="🆕 New Plugin (v${branch_version})" + elif [ "${changed_count}" -gt 0 ]; then + is_gt=$(node --input-type=module -e ' +function parseSemver(v) { + const m = String(v).trim().match(/^v?(\d+)\.(\d+)\.(\d+)(?:-(.+))?$/); + if (!m) return null; + return { major: parseInt(m[1], 10), minor: parseInt(m[2], 10), patch: parseInt(m[3], 10), prerelease: m[4] || "" }; +} +function compare(a, b) { + const pa = parseSemver(a), pb = parseSemver(b); + if (!pa || !pb) return false; + if (pa.major !== pb.major) return pa.major > pb.major; + if (pa.minor !== pb.minor) return pa.minor > pb.minor; + if (pa.patch !== pb.patch) return pa.patch > pb.patch; + if (!pa.prerelease && pb.prerelease) return true; + if (pa.prerelease && !pb.prerelease) return false; + if (!pa.prerelease && !pb.prerelease) return false; + const aParts = pa.prerelease.split("."); + const bParts = pb.prerelease.split("."); + const len = Math.min(aParts.length, bParts.length); + for (let i = 0; i < len; i++) { + const ap = aParts[i], bp = bParts[i]; + if (ap === bp) continue; + const aNum = /^\d+$/.test(ap), bNum = /^\d+$/.test(bp); + if (aNum && bNum) return parseInt(ap, 10) > parseInt(bp, 10); + if (aNum && !bNum) return false; + if (!aNum && bNum) return true; + return ap > bp; + } + return aParts.length > bParts.length; +} +console.log(compare(process.argv[1], process.argv[2]) ? "true" : "false"); +' "${branch_version}" "${main_version}") + + if [ "${is_gt}" = "true" ]; then + status="✅ Bumped (v${main_version} -> v${branch_version})" + else + status="🚨 MODIFIED WITHOUT VERSION BUMP!" + has_error=1 + fi + fi + + printf "%-25s | %-13s | %-14s | %-14s | %-30s\n" "${plugin_name}" "${changed_count}" "${main_version}" "${branch_version}" "${status}" + fi +done + +exit ${has_error} diff --git a/packages/plugins/.bin/check-versions.sh b/packages/plugins/.bin/check-versions.sh new file mode 100755 index 00000000..85ea4022 --- /dev/null +++ b/packages/plugins/.bin/check-versions.sh @@ -0,0 +1,59 @@ +#!/usr/bin/env bash +# Check published NPM versions vs local workspace versions for Tempo and plugins + +set -e + +packages=( + "tempo:packages/tempo/package.json" + "tempo-plugin-ai:packages/plugins/ai/package.json" + "tempo-plugin-astro:packages/plugins/astro/package.json" + "tempo-plugin-batch:packages/plugins/batch/package.json" + "tempo-plugin-finance:packages/plugins/finance/package.json" + "tempo-plugin-snap:packages/plugins/snap/package.json" + "tempo-plugin-sync:packages/plugins/sync/package.json" +) + +# Resolve repository root path (3 levels up from packages/plugins/.bin) +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)" + +printf "%-38s | %-16s | %-16s | %-12s\n" "Package Name" "Published (NPM)" "Local Workspace" "Status" +printf "%-38s-+-%-16s-+-%-16s-+-%-12s\n" "--------------------------------------" "----------------" "----------------" "------------" + +has_error=0 + +for entry in "${packages[@]}"; do + pkg_name="${entry%%:*}" + rel_path="${entry#*:}" + full_npm_name="@magmacomputing/${pkg_name}" + + # Fetch published version from NPM registry + npm_out=$(npm view "${full_npm_name}" version 2>&1) && npm_code=0 || npm_code=$? + if [ "${npm_code}" -eq 0 ]; then + published_ver="${npm_out}" + elif echo "${npm_out}" | grep -q -E "E404|404 Not Found"; then + published_ver="not published" + else + published_ver="lookup failed" + has_error=1 + fi + + # Read local version from package.json + local_ver="unknown" + target_json="${REPO_ROOT}/${rel_path}" + if [ -f "${target_json}" ]; then + local_ver=$(node --input-type=module -e "import fs from 'fs'; console.log(JSON.parse(fs.readFileSync(process.argv[2], 'utf8')).version)" dummy "${target_json}" 2>/dev/null || echo "unknown") + fi + if [ "${local_ver}" = "unknown" ]; then + has_error=1 + fi + + status="Up to date" + if [ "${published_ver}" != "${local_ver}" ]; then + status="Out of sync" + has_error=1 + fi + + printf "%-38s | %-16s | %-16s | %-12s\n" "${full_npm_name}" "${published_ver}" "${local_ver}" "${status}" +done + +exit ${has_error} diff --git a/packages/plugins/ai/CHANGELOG.md b/packages/plugins/ai/CHANGELOG.md index 2c8e1e22..1f1864ca 100644 --- a/packages/plugins/ai/CHANGELOG.md +++ b/packages/plugins/ai/CHANGELOG.md @@ -5,6 +5,47 @@ All notable changes to the `@magmacomputing/tempo-plugin-ai` project will be doc The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.3.0] - 2026-08-10 + +### Added +- **Temporal Difference & Relative Grounding (`diffAI`)**: Added natural language temporal difference calculation and narrative summarization between two `Tempo` points, dates, or timestamps. + - Pre-computes mathematical grounding metrics (`calendarDays`, `elapsedHours`, `businessDays` with weekend and holiday exclusion) to provide strict arithmetic backing for LLM narrative formatting. + - Supports domain-specific delta formatting (e.g. accounting terms, working days, human relative explanations, or business SLAs). + - Handles reversible date intervals with signed/negative business day counts when `start > end`. + - Integrates with the multi-provider dispatch orchestrator (`executeWithMode`) supporting Fallback, Race, Consensus, Hedged, RoundRobin, and Adaptive execution modes. + - Partitioned caching using `diff::` namespace, incorporating start/end epochs, normalized prompts, timezones, locales, regional settings, and sorted holiday lists. + - Supports parallel array batching with optional `softErrors` normalization to `TempoAiError`. +- **Context & Region Inference (`contextAI`)**: Added a context-deduction handler to extract localized environmental settings (`timeZone`, `locale`, `calendar`, `sphere`) from unstructured text inputs, such as user bios, location descriptions, or email bodies. + - Returns `TempoContext` result including IANA timezone, BCP 47 locale tag, Unicode calendar system, and Optional hemisphere (`'north' | 'south'`). + - Supports parallel array batching with optional soft-error mapping. + - Automatically hooks into `executeWithMode` allowing Fallback, Race, and Consensus execution mode routing. + - Fully integrated with caching layers using `context::` namespace partition to prevent collision. + - Enforces minimum confidence checks and validates returned IANA timezones against native Javascript environment `Intl` schemas. + - Resolves workstations and browser-based environment default variables dynamically to construct LLM baseline context prompts. +- **Intelligent Calendar Scheduling (`scheduleAI`)**: Introduced natural language appointment scheduling with deterministic conflict detection and automated slot bumping powered by `Interval.overlaps()`. +- **RFC 5545 Recurrence Engine (`recurrenceAI`)**: Added full recurrence pattern expansion with Cartesian product support across `BYDAY`, `BYHOUR`, `BYMINUTE`, and `BYMONTH` rules, backed by lazy page-based iteration. +- **Deep-Immutability for Default Config**: Migrated `DEFAULT_PROVIDERS` to the `secure()` Proxy utility, enforcing zero-mutation safety across AI provider configurations without dictionary lookup overhead. +- **3-Tier Timeout Resolution Hierarchy**: Introduced flexible, multi-level request timeout control for LLM API queries to prevent network hangs and ensure predictable SLAs: + 1. *Call-site override*: `parseAI(input, { timeout: 3000 })` + 2. *Provider-specific override*: `{ id: 'groq', options: { timeout: 2000 } }` + 3. *Global baseline default*: `initAI({ timeout: 5000 })` (falls back to default 15,000ms). +- **Timeout-Triggered Provider Cascade**: Stalled or slow provider requests trigger an `AbortSignal` cancellation, allowing `AiMode.Fallback` to instantly cascade to secondary providers and `AiMode.Race` to clean up lagging request promises. +- **Request-Locked `.ai.limits` Metadata**: Attached `limits` (`remainingRequests`, `remainingTokens`, `resetAt`) directly to the `.ai` metadata container (`TempoAiMeta`) of returned `Tempo` instances, locking HTTP header rate-limit snapshots to individual requests and preventing concurrency overwrites. +- **Async Storage Adapters (`AiCacheAdapter`)**: Introduced custom storage engine support (`AiCacheAdapter`) in `initAI` and `parseAI` for distributed serverless environments (e.g. Upstash Redis, Cloudflare KV, Memcached). +- **Cascading Cache TTL Hierarchy**: Implemented strict 4-tier TTL resolution (`options.ttl` > `provider.ttl` > `global config.ttl` > default 1 hour / 3,600,000 ms for `parseAI` or 24 hours / 86,400,000 ms for context/difference handlers) for fine-grained cache entry expiration control on stores enforcing TTL. +- **Fail-Open Storage Resilience**: Custom cache adapter errors during `get()` or `set()` operations fail open silently to direct LLM resolution, preserving application request uptime. +- **Dynamic Remote Provider Manifest (`loadRemoteManifest`)**: Lazily fetches remote provider defaults (`providers.v1.json`) on initialization with a 1500ms timeout and automatic air-gapped fallback to compiled `DEFAULT_PROVIDERS`. +- **Cache Eviction Synchronization**: Enhanced `clearAiCache()` to flush matching keys and prefixes across both `Tempo.cache` and custom `AiCacheAdapter` storage engines, returning `Promise` to await async adapter eviction. + +### Changed & Hardened +- **Consensus Mode TTL Resolution**: Fixed a runtime bug where standard provider TTL lookups failed in Consensus mode due to the synthetic sentinel provider ID (`'consensus'`), which caused lookups on the winning provider array to return undefined. Now reduces over all participating provider configs to select the minimum (most conservative) TTL. +- **Deterministic Conflict Bumping (`scheduleAI`)**: Enhanced the scheduling engine's conflict-adjustment logic to iteratively shift proposed intervals against conflicting event bounds, re-verifying against all `busyEvents` and `workingHours` with finite loop guards to guarantee deterministic non-overlapping slots. +- **Streamlined ISO Parsing**: Refactored internal date resolution in `scheduleAI` to delegate directly to core `Tempo` constructors (`new Tempo(str, { timeZone })`), removing redundant regex parsing layers and manual `Temporal.PlainDateTime` conversions. +- **Streamlined Public API Surface**: Removed redundant RFC 5545 utility exports (`isFiniteRule`, `parseRRule`) from the AI plugin entry point in favor of direct imports from `@magmacomputing/tempo/library`. +- **Test Lifecycle & Mock Isolation**: Upgraded all test suite cleanup hooks (`afterEach`) to utilize `vi.restoreAllMocks()` instead of `vi.clearAllMocks()`, ensuring network fetch and console mocks are completely reverted between test cases. Standardized `beforeEach` hooks to run asynchronously and pin `remoteConfigUrl: false` to ensure isolated, deterministic CI test runs. +- **Revision-Guarded State Initialization**: Implemented revision tracking in `initAI` to prevent background remote manifest network resolutions from overwriting newer local initialization configurations. +- **Context Propagation in Recurrence**: Hardened `recurrenceAI` to resolve and propagate full `Tempo` context (`timeZone`, `calendar`, `locale`, `sphere`) across anchor initialization and subsequent recurrence occurrence expansions. + ## [0.2.0] - 2026-07-30 ### Added diff --git a/packages/plugins/ai/README.md b/packages/plugins/ai/README.md index d0fbf429..cd51be0a 100644 --- a/packages/plugins/ai/README.md +++ b/packages/plugins/ai/README.md @@ -6,41 +6,73 @@ npm version npm peer dependency version License TypeScript Ready Documentation

-Tempo community plugin for LLM-powered natural language parsing. +> **Tempo community plugin for LLM-powered natural language date, schedule, and context processing.** -This plugin bridges the gap between deterministic date-math and unstructured NLP inputs, utilizing large language models (like Gemini, Groq, or OpenAI) to safely and asynchronously parse complex natural language expressions into `Tempo` instances. +This plugin bridges deterministic date math and unstructured NLP inputs, leveraging LLMs (Gemini, Groq, OpenAI, Ollama) to asynchronously parse complex natural language expressions into type-safe `Tempo` instances. -> **CRITICAL SECURITY WARNING**: Raw LLM API keys must **never** be exposed in a client-side browser bundle or stored in browser storage (`localStorage`, `sessionStorage`, `IndexedDB`, or browser cache). Client-side storage is vulnerable to XSS attacks, malicious scripts, and browser extension extraction, which can result in API key theft and quota abuse. BYOK (Bring Your Own Key) is only secure on backend servers (Node, edge workers). For public frontend applications, you must route requests through a secure backend proxy service. -> -> **LLM Output Disclaimer**: Large Language Models are probabilistic text generators, not deterministic calculators. Magma Computing Solutions and Tempo core maintainers provide `@magmacomputing/tempo-plugin-ai` "as-is". Developers and organizations are solely responsible for validating AI-generated date and time outputs before relying on them in financial, legal, medical, or time-critical production systems. +> 🔒 **Security Notice**: Raw LLM API keys must **never** be exposed in client-side browser bundles or client storage (`localStorage`, `sessionStorage`, `IndexedDB`). BYOK is only safe on backend servers or edge runtime proxies. -## Installation +--- + +## ⚡ Quick Start + +### 📦 Installation ```bash npm install @magmacomputing/tempo-plugin-ai ``` -## Setup & Usage +### 🎯 Usage ```typescript -import { parseAI, initAI, clearAiCache } from '@magmacomputing/tempo-plugin-ai'; +import { parseAI, initAI } from '@magmacomputing/tempo-plugin-ai'; -// Initialize with your BYOK API Key (ensuring non-undefined string key) -initAI({ +// Initialize with your API keys +await initAI({ providers: [ - ...(process.env.GROQ_API_KEY ? [{ id: 'groq', key: process.env.GROQ_API_KEY }] : []), + { id: 'groq', key: process.env.GROQ_API_KEY! } ] }); -// Parse a complex natural language string! +// Parse natural language into a standard Tempo instance! const dt = await parseAI("The penultimate Tuesday before Thanksgiving in 2026"); -// Evict bad parses from the cache -clearAiCache("The penultimate Tuesday before Thanksgiving in 2026"); +console.log(dt.format('{yyyy}-{mm}-{dd}')); // 2026-11-17 +console.log(dt.ai?.provider); // 'groq' +console.log(dt.ai?.confidence); // 0.98 ``` -Full documentation is available at [https://magmacomputing.github.io/magma/doc/9-plugins/ai.index.html](https://magmacomputing.github.io/magma/doc/9-plugins/ai.index.html). +--- + +## 📚 AI Endpoint Catalog + +| Endpoint | Description | Doc | +| :--- | :--- | :---: | +| **`parseAI`** | Parse relative/point-in-time dates (e.g. *"next Friday at 4pm"*) | | +| **`recurrenceAI`** | Convert repeating patterns (e.g. *"every 2 weeks on Friday"*) to RRULEs | | +| **`diffAI`** | Calculate natural language difference & business days between dates | | +| **`scheduleAI`** | Book appointment slots around busy calendar event bounds | | +| **`contextAI`** | Infer timezone, locale, and calendar from user profiles/bios | | + +--- + +## ✨ Features & Architecture + +* 🤖 **Multi-Provider Routing**: Native support for Groq, OpenAI, Gemini, Mistral, and local Ollama nodes with automatic fallback. +* 🌐 **Dynamic Provider Manifest**: Model IDs and endpoints are lazily updated via hosted JSON manifests with 1500ms fail-open air-gapped fallbacks. +* ⚡ **Two-Tier Caching**: Combines fast local in-memory LRU caching (`BoundedCache`) with optional async storage adapters (`AiCacheAdapter` for Redis / Cloudflare KV). +* ⏱️ **Cascading TTL Policies**: Granular TTL control at call-site, provider, or global levels for TTL-enforcing storage adapters. +* 🛡️ **Fail-Safe Confidence Bounds**: Configurable `minConfidence` thresholds and array batch processing with soft-error handling. + +--- + +## 📚 Complete Guides + +For complete API references, architecture guides, and advanced examples: +📖 **[Read the Official AI Plugin Documentation](https://magmacomputing.github.io/magma/doc/9-plugins/ai.index.html)** + +--- -## Licensing +## ⚖️ Licensing -This is a **Community** plugin. It is completely free and open-source for personal and commercial use. No license token is required. +This is a **Community** plugin. It is completely free and open-source for personal and commercial use under the MIT license. diff --git a/packages/plugins/ai/doc/architecture.md b/packages/plugins/ai/doc/architecture.md index 7a176adb..8f7e7428 100644 --- a/packages/plugins/ai/doc/architecture.md +++ b/packages/plugins/ai/doc/architecture.md @@ -40,12 +40,39 @@ initAI({ id: 'local', key: 'no-key-needed', url: 'http://localhost:11434/v1/chat/completions', - model: 'your-local-model' + model: 'your-local-model', + options: { timeout: 5000 } // Custom provider-level timeout (5s) } ] }); ``` +### Dynamic Provider Manifests & Remote Endpoint Trust + +By default, `@magmacomputing/tempo-plugin-ai` lazily fetches provider defaults (model IDs, endpoints, token parameter keys) from `https://tempo.magmacomputing.com.au/providers.v1.json` once per application lifecycle. + +- **Remote Manifest Trust & Endpoint Enforcement**: + - `remoteConfigUrl` is restricted to fixed trusted hosts (`tempo.magmacomputing.com.au` or trusted internal HTTPS endpoints). + - Any dynamic `provider.url` values received from the manifest or dynamically returned via `fetchDefaults` are strictly validated against an enforced provider host allowlist (or must use verified HTTPS/localhost origins) before `getResolvedProviderDefaults()` merges them into runtime provider configurations. Untrusted or unauthenticated endpoints are rejected and stripped before merging. +- **Validation on `fetchDefaults` Hook**: The exact same host allowlist and HTTPS origin verification is enforced when resolving custom provider options via the `fetchDefaults` callback. Any dynamic hook attempting to return unauthenticated or disallowed host URLs will have the `url` property safely discarded. +- **Async Resolution & Promise Lifecycle**: `initAI()` returns a `Promise`. + - **Synchronous Fire-and-Forget**: Calling `initAI(...)` synchronously without `await` immediately initializes system state with compiled local provider defaults (`DEFAULT_PROVIDERS`). You can execute `parseAI()` immediately on the next line without blocking. The remote manifest is fetched in the background and transparently updates provider defaults once received. + - **Guaranteed Remote Resolution**: If your application strictly requires remote provider defaults to be resolved before executing your first AI request, you can `await initAI(...)`: + ```typescript + // Await guaranteed remote manifest completion before proceeding + await initAI({ + providers: [{ id: 'groq', key: process.env.GROQ_API_KEY! }] + }); + ``` +- **Fail-Open & Air-Gapped Fallback**: If the network request fails, times out (1500ms limit), or the application is running offline or in an air-gapped environment, `initAI()` automatically and silently falls back to compiled local defaults (`DEFAULT_PROVIDERS`). +- **Disabling Remote Manifest**: Pass `remoteConfigUrl: false` to disable remote manifest fetching entirely: + ```typescript + initAI({ + providers: [{ id: 'groq', key: process.env.GROQ_API_KEY! }], + remoteConfigUrl: false // Disable remote manifest fetching + }); + ``` + ### Frontend Security Warning > [!CAUTION] > **Never** expose a raw LLM API key in a client-side browser bundle (like React or Vue) or store it in browser storage (`localStorage`, `sessionStorage`, `IndexedDB`, or browser cache). Any Cross-Site Scripting (XSS) vulnerability, compromised NPM package, or malicious browser extension can easily inspect client-side storage and steal secret keys, leading to quota exhaustion, billing fraud, or permanent provider bans. BYOK keys are *only* safe on backend servers or edge workers. diff --git a/packages/plugins/ai/doc/contextAI.md b/packages/plugins/ai/doc/contextAI.md new file mode 100644 index 00000000..6d6175e1 --- /dev/null +++ b/packages/plugins/ai/doc/contextAI.md @@ -0,0 +1,123 @@ +# `contextAI` — Context & Regional Inference + +`contextAI()` is designed to analyze unstructured or ambiguous text (e.g. user biographies, locations, or email bodies) and infer their regional configuration settings. It resolves these properties to a standard configuration object containing timezone, locale, calendar system, and hemisphere. + +This is highly useful for user onboarding settings, automatic context mapping for calendar syncs, and geolocating inputs dynamically without maintaining static geographic mapping tables. + +--- + +## Basic Usage + +> [!NOTE] +> Like all Tempo AI functions, `contextAI` requires prior initialization with at least one active provider via `initAI()`. + +```typescript +import { initAI, contextAI } from '@magmacomputing/tempo-plugin-ai'; + +// 1. Configure the AI provider farm +await initAI({ + providers: [ + { id: 'groq', key: process.env.GROQ_API_KEY, model: 'llama-3.3-70b-versatile' } + ] +}); + +// 2. Infer contextual settings from unstructured text +const context = await contextAI("I'm a photographer based in Sydney, Australia."); + +console.log(context.timeZone); // "Australia/Sydney" +console.log(context.locale); // "en-AU" +console.log(context.calendar); // "gregory" +console.log(context.sphere); // "south" +console.log(context.confidence); // 0.98 +``` + +--- + +## Configuration Options (`AiContextOptions`) + +| Option | Type | Description | +| :--- | :--- | :--- | +| **`force`** | `boolean` | If true, bypasses the cache to initiate a fresh LLM query. | +| **`cache`** | `boolean` | If false, disables writing to and reading from cache adapters. | +| **`cacheAdapter`** | `AiCacheAdapter` | Custom cache engine (e.g., Redis) for caching results on this request. | +| **`ttl`** | `number` | Time-to-live override in milliseconds for cached results. | +| **`minConfidence`**| `number` | Minimum confidence score threshold (0.0 to 1.0) required to return a valid slot. Throws `TempoAiError(422)` if lower. | +| **`mode`** | `AiMode` | Concurrency routing strategy (`fallback`, `race`, `consensus`, `hedged`, `roundrobin`, `adaptive`). Refer to the [Multi-Provider Execution Modes Guide](./modes.md). | +| **`providers`** | `AiProvider[]` | Per-request provider configuration overrides. | +| **`timeout`** | `number` | Per-request timeout in milliseconds (overrides provider and global timeouts). | +| **`hedgeDelay`** | `number` | Delay in milliseconds before initiating speculative hedging in `AiMode.Hedged` (default: `800ms`). | +| **`debug`** | `boolean` | If true, logs prompt context and LLM payloads to console. | +| **`softErrors`** | `boolean` | If true, returns `TempoAiError` into array index position instead of rejecting the entire batch query. | + +--- + +## Result Schema (`TempoContext`) + +```typescript +export interface TempoContext { + /** Inferred IANA time zone identifier (e.g. 'America/New_York') */ + timeZone: string; + + /** Inferred BCP 47 language/region tag (e.g. 'en-US') */ + locale: string; + + /** Inferred Unicode calendar system type (e.g. 'gregory') */ + calendar: string; + + /** Inferred hemisphere, or undefined if ambiguous */ + sphere?: 'north' | 'south' | undefined; + + /** Confidence rating from 0.0 (unparseable) to 1.0 (certain) */ + confidence: number; + + /** Resolution source ('cache' or provider ID like 'groq', 'gemini', 'openai') */ + provider: string; + + /** Step-by-step reasoning or justification provided by the engine/LLM */ + reasoning?: string | undefined; +} +``` + +--- + +## Key Architectural Behaviors + +### 1. Workspace Baseline Context +`contextAI` inspects the host runtime or current `Tempo` configuration (`Tempo.options.timeZone`, `Tempo.options.locale`, etc.) as a fallback baseline. If an input like `"at home"` is provided, the LLM will ground its inference in the workstation's baseline defaults. + +### 2. Strict Confidence Thresholds +Using `minConfidence`, developers can guarantee that low-certainty or completely ambiguous inputs (e.g., `"in the park"`) throw a `TempoAiError(422)` rather than silently returning guessed context parameters: + +```typescript +const context = await contextAI("meeting somewhere online", { minConfidence: 0.9 }); +// Throws TempoAiError(422): Inferred context confidence (0.4) is below the required threshold of 0.9. +``` + +### 3. Timezone Validation +Before returning, the returned IANA timezone string is dynamically validated against the runtime's native JavaScript `Intl` API. If the LLM returns an unsupported or fake timezone identifier, `contextAI` throws a `TempoAiError(422)` to prevent application runtime failures. + +### 4. Parallel Batch Processing +You can pass an array of strings to process multiple contexts concurrently: +```typescript +const [context1, context2] = await contextAI([ + "Working from Kyoto", + "Living in Melbourne" +]); +``` + +### Combining `contextAI` with `parseAI` (The Pivot Flow) + +Often, a user will mention their location in one sentence and a relative time in another. You can chain these APIs together to form a seamless date-resolution pipeline: + +```typescript +import { contextAI, parseAI } from '@magmacomputing/tempo-plugin-ai'; + +// 1. Deduces the context +const ticketContext = await contextAI("customer issue from our London office"); +// ticketContext = { timeZone: 'Europe/London', locale: 'en-GB', sphere: 'north' } + +// 2. Feed the output context directly as options into parseAI +const resolutionTime = await parseAI("issue occurred on 04/05/2026 at 3 PM", ticketContext); +// 1. Correctly parses 04/05 to May 4th (UK format) rather than April 5th. +// 2. Adjusts to BST/GMT (Europe/London). +``` diff --git a/packages/plugins/ai/doc/diffAI.md b/packages/plugins/ai/doc/diffAI.md new file mode 100644 index 00000000..fc8bd5aa --- /dev/null +++ b/packages/plugins/ai/doc/diffAI.md @@ -0,0 +1,119 @@ +# `diffAI` — Contextual & Business Date Deltas + +`diffAI()` expresses the temporal delta between two `Tempo` instances, timestamps, or date strings in human, business, or operational terms. + +While core `Tempo` provides precise numeric calculations (`start.until(end, 'day')`), `diffAI` bridges the gap to domain-specific narrative explanations (e.g. accounting working days, delivery SLAs, relative countdowns, and sprint planning summaries) backed by arithmetic grounding. + +--- + +## Basic Usage + +```typescript +import { Tempo } from '@magmacomputing/tempo'; +import { initAI, diffAI } from '@magmacomputing/tempo-plugin-ai'; + +// 1. Initialize AI providers +await initAI({ + providers: [ + { id: 'groq', key: process.env.GROQ_API_KEY, model: 'llama-3.3-70b-versatile' } + ] +}); + +const start = new Tempo('2026-08-01T09:00:00Z'); // Saturday +const end = new Tempo('2026-08-10T17:00:00Z'); // Next Monday + +const diff = await diffAI(start, end, 'explain in terms of business working days'); + +console.log(diff.formatted); // "5 business days (approx. 224 calendar hours)" +console.log(diff.businessDays); // 5 +console.log(diff.days); // 9.33 +console.log(diff.hours); // 224 +console.log(diff.confidence); // 0.96 +``` + +--- + +## Configuration Options (`AiDiffOptions`) + +| Option | Type | Description | +| :--- | :--- | :--- | +| **`holidays`** | `string[]` | Explicit array of holiday dates (`'YYYY-MM-DD'`) to exclude from business days. | +| **`region`** | `string` | Regional context (e.g., `'AU-NSW'`, `'US'`) passed to LLM grounding. | +| **`timeZone`** | `string` | Target IANA timezone for date boundaries and calculation. | +| **`locale`** | `string \| string[]` | Locale for narrative language formatting. | +| **`force`** | `boolean` | If true, bypasses the cache to initiate a fresh LLM query. | +| **`cache`** | `boolean` | If false, disables writing to and reading from cache adapters. | +| **`cacheAdapter`** | `AiCacheAdapter` | Custom cache engine (e.g., Redis) for caching results on this request. | +| **`ttl`** | `number` | Time-to-live override in milliseconds for cached results. | +| **`minConfidence`**| `number` | Minimum confidence score threshold (0.0 to 1.0) required. Throws `TempoAiError(422)` if lower. | +| **`mode`** | `AiMode` | Concurrency routing strategy (`fallback`, `race`, `consensus`, `hedged`, `roundrobin`, `adaptive`). Refer to the [Multi-Provider Execution Modes Guide](./modes.md). | +| **`softErrors`** | `boolean` | If true, returns `TempoAiError` into array indices instead of rejecting batch queries. | + +--- + +## Result Schema (`TempoAiDiffResult`) + +```typescript +export interface TempoAiDiffResult { + /** Human-friendly, contextual narrative text summarizing the difference */ + formatted: string; + + /** Total calendar days between start and end */ + days?: number | undefined; + + /** Total elapsed calendar hours between start and end */ + hours?: number | undefined; + + /** Total business working days (excluding weekends and matching holidays) */ + businessDays?: number | undefined; + + /** List of holiday dates (YYYY-MM-DD) encountered within the interval */ + holidays?: string[] | undefined; + + /** Confidence rating from 0.0 (unparseable) to 1.0 (certain) */ + confidence: number; + + /** Resolution source ('cache' or provider ID like 'groq', 'gemini', 'openai') */ + provider: string; + + /** Step-by-step reasoning or justification provided by the engine/LLM */ + reasoning?: string | undefined; +} +``` + +--- + +## Key Architectural Behaviors + +### 1. Native Grounding Context +To guarantee arithmetic precision and prevent LLM hallucinations, `diffAI` natively computes exact calendar days, elapsed hours, and business working days (excluding Saturdays, Sundays, and provided holidays) using `Tempo` before dispatching to the LLM. These metrics are supplied as grounding constraints in the system prompt. + +### 2. Public Holiday Exclusions +You can supply regional public holidays (e.g., from `@magmacomputing/tempo-fns` via `getPublicHolidays`) to automatically adjust business day counters: + +```typescript +import { getPublicHolidays } from '@magmacomputing/tempo-fns'; + +const holidays = (await getPublicHolidays(2026, 'AU')).map(h => h.date); + +const result = await diffAI('2026-12-24', '2027-01-04', 'calculate net business days', { + holidays, + region: 'AU', +}); + +console.log(result.businessDays); // 5 (Christmas, Boxing Day, New Year's Day excluded) +console.log(result.holidays); // ['2026-12-25', '2026-12-26', '2027-01-01'] +``` + +### 3. Directional Awareness & Reverse Intervals +When the end date is earlier than the start date, `diffAI` indicates past/backward direction and provides negative business day counts (e.g. `-5` business days), allowing the LLM to format relative past descriptions (e.g. `"5 business days ago"`). + +### 4. Parallel Batch Processing +You can pass an array of diff pairs to resolve multiple deltas concurrently: + +```typescript +const [diff1, diff2] = await diffAI([ + { start: '2026-08-01', end: '2026-08-05', prompt: 'summarize for billing' }, + { start: '2026-08-05', end: '2026-08-15', prompt: 'explain project sprint' }, +]); +``` diff --git a/packages/plugins/ai/doc/index.md b/packages/plugins/ai/doc/index.md index 644001bd..c417d500 100644 --- a/packages/plugins/ai/doc/index.md +++ b/packages/plugins/ai/doc/index.md @@ -13,99 +13,49 @@ > [!CAUTION] > **LLM Output Disclaimer**: Magma Computing Solutions and the Tempo core maintainers provide `@magmacomputing/tempo-plugin-ai` "as-is" without warranty of any kind. Large Language Models are probabilistic text generators, not deterministic calculators. Developers and organization operators are solely responsible for validating AI-generated date and time outputs before relying on them in financial, legal, medical, or time-critical production systems. -Tempo community plugin for LLM-powered natural language parsing. +Tempo community plugin for LLM-powered natural language date parsing, schedule compilation, and temporal processing. This plugin bridges the gap between deterministic date-math and unstructured NLP inputs, utilizing large language models (like Gemini, Groq, or OpenAI) to safely and asynchronously parse, format, and process complex natural language temporal expressions into `Tempo` instances. -> **Note**: This plugin is **not** a silver-bullet replacement for all your parsing needs! `Tempo.parse()` natively handles structured dates and formats phenomenally well using its Aliases, Layouts, and Snippets. The Tempo AI plugin is specifically designed to be an alternative path for handling completely unstructured, conversational human language that would otherwise be impossible to Regex. -> -> **CRITICAL SECURITY WARNING**: Raw LLM API keys must **never** be exposed in a client-side browser bundle or stored in browser storage (`localStorage`, `sessionStorage`, `IndexedDB`, or browser cache). Client-side storage is vulnerable to XSS attacks, malicious scripts, and browser extension extraction, which can result in API key theft and quota abuse. BYOK (Bring Your Own Key) is only secure on backend servers (Node, edge workers). For public frontend applications, you must route requests through a secure backend proxy service. - -## Ideal Use-Cases - -Good AI function candidates (such as `parseAI`) represent unstructured, conversational, or event-driven natural language expressions that are impossible to Regex or parse with standard relative offset rules: - -- **Holiday & Relative Calendar Math**: `"The Friday after Thanksgiving"`, `"The penultimate Tuesday before Christmas"` -- **Named Cultural / Event Terms**: `"Star Wars Day at 5pm"`, `"A fortnight after Labor Day"` -- **Conversational Relative Terms**: `"The last working day of Q3"`, `"Midday on the summer solstice"` +> **CRITICAL SECURITY WARNING**: Raw LLM API keys must **never** be exposed in a client-side browser bundle or stored in browser storage (`localStorage`, `sessionStorage`, `IndexedDB`, or browser cache). BYOK (Bring Your Own Key) is only secure on backend servers (Node, edge workers). For public frontend applications, route requests through a secure backend proxy service. -> **Avoid Simple Offsets**: Phrases like `"in 5 minutes"`, `"tomorrow"`, or `"next Friday"` are natively intercepted and resolved by core `Tempo` without calling the LLM (unless `force: true` is passed). - -## Installation +## Installation & Quickstart ```bash npm install @magmacomputing/tempo-plugin-ai ``` -## Setup & Usage - ```typescript -import { parseAI, initAI, clearAiCache } from '@magmacomputing/tempo-plugin-ai'; - -// Initialize with your BYOK API Key -initAI({ - providers: [ - { id: 'openai', key: process.env.OPENAI_API_KEY, model: 'your-preferred-model' }, - ], - debug: true // (Development-only) Enable verbose console logging -}); -``` - -> **Tip**: `initAI` is fully re-callable! You can invoke it multiple times during your application's lifecycle to hot-swap API keys or update your fallback providers mid-stream without restarting your server. - -```typescript -// Parse a complex natural language string! -const dt1 = await parseAI("The penultimate Tuesday before Thanksgiving in 2026"); - -// Evict bad parses from the cache -clearAiCache("The penultimate Tuesday before Thanksgiving in 2026"); -``` +import { parseAI, initAI } from '@magmacomputing/tempo-plugin-ai'; -## Execution Modes & Multi-Provider Options - -The AI plugin supports multi-provider execution strategies (`fallback`, `race`, `consensus`) and confidence filtering on per-request options: - -```typescript -// 1. Fallback mode (default): query providers sequentially in array order until one succeeds -const fallback = await parseAI("First Monday of November", { - mode: 'fallback', // Default strategy if omitted - minConfidence: 0.8 // Require at least 0.8 confidence threshold +// Initialize provider farm (Node/SSR backend) +await initAI({ + providers: [{ id: 'groq', key: process.env.GROQ_API_KEY }] }); -// 2. Race mode: send concurrent requests to all providers, returning the fastest valid response -const fastest = await parseAI("Third Friday of October", { mode: 'race' }); - -// 3. Consensus mode: query providers concurrently and boost confidence when outputs agree -const agreed = await parseAI("The penultimate Tuesday before Thanksgiving", { - mode: 'consensus', - minConfidence: 0.85 // Require at least 0.85 confidence threshold -}); +// Parse natural language temporal expressions +const dt = await parseAI("The penultimate Tuesday before Thanksgiving in 2026"); +console.log(dt.format('{yyyy}-{mm}-{dd}')); // 2026-11-17 ``` -## Debugging & Forced Evaluation - -When building your LLM queries, it is often useful to see exactly how AI functions route your data. +## AI Function Catalog +All AI functions return a standard ES Promise wrapped object. -**Global Debugging** -Passing `debug: true` into `initAI` is intended for **development environments only**. It will globally log system prompts, localized context, and raw LLM responses to the console. Because prompts, context, and responses may contain user-supplied or sensitive data, disable `debug: true` or redact sensitive logs in production. +| Function | Input | Returns (`Promise<...>`) | Description | Doc | +| :--- | :--- | :--- | :--- | :---: | +| **`initAI`** | Provider config & API keys | `void` | Configured AI provider farm | | +| **`parseAI`** | Natural language text string(s) | `Tempo` \| `Tempo[]` \| `(Tempo \| TempoAiError)[]` | Single point-in-time `Tempo` instance (or batch array) | | +| **`recurrenceAI`** | Natural language pattern or RRULE string | `TempoRecurrenceResult` | **Iterable series of `Tempo` dates** (with `.take(n)`, `[Symbol.iterator]`, & RRULE string) | | +| **`scheduleAI`** | Booking prompt + busy constraints | `TempoScheduleResult` | **Resolved appointment slot** (`start`, `end`, `slot`, `alternatives`, `ai.conflictBumped`) | | +| **`contextAI`** | Context text string(s) | `TempoContext` \| `TempoContext[]` \| `(TempoContext \| TempoAiError)[]` | **Inferred regional context** (`timeZone`, `locale`, `calendar`, `sphere`) | | +| **`diffAI`** | Start & End dates + prompt | `TempoAiDiffResult` \| `TempoAiDiffResult[]` \| `(TempoAiDiffResult \| TempoAiError)[]` | **Narrative time delta & business days** (`formatted`, `businessDays`, `days`, `hours`, `holidays`) | | -**Forced Evaluation** -If a relative phrase (like `"Next Friday"`) would normally be resolved by the native `Tempo` engine or read from existing cache, you can skip native pre-parsing and cache lookups by passing `force: true`. The resulting LLM response is still written to `Tempo.cache` for subsequent lookups: - -```typescript -const dt = await parseAI("Next Friday", { - anchor: '2026-09-01T00:00:00Z', - force: true, // Skips native pre-parsing & cache lookup; forces an LLM request (result is cached) - debug: true // Overrides the global debug flag for this specific request -}); -``` - -## Documentation Topics +## Architecture & Infrastructure Guides > [!IMPORTANT] -> **Production Recommendation**: Due to the complexities of LLM APIs, including caching gotchas, context injection, rate limits, and calendar math hallucinations, we politely but firmly recommend reading the three dedicated guides below before deploying this plugin in a production environment. +> **Production Recommendation**: Due to the complexities of LLM APIs, including caching gotchas, context injection, rate limits, and calendar math hallucinations, we politely but firmly recommend reading the dedicated guides below before deploying this plugin in a production environment. -To learn more about configuring and optimizing the AI Plugin, check out the dedicated guides: +- [Multi-Provider Execution Modes](./modes.md) (Hedged, RoundRobin, Adaptive, Race, Consensus, Fallback) - [Provider Architecture & Security](./architecture.md) (BYOK vs Proxy patterns, Frontend Security) - [Context & Natural Language Parsing](./context.md) (How Timezone and Locale are injected) - [Rate Limits & Cache Management](./rate-limits.md) (Tracking API quotas, handling 429 errors, and custom Redis caches) @@ -113,3 +63,4 @@ To learn more about configuring and optimizing the AI Plugin, check out the dedi ## Licensing This is a **Community** plugin. It is completely free and open-source for personal and commercial use. No license token is required. + diff --git a/packages/plugins/ai/doc/init.md b/packages/plugins/ai/doc/init.md new file mode 100644 index 00000000..49c93810 --- /dev/null +++ b/packages/plugins/ai/doc/init.md @@ -0,0 +1,115 @@ +# `initAI` — Provider Initialization & Farm Configuration + +`initAI()` sets up the global configuration for `@magmacomputing/tempo-plugin-ai`, managing provider authentication, multi-provider execution modes, global SLAs/timeouts, and caching strategies. + +## Basic Usage + +```typescript +import { initAI, parseAI } from '@magmacomputing/tempo-plugin-ai'; + +// Initialize with BYOK (Bring Your Own Key) provider credentials +await initAI({ + providers: [ + { id: 'groq', key: process.env.GROQ_API_KEY }, + { id: 'openai', key: process.env.OPENAI_API_KEY, model: 'gpt-4o' } + ], + timeout: 5000, // 5-second global SLA default + debug: true // Enable operational trace logging (development-only) +}); +``` + +> **Tip**: `initAI` returns a `Promise` and is fully re-callable! Calling it synchronously without `await` instantly initializes local configurations so you can call `parseAI` immediately, while `await initAI()` guarantees that remote provider manifest defaults are fetched and applied before proceeding (with explicit provider configuration values always taking precedence over remote manifest defaults). + +## Execution Modes & Multi-Provider Options + +The AI plugin supports six multi-provider execution strategies (`fallback`, `race`, `consensus`, `adaptive`, `hedged`, `roundrobin`): + +* **`fallback`** (default): Queries providers sequentially in array order until one succeeds. +* **`race`**: Sends concurrent requests to all providers, returning the fastest valid response. +* **`consensus`**: Queries providers concurrently and boosts confidence when outputs agree. +* **`hedged`**: Initiates staggered latency hedging, querying subsequent providers if the primary is slow. +* **`roundrobin`**: Cyclic load balancing across the provider pool. +* **`adaptive`**: Proactive telemetry-aware rate-limit avoidance. + +For a comprehensive guide, decision flowcharts, and configuration details for all execution modes, refer to the [Multi-Provider Execution Modes Guide](./modes.md). + +```typescript +// 1. Fallback mode (default): query providers sequentially in array order until one succeeds +initAI({ + mode: 'fallback', + providers: [ + { id: 'groq', key: process.env.GROQ_API_KEY }, // Primary provider + { id: 'openai', key: process.env.OPENAI_API_KEY } // Fallback provider + ] +}); + +// 2. Race mode: send concurrent requests to all providers, returning the fastest valid response +const fastest = await parseAI("Third Friday of October", { mode: 'race' }); + +// 3. Consensus mode: query providers concurrently and boost confidence when outputs agree +const agreed = await parseAI("The penultimate Tuesday before Thanksgiving", { + mode: 'consensus', + minConfidence: 0.85 +}); + +// 4. Hedged mode: speculative concurrency with staggered delay (e.g. 800ms) +const hedged = await parseAI("First Monday of December", { + mode: 'hedged', + hedgeDelay: 800 +}); +``` + +## Timeout Controls & SLAs + +Prevent hanging requests using the 3-tier timeout hierarchy (`call-site` > `provider` > `global` > `default 15s`): + +```typescript +initAI({ + providers: [ + { id: 'groq', key: process.env.GROQ_API_KEY, options: { timeout: 2000 } } // 2s timeout for fast provider + ], + timeout: 5000 // 5s global default timeout +}); + +// Hard 3-second SLA override for a specific call-site +const dt = await parseAI("Next Tuesday at 4pm", { timeout: 3000 }); +``` + +## Operational Trace Logging & Debugging + +**Operational Trace Logging** +Passing `debug: true` into `initAI` (or setting `{ debug: true }` on a per-request options object) outputs concise operational trace logs (prefixed with `[tempo-plugin-ai]`) to the developer console for monitoring provider fallback decisions and execution timing. + +Detailed diagnostic context—including `rawPrompt`, `normalizedPrompt`, `reasoning`, confidence scores, and rate limit snapshots—is attached directly to the returned `Tempo` instance via the `.ai` property when `debug: true` is enabled. + +> [!WARNING] +> **Diagnostic Security Notice**: Inspecting or exposing the `.ai` metadata property (such as `rawPrompt` or `reasoning`) in public UI components or client-side telemetry may expose raw user inputs. Ensure sensitive diagnostic fields on `Tempo.ai` are sanitized before forwarding instances to external monitoring tools. + +## Configuration Options Reference + +```typescript +export interface AiConfig { + /** List of configured AI providers */ + providers?: AiProvider[]; + /** Default execution mode across providers ('fallback' | 'race' | 'consensus' | 'hedged' | 'roundrobin' | 'adaptive') */ + mode?: 'fallback' | 'race' | 'consensus' | 'hedged' | 'roundrobin' | 'adaptive'; + /** Speculative hedge delay in milliseconds (hedged mode only, default: 800ms) */ + hedgeDelay?: number; + /** Global SLA timeout in milliseconds */ + timeout?: number; + /** Global debug flag for operational trace logging */ + debug?: boolean; + /** Synchronous Map or BoundedCache for static glossary terms */ + cache?: Map; + /** Custom cache adapter for distributed storage (e.g. Redis, KV) */ + cacheAdapter?: AiCacheAdapter; + /** Global default time-to-live in milliseconds for cache adapters */ + ttl?: number; + /** Minimum confidence threshold for AI parsing results (0.0 to 1.0) */ + minConfidence?: number; + /** Optional hook to intercept and resolve dynamic provider defaults */ + fetchDefaults?: (providerId: string) => Promise | null> | Partial | null; + /** URL for dynamic remote provider manifest updates, or `false` to disable */ + remoteConfigUrl?: string | false; +} +``` diff --git a/packages/plugins/ai/doc/modes.md b/packages/plugins/ai/doc/modes.md new file mode 100644 index 00000000..17cd66b3 --- /dev/null +++ b/packages/plugins/ai/doc/modes.md @@ -0,0 +1,164 @@ +# Multi-Provider Execution Modes (`AiMode`) + +`@magmacomputing/tempo-plugin-ai` provides a robust, multi-provider dispatch orchestrator that handles latency hedging, quota load-balancing, consensus verification, and fault-tolerant failovers across diverse LLM providers (e.g. OpenAI, Anthropic, Groq, Mistral, Google Gemini, Ollama, local vLLM). + +Execution modes can be configured globally during plugin initialization or overridden per request: + +```typescript +import { initAI, parseAI, AiMode } from '@magmacomputing/tempo-plugin-ai'; + +// Global configuration +await initAI({ + providers: [ + { id: 'groq', key: process.env.GROQ_API_KEY }, + { id: 'openai', key: process.env.OPENAI_API_KEY } + ], + mode: AiMode.Hedged +}); + +// Per-request override +const dt = await parseAI('next friday at 3pm', { + mode: AiMode.Adaptive +}); +``` + +--- + +## Strategy Comparison + +| Mode | Dispatch | Token Cost | Latency | Rate-Limit Resilience | +| :--- | :--- | :---: | :---: | :---: | +| **`Fallback`** *(Default)* | Sequential | 🟢 1 request | 🟡 Moderate | 🟡 Reactive | +| **`Hedged`** | Staggered (primary + timer) | 🟢 ~1.15 avg | 🟢 Ultra-Fast | 🟡 Reactive | +| **`RoundRobin`** | Cyclic rotation | 🟢 1 request | 🟡 Moderate | 🟢 High | +| **`Adaptive`** | Quota-sorted rotation | 🟢 1 request | 🟡 Moderate | 🟢 Maximum | +| **`Race`** | Full parallel | 🔴 N requests | 🟢 Ultra-Fast | 🟡 Reactive | +| **`Consensus`** | Full parallel + voting | 🔴 N requests | 🟡 Moderate | 🟡 Reactive | + +--- + +## Dispatch Decision Guide + +```mermaid +flowchart TD + Start(["Where does this run?"]) --> UI["User-facing\n(search bar, chatbot, form)"] + Start --> Batch["Background / batch\n(ingestion, ETL, reports)"] + Start --> Audit["High-stakes audit\n(legal, financial, scheduling)"] + + UI --> MultiUI{"Multiple providers\nor API keys?"} + MultiUI -- No --> Fallback["⬇️ AiMode.Fallback\nDefault • Most cost-efficient"] + MultiUI -- Yes --> CostSpeed{"Latency sensitive\nor cost concern?"} + CostSpeed -- "Fastest response (high cost)" --> Race["🏁 AiMode.Race\nAbsolute fastest response"] + CostSpeed -- "Staggered concurrency (lower cost)" --> Hedged["⏳ AiMode.Hedged\nStaggered latency hedging"] + + Batch --> Quota{"Quota pressure\nor multiple keys?"} + Quota -- "Spread evenly" --> RR["🔄 AiMode.RoundRobin\nCyclic key rotation"] + Quota -- "Avoid 429 proactively" --> Adaptive["📡 AiMode.Adaptive\nTelemetry-driven ordering"] + Quota -- "Single key, simple" --> Fallback["⬇️ AiMode.Fallback\nDefault • Most cost-efficient"] + + Audit --> Consensus["🗳️ AiMode.Consensus\nCross-LLM voting • Highest accuracy"] +``` + + +--- + +## Mode Deep-Dives & Code Examples + +### 1. `AiMode.Fallback` — Sequential Cascade *(Default)* + +Dispatches requests to providers sequentially in configured order until a provider succeeds and satisfies `minConfidence`. The most cost-efficient mode. + +**Best for:** Standard production baseline — general date parsing and background tasks. + +```typescript +const dt = await parseAI('first monday in october 2026', { + mode: AiMode.Fallback, + minConfidence: 0.85 // Automatically cascades to next provider if score is too low +}); +``` + +--- + +### 2. `AiMode.Hedged` — Speculative Latency Hedging + +Sends a request to the primary provider immediately. If no valid response arrives within `hedgeDelay` (default: `800ms`), launches a speculative concurrent request to the secondary provider. The first valid response wins; all in-flight requests are aborted. + +**Best for:** Latency-sensitive user-facing APIs — search bars, chatbots, web forms. + +```typescript +const dt = await parseAI('schedule team sync for next wednesday at 2pm', { + mode: AiMode.Hedged, + hedgeDelay: 600 // Launch hedge after 600ms if primary is still pending +}); +``` + +> [!TIP] +> `hedgeDelay` can also be set globally in `initAI({ hedgeDelay: 600 })` so it applies to all functions (`parseAI`, `recurrenceAI`, `scheduleAI`). + +--- + +### 3. `AiMode.RoundRobin` — Multi-Key Cyclic Load Balancing + +Cycles through the configured provider pool on each invocation (`0 → 1 → 2 → 0 ...`). If the selected starting provider fails, automatically falls over to the remaining providers in cyclic order. + +**Best for:** High-throughput batch processing — ingesting large volumes of dates across multiple API keys to avoid single-account RPM throttling. + +```typescript +await initAI({ + providers: [ + { id: 'groq-key-1', key: process.env.GROQ_KEY_1 }, + { id: 'groq-key-2', key: process.env.GROQ_KEY_2 }, + { id: 'groq-key-3', key: process.env.GROQ_KEY_3 } + ], + mode: AiMode.RoundRobin +}); +``` + +--- + +### 4. `AiMode.Adaptive` — Rate-Limit Telemetry Prioritization + +Reads `x-ratelimit-*` HTTP headers after every provider response and stores per-provider quota snapshots. On the next request, providers with `remainingRequests === 0` in an active reset window are automatically deprioritized; remaining providers are sorted by highest available quota. + +**Best for:** Multi-tier production gateways — mixed free/paid provider pools where proactively avoiding `429 Too Many Requests` is essential. + +```typescript +const dt = await parseAI('quarterly review deadline next quarter', { + mode: AiMode.Adaptive +}); +``` + +> [!NOTE] +> Telemetry accumulates across calls. The first request in a session uses original provider order; sorting kicks in from the second call onward once header data is available. + +--- + +### 5. `AiMode.Race` — Speculative Parallel Execution + +Dispatches requests concurrently across all configured providers. The fastest successful response is returned, and all remaining requests are immediately cancelled via `AbortSignal`. + +**Best for:** Real-time interactive typeahead — live search inputs or autocomplete where the fastest possible response is required regardless of token cost. + +```typescript +const dt = await parseAI('tomorrow at noon', { + mode: AiMode.Race +}); +``` + +--- + +### 6. `AiMode.Consensus` — Multi-LLM Cross-Validation + +Dispatches requests concurrently across all providers and compares the normalized ISO timestamps or RRULE strings. If all responding providers agree, confidence is elevated to `1.0` (unanimous). If providers disagree, the highest-confidence candidate is returned and flagged with `dt.ai.ambiguous = true`. + +**Best for:** High-stakes legal, financial, and scheduling — contract dates, event conflict resolution, or auditing where hallucination prevention requires unanimous LLM agreement. + +```typescript +const dt = await parseAI('contract renewal date', { + mode: AiMode.Consensus +}); + +if (dt.ai?.ambiguous) { + console.warn('Providers disagreed — treat this result with caution.'); +} +``` diff --git a/packages/plugins/ai/doc/parseAI.md b/packages/plugins/ai/doc/parseAI.md new file mode 100644 index 00000000..5189c5bb --- /dev/null +++ b/packages/plugins/ai/doc/parseAI.md @@ -0,0 +1,77 @@ +# `parseAI` — Natural Language Point-in-Time Parsing + +`parseAI()` is the primary entry point for converting complex, unstructured natural language date/time expressions into deterministic `Tempo` instances. + +## Basic Usage + +```typescript +import { parseAI, initAI } from '@magmacomputing/tempo-plugin-ai'; + +// Initialize AI providers +await initAI({ + providers: [ + { id: 'groq', key: process.env.GROQ_API_KEY } + ] +}); + +// Parse natural language +const dt = await parseAI("The penultimate Tuesday before Thanksgiving in 2026"); + +console.log(dt.format('{yyyy}-{mm}-{dd}')); // 2026-11-17 +console.log(dt.ai?.confidence); // 0.98 +``` + +## Options & Overrides + +`parseAI(input, options)` accepts per-request options: + +```typescript +const dt = await parseAI("Third Friday of October", { + anchor: '2026-05-10T12:00:00Z', // Anchor date for relative calculations + timeZone: 'Australia/Sydney', // Context timezone + locale: 'en-AU', // Context locale + minConfidence: 0.85, // Require at least 0.85 confidence score + timeout: 3000, // 3-second SLA call-site timeout + force: true, // Skip native pre-parsing & cache lookup + debug: true // Enable operational trace logging & .ai metadata +}); +``` + +## Multi-Provider Execution Modes + +`parseAI` supports all six multi-provider execution strategies (`fallback`, `race`, `consensus`, `hedged`, `roundrobin`, `adaptive`) configured globally or overridden per-request. + +For a comprehensive guide, decision flowcharts, and configuration details for all execution modes, refer to the [Multi-Provider Execution Modes Guide](./modes.md). + + +## Batch Array Parsing + +Pass an array of prompts to process multiple queries in parallel while preserving index ordering: + +```typescript +const [dt1, dt2] = await parseAI([ + "New Years Day 2026", + "Groundhog Day 2026" +]); + +console.log(dt1.format('{yyyy}-{mm}-{dd}')); // 2026-01-01 +console.log(dt2.format('{yyyy}-{mm}-{dd}')); // 2026-02-02 +``` + +## Diagnostic Metadata (`.ai`) + +When a date is parsed, a frozen diagnostic metadata object is attached to the returned `Tempo` instance: + +```typescript +console.log(dt.ai); +/* +{ + provider: 'groq', + cached: false, + confidence: 0.98, + ambiguous: false, + granularity: 'day', + rawIso: '2026-11-17T00:00:00' +} +*/ +``` diff --git a/packages/plugins/ai/doc/rate-limits.md b/packages/plugins/ai/doc/rate-limits.md index 1765ec3e..d8d10562 100644 --- a/packages/plugins/ai/doc/rate-limits.md +++ b/packages/plugins/ai/doc/rate-limits.md @@ -5,18 +5,35 @@ When using third-party AI APIs, your application is subject to strict rate limit The plugin automatically tracks these limits by reading the standard `x-ratelimit-*` HTTP headers returned by providers like OpenAI and Groq. ## Tracking Quota Real-time -To expose this data without ruining the clean return signatures of Tempo AI functions, the plugin provides a dedicated utility function: `getAiRateLimits()`. + +Quota and rate-limit metadata can be inspected in two convenient ways: + +### 1. Request-Locked Instance Metadata (`dt.ai.limits`) +Every `Tempo` instance returned by `parseAI` includes a frozen `.ai.limits` snapshot representing the rate limit state returned by provider HTTP headers for *that specific request*. Note that `.ai.limits` is guaranteed only for provider-backed network requests where rate-limit headers are returned by the selected provider; results resolved via native parsing (`provider: 'native'`) or cache hits (`provider: 'cache'`) may omit `.ai.limits` (`undefined`). + +```typescript +const dt = await parseAI("The third Friday of next month"); + +if (dt.ai?.limits) { + console.log(`Remaining Tokens: ${dt.ai.limits.remainingTokens}`); + console.log(`Remaining Requests: ${dt.ai.limits.remainingRequests}`); + console.log(`Resets At: ${dt.ai.limits.resetAt?.format('{hh}:{mi}:{ss}')}`); +} +``` + +### 2. Global State Utility (`getAiRateLimits()`) +For quick status checks or global monitoring across the application lifecycle, `getAiRateLimits()` exposes the stats from the most recent LLM request: ```typescript import { getAiRateLimits } from '@magmacomputing/tempo-plugin-ai'; -// Returns the stats from the most recent LLM proxy request +// Returns global stats from the most recent LLM proxy request const stats = getAiRateLimits(); if (stats) { console.log(`Remaining Tokens: ${stats.remainingTokens}`); console.log(`Remaining Requests: ${stats.remainingRequests}`); - console.log(`Limits Reset At: ${stats.resetAt.format('{hh}:{mi}:{ss}')}`); + console.log(`Limits Reset At: ${stats.resetAt?.format('{hh}:{mi}:{ss}')}`); } ``` @@ -108,15 +125,55 @@ If you want to explicitly query the LLM again and *overwrite* the existing cache const dt = await parseAI("Q3_START", { force: true }); ``` -### Extensible Caching (Enterprise) -For edge environments or custom application architectures, you can provide custom cache instances via `initAI({ cache })` or `Tempo.init({ cache })`! +### Extensible Caching & Async Storage Adapters (`AiCacheAdapter`) -You can provide any object that implements the standard **synchronous** `Map` interface (`get`, `set`, `has`, `delete`). Note that all cache adapter methods must execute synchronously, as the cache lookup engine does not await promise-returning cache operations. +By default, parsed AI responses are cached in memory using `Tempo.cache` (`BoundedCache`). For distributed serverless environments (e.g. Next.js, Cloudflare Workers, Express) or cluster nodes, you can pass a custom synchronous or asynchronous storage adapter (`AiCacheAdapter`): ```typescript -// Custom synchronous cache implementation +import { initAI, parseAI, type AiCacheAdapter } from '@magmacomputing/tempo-plugin-ai'; +import { Redis } from '@upstash/redis'; + +const redis = new Redis({ url: process.env.UPSTASH_URL!, token: process.env.UPSTASH_TOKEN! }); + +// Implement custom async Redis storage adapter with namespacing & prefix deletion support +const redisAdapter: AiCacheAdapter = { + get: async (key) => (await redis.get(`tempo:ai:${key}`)) ?? undefined, + set: async (key, value, ttlMs) => { + if (ttlMs !== undefined) await redis.set(`tempo:ai:${key}`, value, { px: ttlMs }); + else await redis.set(`tempo:ai:${key}`, value); + }, + delete: async (key) => { + await redis.del(`tempo:ai:${key}`); + }, + clear: async (prefix) => { + const pattern = prefix ? `tempo:ai:${prefix}*` : `tempo:ai:*`; + let cursor = '0'; + do { + const [nextCursor, keys] = await redis.scan(cursor, { match: pattern, count: 100 }); + cursor = nextCursor; + if (keys.length > 0) await redis.del(...keys); + } while (cursor !== '0'); + } +}; + initAI({ - providers: [{ id: 'groq', key: '...' }], - cache: new MyCustomSyncCache() + providers: [{ id: 'groq', key: process.env.GROQ_API_KEY!, ttl: 7200000 }], // Provider-specific TTL (2 hours) + cacheAdapter: redisAdapter, + ttl: 3600000 // Global default TTL (1 hour) }); + +// Call-site TTL override (15 minutes) +const dt = await parseAI("next Monday at 9am", { ttl: 900000 }); ``` + +### Cascading TTL Resolution Policies + +The plugin calculates cache TTL per entry using a strict resolution hierarchy: +1. **Call-site `options.ttl`**: `parseAI(prompt, { ttl: 900000 })` +2. **Provider-level `provider.ttl`**: `providers: [{ id: 'groq', ttl: 7200000 }]` +3. **Global `initAI({ ttl: 3600000 })`** +4. **Default TTL**: `3,600,000` ms (1 hour) + +### Fail-Open Cache Resilience + +Custom storage adapter calls (`adapter.get` and `adapter.set`) are wrapped in safe error handlers. If an external Redis instance crashes or encounters a network partition, the plugin logs a debug warning (if `debug: true`) and gracefully fails open to direct LLM resolution without crashing the application request. diff --git a/packages/plugins/ai/doc/recurrenceAI.md b/packages/plugins/ai/doc/recurrenceAI.md new file mode 100644 index 00000000..e2127cf8 --- /dev/null +++ b/packages/plugins/ai/doc/recurrenceAI.md @@ -0,0 +1,110 @@ +# `recurrenceAI` — Recurrence Rules & Schedule Translation + +`recurrenceAI()` provides multi-directional translation between natural language repeating schedule descriptions (*"Every 2nd Tuesday of the month at 3pm"*) and RFC 5545 **RRULE strings**, generating paged `Tempo` instance batches on demand. + +## Basic Usage + +```typescript +import { recurrenceAI, initAI } from '@magmacomputing/tempo-plugin-ai'; + +// 1. Initialize provider configuration +await initAI({ + providers: [{ id: 'groq', key: process.env.GROQ_API_KEY }] +}); + +// 2. Natural Language Input (human-in -> RRule & Tempo batches out) +const result = await recurrenceAI("Every 2 weeks on Friday at 9am", { + locale: 'fr-FR', // Output localized human summary + count: 5 // Default batch size +}); + +console.log(result.rrule); // "FREQ=WEEKLY;INTERVAL=2;BYDAY=FR;BYHOUR=9" +console.log(result.summary); // "Chaque 2 semaines le vendredi à 09:00" +console.log(result.isFinite); // false (recurs indefinitely) +console.log(result.size); // Infinity +``` + +## Stateful Paged Batching (`.take(n)`) + +`recurrenceAI` maintains an internal date cursor. Calling `.take(n)` repeatedly returns consecutive batches of `Tempo` instances: + +```typescript +// Fetch initial batch of 5 items +const batch1 = result.take(5); +console.log(batch1.length); // 5 + +// Fetch NEXT batch of 5 items starting right where batch 1 left off +const batch2 = result.take(5); +console.log(batch2.length); // 5 +``` + +When a finite schedule (e.g. `COUNT=10`) completes, `.take(n)` returns an empty array `[]` to signal exhaustion: + +```typescript +const finiteResult = await recurrenceAI("FREQ=MONTHLY;BYDAY=1MO;COUNT=2"); + +const b1 = finiteResult.take(2); // [ Tempo(Month 1), Tempo(Month 2) ] +const b2 = finiteResult.take(2); // [] (Exhausted) +``` + +## Native RRULE Parsing (Zero Network Overhead) + +Passing a raw RFC 5545 RRULE string directly to `recurrenceAI` bypasses network LLM calls entirely (`provider: 'rrule-parser'`), functioning as an instant native parser: + +```typescript +const native = await recurrenceAI("FREQ=MONTHLY;BYDAY=1MO;COUNT=12"); + +console.log(native.provider); // "rrule-parser" (Instant native resolution) +console.log(native.isFinite); // true +console.log(native.size); // 12 +``` + +## Lazy Iteration (`for...of`) + +`TempoRecurrenceResult` implements `[Symbol.iterator]`, allowing lazy iteration over occurrences up to the batch limit (`count: 5` by default). + +When iterating over open-ended schedules (`isFinite === false`), build a `break` termination clause into the loop: + +```typescript +const schedule = await recurrenceAI("Every Friday"); + +for (const occurrence of schedule) { + // Always include a termination condition for open-ended schedules + if (occurrence.year > 2028) break; + + console.log(occurrence.format('{yyyy}-{mm}-{dd}')); +} +``` + +## Result Interface + +```typescript +export interface TempoRecurrenceResult { + /** Standard RFC 5545 RRULE string (e.g. 'FREQ=WEEKLY;BYDAY=TU') */ + rrule: string; + + /** Localized human-friendly schedule summary */ + summary: string; + + /** Reasoning / explanation of how the recurrence pattern was parsed */ + reasoning?: string; + + /** True if schedule has an explicit end date or count limit; false if infinite */ + isFinite: boolean; + + /** Total count of occurrences if finite, or Infinity (Number.POSITIVE_INFINITY) */ + size: number; + + /** Advances cursor and returns the next batch of N Tempo instances */ + take(count?: number): Tempo[]; + + /** Lazy generator yielding Tempo instances */ + [Symbol.iterator](): Generator; + + /** Confidence score (0.0 to 1.0) */ + confidence: number; + + /** Provider ID responsible for processing or 'rrule-parser' */ + provider: string; +} +``` diff --git a/packages/plugins/ai/doc/scheduleAI.md b/packages/plugins/ai/doc/scheduleAI.md new file mode 100644 index 00000000..d0760c6b --- /dev/null +++ b/packages/plugins/ai/doc/scheduleAI.md @@ -0,0 +1,96 @@ +# `scheduleAI` — Intelligent Appointment Booking & Conflict Resolution + +`scheduleAI()` provides automated slot booking, calendar conflict detection, working hour verification, and iterative slot bumping. It evaluates a natural language booking request (e.g. *"45 min sync next Wednesday afternoon"*) alongside a list of existing busy intervals and working hours to find the best available appointment slots. + +--- + +## Basic Usage + +```typescript +import { scheduleAI, initAI } from '@magmacomputing/tempo-plugin-ai'; + +// 1. Initialize provider +await initAI({ + providers: [{ id: 'groq', key: process.env.GROQ_API_KEY }] +}); + +// 2. Schedule a meeting avoiding team standups +const booking = await scheduleAI("45 min sync next Wednesday afternoon", { + anchor: "2026-08-10 09:00", + timeZone: "America/New_York", + events: [ + { start: "2026-08-12 14:00", end: "2026-08-12 15:00", title: "Team standup" } + ], + workingHours: { start: "09:00", end: "17:00" } // only match New York business hours +}); + +console.log(booking.start.format('{yyyy}-{mm}-{dd} {hh}:{mi}')); // 2026-08-12 15:00 +console.log(booking.end.format('{yyyy}-{mm}-{dd} {hh}:{mi}')); // 2026-08-12 15:45 +console.log(booking.durationMinutes); // 45 +console.log(booking.ai?.conflictBumped); // true (pushed past team standup) +``` + +--- + +## Configuration Options (`AiScheduleOptions`) + +| Option | Type | Description | +| :--- | :--- | :--- | +| **`anchor`** | `Tempo \| Date \| string \| number` | Anchor reference time to evaluate relative dates from. Defaults to workstation/browser current time. | +| **`events`** | `TempoEvent[]` | A list of existing busy calendar intervals that the meeting must not overlap with. | +| **`workingHours`** | `{ start: string; end: string }` | Daily time window constraint (HH:MM formats) inside which slots must fit. | +| **`timeZone`** | `string` | Target IANA timezone to calculate the booking slot and boundaries within. | +| **`minConfidence`**| `number` | Minimum confidence score threshold (0.0 to 1.0) required to return a valid slot. | +| **`mode`** | `AiMode` | Concurrency routing strategy (`fallback`, `race`, `consensus`, `hedged`, `roundrobin`, `adaptive`). Refer to the [Multi-Provider Execution Modes Guide](./modes.md). | + +### `TempoEvent` Interface +```typescript +interface TempoEvent { + start: Tempo | Date | string | number; + end: Tempo | Date | string | number; + title?: string; +} +``` + +--- + +## Result Schema (`TempoScheduleResult`) + +```typescript +export interface TempoScheduleResult { + /** The calculated start timestamp of the selected slot */ + start: Tempo; + + /** The calculated end timestamp of the selected slot */ + end: Tempo; + + /** The resolved slot represented as a Tempo Interval object */ + slot: Interval; + + /** The duration of the resolved meeting in minutes */ + durationMinutes: number; + + /** Alternative available slot intervals that also satisfy all constraints */ + alternatives: Interval[]; + + /** Metadata on the AI resolution process */ + ai?: { + provider: string; + confidence: number; + conflictBumped: boolean; // True if the slot was pushed/bumped to avoid a busyEvent conflict + reasoning?: string; + }; +} +``` + +--- + +## Internal Conflict Bumping Engine + +When an LLM proposes a slot (like Wednesday at 2:00 PM), `scheduleAI` doesn't just trust it blindly. It runs a deterministic check using Tempo's native `Interval.overlaps()` method against your `events` list: +1. If the proposed slot overlaps with any busy event, the engine shifts (bumps) the slot forward iteratively. +2. It re-verifies the new slot against all other busy events and working hours constraints. +3. If it successfully lands on a free spot, it marks `ai.conflictBumped: true`. +4. If no free spot can be found within the proposed date frame, it returns a failed interval with low confidence. + +This decoupled architecture ensures that the LLM is used for parsing intent (finding the duration and preferred time slot) while Tempo's math engine handles the strict conflict prevention. diff --git a/packages/plugins/ai/package.json b/packages/plugins/ai/package.json index a580adec..59d49efd 100644 --- a/packages/plugins/ai/package.json +++ b/packages/plugins/ai/package.json @@ -1,6 +1,6 @@ { "name": "@magmacomputing/tempo-plugin-ai", - "version": "0.2.0", + "version": "0.3.0", "description": "Tempo community plugin for LLM-powered natural language parsing.", "main": "dist/index.js", "types": "dist/index.d.ts", @@ -17,7 +17,7 @@ }, "scripts": { "build": "tsup", - "test": "vitest run -c ../vitest.shared.ts", + "test": "cross-env TEMPO_LICENSE_KEY=\"\" vitest run -c ../vitest.shared.ts", "prepublishOnly": "if [ $(git rev-parse --abbrev-ref HEAD) != main ]; then echo 'ERROR: Must be on main branch to publish.'; exit 1; fi && npm run build" }, "tempo": { @@ -25,7 +25,7 @@ "plan": "community" }, "peerDependencies": { - "@magmacomputing/tempo": "^3.11.0" + "@magmacomputing/tempo": "^3.11.1" }, "devDependencies": { "@js-temporal/polyfill": "^0.5.1" diff --git a/packages/plugins/ai/plan/extractAI.plan.md b/packages/plugins/ai/plan/extractAI.plan.md new file mode 100644 index 00000000..43e8e1ef --- /dev/null +++ b/packages/plugins/ai/plan/extractAI.plan.md @@ -0,0 +1,137 @@ +# Implementation Plan: `extractAI` + +## 1. Overview & Goal +`extractAI` scans unstructured, multi-paragraph text (emails, transcripts, chat logs, meeting agendas, task notes) to identify, parse, and extract all embedded temporal entities and time-bound events into structured `TempoAiExtractResult` records containing `TempoExtractedEvent[]` (`label`, `start`, `end`, `type`, `rawText`, `confidence`). + +It anchors relative mentions (e.g., *"tomorrow at 2pm"*, *"next Tuesday from 9 to 11am"*, *"the last day of next month"*) against an explicit or current reference `anchor` timestamp and timezone. + +--- + +## 2. Public API & Type Definitions + +### 2.1 Types (`packages/plugins/ai/src/types/extract.type.ts`) +```typescript +import type { Tempo } from '@magmacomputing/tempo'; +import type { AiOptions } from './common.type.js'; +import type { TempoAiError } from '../core/error.js'; + +export type TempoEventType = 'point' | 'interval' | 'deadline' | 'recurrence' | 'tentative'; + +export interface TempoExtractedEvent { + /** Short descriptive label or title of the extracted event/activity. */ + label: string; + /** Start date-time point as an instantiated Tempo instance. */ + start: Tempo; + /** Optional end date-time point (if an interval or duration was mentioned). */ + end?: Tempo; + /** Classification category of the temporal mention. */ + type: TempoEventType; + /** Raw text snippet extracted from the source document. */ + rawText?: string; + /** Confidence score for this specific entity extraction (0.0 to 1.0). */ + confidence: number; +} + +/** Backward compatibility alias for TempoExtractedEvent */ +export type TempoEvent = TempoExtractedEvent; + +export interface TempoAiExtractResult { + /** Array of extracted events with instantiated Tempo objects. */ + events: TempoExtractedEvent[]; + /** Overall confidence score. */ + confidence: number; + /** Provider ID that fulfilled the request (or 'cache'). */ + provider: string; + /** Optional summary or reasoning from the LLM. */ + reasoning?: string; +} + +export interface AiExtractOptions extends AiOptions { + /** Reference anchor date-time for relative expressions (defaults to now). */ + anchor?: Tempo | Date | string | number; + /** Reference IANA timezone (defaults to global options or 'UTC'). */ + timeZone?: string; + /** Reference BCP 47 locale (defaults to global options or 'en-US'). */ + locale?: string | string[]; + /** Preferred calendar system (e.g. 'gregory', 'islamic', 'hebrew'). */ + calendar?: string; + /** Optional category filter to restrict extracted entities (e.g. ['meeting', 'deadline']). */ + categories?: string[]; + /** Optional regional context (e.g. 'US-NY', 'GB'). */ + region?: string; +} +``` + +### 2.2 Function Signature (`packages/plugins/ai/src/functions/extract.ts`) +```typescript +export async function extractAI(texts: string[], options?: AiExtractOptions): Promise<(TempoAiExtractResult | TempoAiError)[]>; +export async function extractAI(text: string, options?: AiExtractOptions): Promise; +``` + +--- + +## 3. Grounding & Prompt Strategy + +### 3.1 Context Construction +Pass anchor metadata so the LLM has a solid temporal baseline: +* **Reference Anchor**: `anchorTempo.format('{yyyy}-{mm}-{dd}T{hh}:{mi}:{ss}')` (`anchorTempo.tz`) +* **Reference Day of Week**: `anchorTempo.dow` / weekday name +* **Current Year / Month / Day**: Pre-resolved ISO components +* **Target Categories / Constraints**: e.g., Filter meetings, deadlines, or flights + +### 3.2 System Prompt & Schema +```markdown +You are an expert temporal entity extraction engine. +Scan the user-provided text for all temporal expressions, deadlines, meetings, and intervals. +Resolve relative references ("tomorrow", "next Monday", "in 2 hours") strictly against the Reference Anchor date and timezone. + +Return ONLY a valid JSON object matching this schema: +{ + "events": [ + { + "label": "Brief descriptive title", + "start": "ISO 8601 string (e.g. 2026-08-13T14:00:00)", + "end": "ISO 8601 string or null", + "type": "point | interval | deadline | recurrence | tentative", + "rawText": "Exact text fragment from the input", + "confidence": 0.95 + } + ], + "confidence": 0.95, + "reasoning": "Identified 2 scheduled meetings and 1 project deadline." +} +``` + +### 3.3 Post-Processing & Validation +1. For each item in `events`, validate `start` using `new Tempo(item.start, { timeZone: tz, locale: loc, calendar: cal })`. +2. If `item.end` is present, construct `new Tempo(item.end, { timeZone: tz, locale: loc, calendar: cal })`. +3. Filter out invalid date results gracefully. +4. Ensure returned `start` and `end` are native `Tempo` instances for immediate date arithmetic. + +--- + +## 4. Caching & Dispatch Pipeline + +1. **Cache Key Partition**: + `extract::${normalizedTextHash}::${anchorTempo.format('{yyyy}-{mm}-{dd}')}::${tz}::${loc}::${cal}::${region}` +2. **Multi-tier Caching**: + - Check `AiCacheAdapter` then `Tempo.cache`. + - Reconstitute cached ISO strings into `Tempo` instances upon cache hit. +3. **Execution Modes**: + - Dispatch via `executeWithMode` supporting all 6 modes. +4. **Batch Processing**: + - `Promise.all` / `Promise.allSettled` (with `softErrors` normalization). + +--- + +## 5. Verification & Test Plan +* **Unit Tests (`packages/plugins/ai/test/extract.test.ts`)**: + - Extract multiple events from email text (e.g., meeting + follow-up deadline). + - Resolve relative dates against custom `anchor` timestamps. + - Interval extraction with start and end times. + - Handle inputs containing no temporal entities (returns `events: []`). + - Cache hit rehydration into `Tempo` instances. + - Multi-provider execution modes (Fallback, Race, Adaptive). + - Batch processing with `softErrors: true`. +* **Documentation (`packages/plugins/ai/doc/extractAI.md`)**: + - TSDoc, usage examples with email text, calendar creation, and options guide. diff --git a/packages/plugins/ai/plan/formatAI.plan.md b/packages/plugins/ai/plan/formatAI.plan.md new file mode 100644 index 00000000..dcaa60ee --- /dev/null +++ b/packages/plugins/ai/plan/formatAI.plan.md @@ -0,0 +1,116 @@ +# Implementation Plan: `formatAI` + +## 1. Overview & Goal +`formatAI` transforms a `Tempo` instance, `Date`, timestamp, or ISO string into human-friendly, contextual narrative text tailored to specific prompts, UI tones, business domains, or relative countdown styles (e.g., *"this Friday at 5:00 PM EST (in 5 days)"*, *"Q3 Fiscal Close — 14 business days remaining"*). + +By combining deterministic date-time grounding (formatted ISO components, day of week, relative difference to anchor/now, season, quarter) with LLM prompt execution, `formatAI` eliminates date hallucination while delivering expressive, localized language. + +--- + +## 2. Public API & Type Definitions + +### 2.1 Types (`packages/plugins/ai/src/types/format.type.ts`) +```typescript +import type { Tempo } from '@magmacomputing/tempo'; +import type { AiOptions } from './common.type.js'; +import type { TempoAiError } from '../core/error.js'; + +export interface AiFormatOptions extends AiOptions { + /** Reference anchor date for relative calculations (defaults to now). */ + anchor?: Tempo | Date | string | number; + /** Target IANA timezone (defaults to Tempo instance timezone or global options). */ + timeZone?: string; + /** Target BCP 47 locale or language tag (defaults to global options or 'en-US'). */ + locale?: string | string[]; + /** Desired narrative tone or formatting style hint (e.g. 'casual', 'formal', 'compact', 'countdown'). */ + style?: string; + /** Custom regional context (e.g. 'AU-NSW', 'US-CA'). */ + region?: string; +} + +export interface FormatItem { + /** Date-time instance or string to format. */ + date: Tempo | Date | string | number; + /** Prompt instructions guiding the output narrative. */ + prompt?: string; +} + +export interface TempoAiFormatResult { + /** Formatted narrative string. */ + formatted: string; + /** Confidence score between 0.0 and 1.0. */ + confidence: number; + /** ID of the provider that fulfilled the request (or 'cache'). */ + provider: string; + /** Optional step-by-step rationale from the LLM. */ + reasoning?: string; +} +``` + +### 2.2 Function Signature (`packages/plugins/ai/src/functions/format.ts`) +```typescript +export async function formatAI(items: FormatItem[], options?: AiFormatOptions): Promise<(TempoAiFormatResult | TempoAiError)[]>; +export async function formatAI(date: any, prompt?: string, options?: AiFormatOptions): Promise; +``` + +--- + +## 3. Mathematical Grounding & Prompt Strategy + +### 3.1 Grounding Calculation +To guarantee accuracy, pre-compute: +* **Canonical ISO Representation**: `targetTempo.format('{yyyy}-{mm}-{dd}T{hh}:{mi}:{ss}')` +* **Target Timezone & Offset**: `targetTempo.tz`, `targetTempo.offset` +* **Day of Week & Ordinal**: `targetTempo.dow` (1=Mon, 7=Sun), weekday name +* **Relative Delta to Anchor**: + * Calendar days: `Math.round(anchorTempo.until(targetTempo, 'day') * 100) / 100` + * Elapsed hours: `Math.round(anchorTempo.until(targetTempo, 'hour') * 100) / 100` + * Relative direction: Past / Present / Future + +### 3.2 Context & System Prompt +```markdown +Grounding Context: +- Target Date-Time: 2026-08-14T17:00:00 (America/New_York) +- Day of Week: Friday (Day 5) +- Reference Anchor: 2026-08-12T08:00:00 (America/New_York) +- Relative Delta: +2.38 days (+57.0 hours) in the Future +- Target Locale: en-US +- Formatting Style: casual +- Prompt: "Express as an upcoming meeting reminder with relative countdown" +``` + +Schema enforcement: +```json +{ + "formatted": "this Friday at 5:00 PM EST (in 2 days)", + "confidence": 0.98, + "reasoning": "Target timestamp is in 2 days on Friday afternoon." +} +``` + +--- + +## 4. Caching & Dispatch Pipeline + +1. **Cache Key Partition**: + `format::${targetTempo.epoch.ms}::${anchorTempo.epoch.ms}::${normalizedPrompt}::${tz}::${loc}::${style}::${region}` +2. **Multi-tier Caching**: + - Check `AiCacheAdapter` (Redis / Cloudflare KV) then local `Tempo.cache`. + - Validate non-empty `formatted` and check `effectiveMinConfidence`. +3. **Execution Modes**: + - Dispatch via `executeWithMode` supporting all 6 modes (`Fallback`, `Race`, `Consensus`, `Hedged`, `RoundRobin`, `Adaptive`). +4. **Batch Processing**: + - Concurrent `Promise.all` / `Promise.allSettled` (with `softErrors: true` normalizing rejections to `TempoAiError`). + +--- + +## 5. Verification & Test Plan +* **Unit Tests (`packages/plugins/ai/test/format.test.ts`)**: + - Valid date formatting across diverse prompt instructions (business SLA, casual relative, compact countdown). + - Timezone normalization (preserves target instant in requested timeZone). + - Cache hit preservation and region/style cache isolation. + - Multi-provider execution modes (Race, Consensus, Hedged). + - Batch array processing with `softErrors: true`. + - Confidence threshold rejection (`minConfidence`). +* **Documentation (`packages/plugins/ai/doc/formatAI.md`)**: + - TSDoc, basic usage with `initAI`, options guide, batch examples. diff --git a/packages/plugins/ai/plan/v0.3.0-roadmap.md b/packages/plugins/ai/plan/v0.3.0-roadmap.md index fc3e0aa4..3a9bbfa9 100644 --- a/packages/plugins/ai/plan/v0.3.0-roadmap.md +++ b/packages/plugins/ai/plan/v0.3.0-roadmap.md @@ -4,103 +4,63 @@ This document captures the planned feature set, architectural requirements, and --- -## 1. Remote Provider Manifest & Dynamic Defaults - -### Background & Objective -AI provider model identifiers, endpoints, and parameters update frequently. Hardcoding model defaults into the published NPM package requires frequent patch releases. v0.3.0 will introduce remote provider manifest fetching. - -### Requirements & Architecture -* **Hosted Manifest Endpoint**: Host a static `providers.v1.json` manifest on the Firebase-hosted Tempo Registry (`https://registry.tempo.dev/ai/providers.v1.json`). -* **Lifecycle & Caching**: - * Fetch occurs **once** per application lifecycle / module load (lazy-evaluated on first `initAI()` call). - * Manifest response is cached in module-scoped memory (`_remoteDefaults`). - * Re-calling `initAI()` reads from in-memory cache without triggering new network requests. -* **Fail-Open & Offline Support**: - * If the network request fails, times out, or the client is offline/air-gapped, `initAI()` synchronously falls back to compiled local `DEFAULT_PROVIDERS`. -* **Developer Override Options**: - ```typescript - initAI({ - providers: [{ id: 'openai', key: '...' }], - remoteConfigUrl: 'https://custom-registry.internal.net/ai/providers.json', // Custom endpoint - fetchDefaults: async (providerId) => { ... } // Custom resolver hook - }); - ``` +## 1. AI Function Handler Implementations (v0.3.0 Status) ---- - -## 2. Implementation of Scaffolded AI Function Handlers - -In v0.2.0, upcoming function handlers were scaffolded with `@internal` JSDoc tags and `not yet implemented` guards. v0.3.0 will implement the following functions: - -### 2.1 `formatAI(tempo: Tempo, prompt: string, options?: AiOptions): Promise` -* Formats a `Tempo` instance into human-friendly, contextual narrative text tailored to UI tones or relative countdowns. -* **Example**: `"this Friday at 5:00 PM EST (in 5 days)"`. +### 1.1 ✅ `scheduleAI(prompt: string, options?: TempoScheduleOptions): Promise` +* Resolves natural language scheduling prompts against working hours, existing calendar events, and timezones into an optimal start/end `TempoScheduleResult` interval (`slot`, `alternatives`, `ai.conflictBumped`). -### 2.2 `extractAI(text: string, options?: AiOptions): Promise` -* Scans unstructured text (emails, transcripts, task notes) to extract embedded temporal entities into structured `TempoEvent` records (`label`, `start`, `end`, `type`). +### 1.2 ✅ `recurrenceAI(prompt: string, options?: TempoRecurrenceOptions): Promise` +* Translates complex natural language repeating schedule descriptions into standard RFC 5545 RRULE strings and stateful `Tempo` date batches (`rule.take(count)`). -### 2.3 `diffAI(start: Tempo, end: Tempo, prompt?: string, options?: AiOptions): Promise` -* Calculates and summarizes the delta between two `Tempo` instances in human, business, or operational terms (e.g., `"6 working business days (48 hours)"`). - -### 2.4 `scheduleAI(prompt: string, options?: AiScheduleOptions): Promise` -* Resolves natural language scheduling prompts against working hours, existing calendar events, and timezones into an optimal start/end `Tempo` interval. - -### 2.5 `recurrenceAI(prompt: string, options?: AiOptions): Promise` -* Translates complex natural language repeating schedule descriptions into standard RRULE strings and `Tempo` instance date generators (`rule.next(count)`). - -### 2.6 `contextAI(text: string, options?: AiOptions): Promise` +### 1.3 ✅ `contextAI(text: string, options?: AiContextOptions): Promise` * Infers `timeZone`, `locale`, and preferred `calendar` system from ambiguous location descriptions or user bios. ---- - -## 3. Telemetry & Token Usage Tracking - -Extend the `.ai` metadata property attached to returned `Tempo` instances to include token consumption metrics: -```typescript -interface AiMeta { - provider: string; - cached: boolean; - confidence: number; - ambiguous: boolean; - granularity: string; - usage?: { - promptTokens: number; - completionTokens: number; - totalTokens: number; - }; -} -``` - ---- - -## 4. Advanced Cache TTL & Eviction Policies - -* Allow per-provider or per-request TTL configurations in `initAI({ ttl: 3600000 })`. -* Support optional async storage adapters (e.g., Redis / KV stores) via explicit async wrapper interfaces. +### 1.4 ✅ `diffAI(start: any, end: any, prompt?: string, options?: AiDiffOptions): Promise` +* Calculates and summarizes the delta between two `Tempo` instances in human, business, or operational terms (e.g., `"5 business days (48 hours)"`), backed by native grounding metrics (calendar days, hours, business days with weekend & holiday exclusion). --- -## 5. Tempo Config Assistant (`configAI`) — Strategic & Technical Analysis +## 2. Upcoming AI Function Handlers (Post-v0.3.0 Roadmap) -### 5.1 Objective & Overview -`configAI(prompt: string, options?: AiOptions): Promise` is a proposed developer-facing function to translate natural language configuration requirements (e.g., *"Set up fiscal quarters starting Nov 1 and a layout for short weekday with 12-hour time"*) into valid Tempo runtime configurations (`Aliases`, `Layouts`, `Snippets`, `Formats`, or `TermPlugin` declarations). +The following functions remain scaffolded for upcoming releases: -### 5.2 Business & Consulting Revenue Impact -* **Consulting & Support Cannibalization**: Providing automated, self-serve AI config generation reduces developer reliance on Magma's high-margin professional services, enterprise consulting, and tier-3 support packages for custom domain-math integrations. -* **Monetization & Upsell Safeguards**: If implemented, `configAI` must act as an upsell vehicle, surfacing recommendations for Magma's licensed premium plugins (e.g., `@magmacomputing/tempo-plugin-ticker`, `@magmacomputing/tempo-plugin-sync`) whenever multi-interval scheduling or atomic clock synchronization is requested. - -### 5.3 Technical Context Gap & Smart Endpoint Requirement -* **The LLM Knowledge Gap**: Because `Tempo` introduces novel Temporal concepts, custom tokens (`{yw}`, `{wy}`, `{eon}`), and zero-cost proxy getters not broadly indexed across web codebases, standard foundation models (e.g., generic ChatGPT/Chrome LLM queries) lack sufficient context to generate valid Tempo code without hallucinating syntaxes. -* **Dedicated RAG / System-Prompt Endpoint**: To function accurately, `configAI` requires either: - * An embedded, highly compressed system prompt containing Tempo's exact token/layout grammar. - * A managed backend RAG endpoint (`https://registry.tempo.dev/ai/config`) hosted via Magma's license registry infrastructure. +### 2.1 `formatAI(date: any, prompt?: string, options?: AiFormatOptions): Promise` +* Formats a `Tempo` instance into human-friendly, contextual narrative text tailored to UI tones or relative countdowns. +* **Example**: `"this Friday at 5:00 PM EST (in 5 days)"`. -### 5.4 Ecosystem Adoption Lifecycle -* **Early Adoption (High Value)**: During early ecosystem growth, `configAI` serves as a critical bridge while public LLM training sets lack native Tempo awareness. -* **Widespread Acceptance (Declining Relevance)**: As Tempo adoption scales across public repositories and developer docs, foundation models will naturally absorb Tempo's API surface into their training sets. Over time, generic LLMs will generate valid Tempo configurations out-of-the-box, rendering a specialized `configAI` assistant progressively redundant. +### 2.2 `extractAI(text: string, options?: AiExtractOptions): Promise` +* Scans unstructured text (emails, transcripts, task notes) to extract embedded temporal entities into structured `TempoAiExtractResult` records (`events: TempoExtractedEvent[]`). -### 5.5 Verdict & Recommended Strategy: Deprioritized (Prefer `llms.txt`) -* **Decision**: **Do not build a runtime `configAI()` endpoint.** The ongoing engineering cost, RAG hosting infrastructure, and revenue cannibalization risks far outweigh the short-term benefits. -* **Low-Cost Alternative (`llms.txt` / Cursor Rules)**: Rather than maintaining a custom AI endpoint, publish an official `llms.txt` and Cursor/Copilot context file alongside the VitePress documentation. Developer IDEs (Cursor, Copilot, ChatGPT) will consume the docs directly at zero infrastructure cost to Magma, leaving engineering resources focused on core performance and licensed premium plugins. +--- +Conceptually, the execution modes actually govern two different, orthogonal concerns: + +Concurrency Strategy (How do we invoke providers?): + +Sequential: Call one-by-one (e.g., Fallback, RoundRobin, Adaptive). +Speculative: Call with a staggered delay (e.g., Hedged). +Parallel: Call all at once (e.g., Race, Consensus). +Provider Prioritization Strategy (Which providers do we call, and in what order?): + +Static: Configured order (e.g., Fallback, Hedged, Race, Consensus). +Cyclic: Round-robin rotation (e.g., RoundRobin). +Telemetry-Aware: Sorted dynamically by remaining rate-limit quota (e.g., Adaptive). +Should Telemetry (Adaptive) be "on" for all modes? +Yes! Conceptually, Telemetry-Awareness (filtering out exhausted providers in an active cooldown window) should ideally be active globally, regardless of the concurrency strategy: + +In Hedged / Fallback: Instead of starting with a hardcoded primary provider (which might have 0 remaining requests), we should start with the provider that telemetry reports has the highest quota, and hedge to the one with the second-highest. +In Race / Consensus: If we know a provider is currently rate-limited (in a 429 cooldown reset window), launching a request to it is a waste of network resources and will immediately fail. We should filter it out of the race/consensus pool before firing. +Why it isn't "on" everywhere by default in practice +Stale Telemetry Risks: Telemetry depends on header ingestion from previous calls. If the reset window is short (e.g., resets in 2 seconds), but our cache reports "exhausted" for another 3 seconds, we might unnecessarily skip a provider that has recovered. Sequential fallback acts as a natural check. +Simple/Deterministic expectation: Users choosing Race or Fallback often expect absolute determinism based strictly on their provider configuration array order. +Single-Provider setups: For single-provider setups, telemetry-aware prioritization has no effect. +Future Design Direction +If we wanted to support combining them in a future version of Tempo, we could decouple the selection strategy from the concurrency mode: +```typescript +await initAI({ + mode: AiMode.Hedged, // How we invoke + prioritization: 'telemetry' // How we order/filter (static | cyclic | telemetry) +}); +``` +For the current codebase, keeping them as separate named presets (RoundRobin vs. Adaptive) keeps the setup simple and easy to reason about, but we could certainly update the other strategies to check for and skip active cooldown limits in a future update! \ No newline at end of file diff --git a/packages/plugins/ai/src/core/config.ts b/packages/plugins/ai/src/core/config.ts index 947f42ec..9a963d6c 100644 --- a/packages/plugins/ai/src/core/config.ts +++ b/packages/plugins/ai/src/core/config.ts @@ -1,4 +1,5 @@ -import type { AiProvider } from './types.js'; +import { secure } from '@magmacomputing/tempo/library'; +import type { AiProvider } from '../types/index.js'; /** * ## AiMode @@ -6,11 +7,17 @@ import type { AiProvider } from './types.js'; * - `Fallback` ('fallback'): Sequential provider rotation until one succeeds. * - `Race` ('race'): Concurrent speculative requests; returns fastest success. * - `Consensus` ('consensus'): Concurrent requests across all providers with confidence voting. + * - `Hedged` ('hedged'): Staggered latency hedging (near-Race speed with reduced token usage). + * - `RoundRobin` ('roundrobin'): Cyclic load balancing across provider pool. + * - `Adaptive` ('adaptive'): Proactive telemetry-aware rate-limit avoidance. */ export const AiMode = Object.freeze({ Fallback: 'fallback', Race: 'race', - Consensus: 'consensus' + Consensus: 'consensus', + Hedged: 'hedged', + RoundRobin: 'roundrobin', + Adaptive: 'adaptive', } as const); export type AiMode = (typeof AiMode)[keyof typeof AiMode]; @@ -18,30 +25,30 @@ export type AiMode = (typeof AiMode)[keyof typeof AiMode]; /** * Keywords reserved by parseAI to avoid provider configuration collisions. */ -export const RESERVED_PROVIDER_IDS: ReadonlySet = new Set(['native', 'cache']); +export const RESERVED_PROVIDER_IDS: ReadonlySet = new Set(['native', 'cache', 'consensus', 'fallback', 'builtin']); /** * Built-in default endpoint and model configurations for popular providers. */ -export const DEFAULT_PROVIDERS: Readonly>>> = Object.freeze({ - groq: Object.freeze({ +export const DEFAULT_PROVIDERS: Readonly>> = secure({ + groq: { url: 'https://api.groq.com/openai/v1/chat/completions', model: 'llama-3.3-70b-versatile', tokenParam: 'max_tokens' - }), - openai: Object.freeze({ + }, + openai: { url: 'https://api.openai.com/v1/chat/completions', model: 'gpt-5.4-mini', tokenParam: 'max_completion_tokens' - }), - gemini: Object.freeze({ + }, + gemini: { url: 'https://generativelanguage.googleapis.com/v1beta/openai/chat/completions', model: 'gemini-3.6-flash', tokenParam: 'max_tokens' - }), - mistral: Object.freeze({ + }, + mistral: { url: 'https://api.mistral.ai/v1/chat/completions', model: 'mistral-small-latest', tokenParam: 'max_tokens' - }) + } }); diff --git a/packages/plugins/ai/src/core/dispatch.ts b/packages/plugins/ai/src/core/dispatch.ts new file mode 100644 index 00000000..d6235dbe --- /dev/null +++ b/packages/plugins/ai/src/core/dispatch.ts @@ -0,0 +1,438 @@ +import { TempoAiError } from './error.js'; +import { AiMode } from './config.js'; +import { _state } from './init.js'; +import type { AiProvider } from '../types/index.js'; + +/** + * ## ModeCandidate + * Represents the normalized execution result returned by a single AI provider task. + * + * @template T - The domain-specific payload type (e.g. parsed date components, RRULE structure, schedule slot). + */ +export interface ModeCandidate { + /** The domain-specific result payload returned by the provider. */ + data: T; + /** The identifier of the AI provider that produced this candidate. */ + providerId: string; + /** Confidence score between 0.0 (unusable) and 1.0 (certain). */ + confidence?: number | undefined; + /** Rate limit metadata headers extracted from provider response. */ + rateLimits?: any; + /** + * Unique token or key used to determine consensus between different providers + * (e.g. normalized ISO string in parse, RRULE string in recurrence, timestamp pair in schedule). + */ + consensusKey?: string | undefined; + /** Whether the candidate was flagged as ambiguous or had conflicting alternatives. */ + ambiguous?: boolean | undefined; +} + +/** + * ## ProviderTask + * Async closure that executes a provider-specific AI request. + * + * @template T - The domain-specific payload type. + * @param provider - The target AI provider configuration to call. + * @param signal - Optional AbortSignal for aborting in-flight requests during race conditions. + * @returns Promise resolving to a normalized `ModeCandidate`. + */ +export type ProviderTask = (provider: AiProvider, signal?: AbortSignal | undefined) => Promise>; + +/** + * ## ExecuteModeOptions + * Optional configuration overrides passed to the mode execution orchestrator. + */ +export interface ExecuteModeOptions { + /** Minimum confidence threshold (0.0 to 1.0) required to accept a result without cascading. */ + minConfidence?: number | undefined; + /** Whether to emit debug telemetry and provider fallback warnings to stdout/stderr. */ + debug?: boolean | undefined; + /** Logging tag prefix for debugging output (e.g. 'tempo-plugin-ai:parse'). */ + tag?: string | undefined; + /** Optional delay in milliseconds before initiating speculative hedging in AiMode.Hedged (default: 800ms). */ + hedgeDelay?: number | undefined; +} + +/** + * Normalizes and extracts meaningful error details from multi-provider execution failures. + * Unwraps `AggregateError` instances (from `Promise.any`), preserving internal `TempoAiError` + * status codes and messages. + * + * @internal + * @param err - The raw error caught from the provider execution promise. + * @param fallbackPrefix - Contextual error prefix for generic error wrappers. + * @returns Normalized `Error` instance. + */ +function unwrapExecutionError(err: unknown, fallbackPrefix: string): Error { + if (err instanceof TempoAiError) return err; + if (err instanceof AggregateError) { + const firstTempoError = err.errors.find(e => e instanceof TempoAiError); + if (firstTempoError) return firstTempoError; + return new TempoAiError(`${fallbackPrefix}: ${err.message}`, 500); + } + const message = err instanceof Error ? err.message : String(err); + return new TempoAiError(`${fallbackPrefix}: ${message}`, 500); +} + +/** + * Executes providers sequentially in configured order. + * - Resolves immediately if a candidate meets or exceeds `minConfidence`. + * - If no candidate meets the threshold, falls back to the candidate with the highest confidence score. + * - Throws if all configured providers fail. + * + * @internal + */ +async function executeFallbackMode( + providers: AiProvider[], + task: ProviderTask, + options?: ExecuteModeOptions, +): Promise> { + let lastError: any = null; + let bestCandidate: ModeCandidate | null = null; + + for (const provider of providers) { + try { + const candidate = await task(provider); + const confidence = typeof candidate.confidence === 'number' ? candidate.confidence : 1.0; + const bestConfidence = bestCandidate ? (typeof bestCandidate.confidence === 'number' ? bestCandidate.confidence : 1.0) : -1; + + if (!bestCandidate || confidence > bestConfidence) + bestCandidate = candidate; + + if (options?.minConfidence === undefined || confidence >= options.minConfidence) + return candidate; + + if (options?.debug) + console.log(`[${options.tag || 'tempo-plugin-ai'}] Provider '${candidate.providerId}' confidence (${confidence}) below minConfidence (${options.minConfidence}). Cascading to next provider...`); + + } catch (err: any) { + lastError = err; + if (err instanceof TempoAiError && err.code === 422 && options?.minConfidence === undefined) break; + if (options?.debug) + console.warn(`[${options.tag || 'tempo-plugin-ai'}] Provider '${provider.id}' failed:`, err); + } + } + + if (bestCandidate) return bestCandidate; + throw lastError || new TempoAiError('All configured AI providers failed.', 500); +} + +/** + * Executes providers concurrently, resolving with the fastest successful response. + * Uses an `AbortController` to cancel in-flight requests once a winner emerges. + * + * @internal + */ +async function executeRaceMode( + providers: AiProvider[], + task: ProviderTask, + options?: ExecuteModeOptions, +): Promise> { + if (providers.length <= 1) return executeFallbackMode(providers, task, options); + + const controller = new AbortController(); + const minConfidence = options?.minConfidence; + const errors: any[] = []; + let bestCandidate: ModeCandidate | null = null; + let settled = false; + let completedCount = 0; + + return new Promise>((resolve, reject) => { + const handleOutcome = (candidate: ModeCandidate | null, error: any) => { + if (settled) return; + + completedCount++; + + if (candidate) { + const confidence = typeof candidate.confidence === 'number' ? candidate.confidence : 1.0; + const bestConf = bestCandidate ? (typeof bestCandidate.confidence === 'number' ? bestCandidate.confidence : 1.0) : -1; + if (!bestCandidate || confidence > bestConf) + bestCandidate = candidate; + + if (minConfidence === undefined || confidence >= minConfidence) { + settled = true; + controller.abort(); + resolve(candidate); + return; + } + } else { + errors.push(error); + } + + if (completedCount >= providers.length) { + settled = true; + controller.abort(); + if (bestCandidate) + resolve(bestCandidate); + else { + const unwrapped = unwrapExecutionError( + errors.length > 1 ? new AggregateError(errors, 'All raced providers failed') : errors[0], + 'Provider race failed' + ); + reject(unwrapped); + } + } + }; + + providers.forEach(p => { + task(p, controller.signal) + .then(candidate => handleOutcome(candidate, null)) + .catch(err => handleOutcome(null, err)); + }); + }); +} + +/** + * Dispatches concurrent requests across all providers and evaluates consensus: + * - If all responding providers yield the same `consensusKey`, confidence is elevated to 1.0 (unanimous). + * - If candidates disagree, selects the candidate with highest confidence and flags it as `ambiguous: true`. + * - Throws if all providers reject. + * + * @internal + */ +async function executeConsensusMode( + providers: AiProvider[], + task: ProviderTask, +): Promise> { + const controller = new AbortController(); + const promises = providers.map(p => task(p, controller.signal)); + const settled = await Promise.allSettled(promises); + + const fulfilled = settled + .filter((s): s is PromiseFulfilledResult> => s.status === 'fulfilled') + .map(s => s.value); + + if (fulfilled.length === 0) { + const firstRejected = settled.find(s => s.status === 'rejected') as PromiseRejectedResult | undefined; + throw unwrapExecutionError(firstRejected?.reason, 'Consensus failed: all providers rejected.'); + } + + if (fulfilled.length === 1) return fulfilled[0]; + + const firstKey = fulfilled[0].consensusKey; + const allMatch = firstKey !== undefined && fulfilled.every(f => f.consensusKey === firstKey); + + if (allMatch) { + return { + ...fulfilled[0], + confidence: 1.0, + ambiguous: false, + providerId: AiMode.Consensus, + }; + } + + const sorted = [...fulfilled].sort((a, b) => { + const confB = typeof b.confidence === 'number' ? b.confidence : 1.0; + const confA = typeof a.confidence === 'number' ? a.confidence : 1.0; + return confB - confA; + }); + return { + ...sorted[0], + ambiguous: true, + }; +} + +/** + * Executes providers with speculative staggered hedging: + * - Dispatches request to the primary provider immediately. + * - If primary provider does not respond within `hedgeDelay` (default: 800ms), dispatches speculative requests to subsequent providers. + * - Resolves with the first valid candidate meeting `minConfidence`. + * - Uses an `AbortController` to cancel in-flight and pending requests once a winner emerges. + * + * @internal + */ +async function executeHedgedMode( + providers: AiProvider[], + task: ProviderTask, + options?: ExecuteModeOptions, +): Promise> { + if (providers.length <= 1) return executeFallbackMode(providers, task, options); + + const controller = new AbortController(); + const delay = options?.hedgeDelay ?? 800; + const minConfidence = options?.minConfidence; + const errors: any[] = []; + let bestCandidate: ModeCandidate | null = null; + + return new Promise>((resolve, reject) => { + let settled = false; + let completedCount = 0; + const timeouts: any[] = []; + + const cleanup = () => { + timeouts.forEach(t => clearTimeout(t)); + }; + + const runProvider = async (index: number) => { + if (settled) return; + const provider = providers[index]; + try { + const candidate = await task(provider, controller.signal); + if (settled) return; + + const confidence = typeof candidate.confidence === 'number' ? candidate.confidence : 1.0; + const bestConf = bestCandidate ? (typeof bestCandidate.confidence === 'number' ? bestCandidate.confidence : 1.0) : -1; + if (!bestCandidate || confidence > bestConf) + bestCandidate = candidate; + + if (minConfidence === undefined || confidence >= minConfidence) { + settled = true; + cleanup(); + controller.abort(); + resolve(candidate); + return; + } + } catch (err: any) { + if (settled) return; + errors.push(err); + } finally { + completedCount++; + if (!settled && completedCount >= providers.length) { + settled = true; + cleanup(); + controller.abort(); + if (bestCandidate) { + resolve(bestCandidate); + } else { + const unwrapped = unwrapExecutionError( + errors.length > 1 ? new AggregateError(errors, 'All hedged providers failed') : errors[0], + 'Hedged dispatch failed' + ); + reject(unwrapped); + } + } + } + }; + + // Start first provider immediately + runProvider(0); + + // Stagger remaining providers + for (let i = 1; i < providers.length; i++) { + const t = setTimeout(() => { + if (!settled) runProvider(i); + }, delay * i); + timeouts.push(t); + } + }); +} + +let _rrIndex = 0; + +/** + * Dispatches requests rotating the starting provider across the provider pool in round-robin fashion. + * Cascades to remaining providers in cyclic order if the selected primary provider fails. + * + * @internal + */ +async function executeRoundRobinMode( + providers: AiProvider[], + task: ProviderTask, + options?: ExecuteModeOptions, +): Promise> { + if (providers.length <= 1) return executeFallbackMode(providers, task, options); + + const start = (_rrIndex++) % providers.length; + const rotated = [...providers.slice(start), ...providers.slice(0, start)]; + return executeFallbackMode(rotated, task, options); +} + +/** + * Executes providers adaptively based on real-time rate limit telemetry: + * - Deprioritizes or temporarily avoids providers whose request quota is exhausted (`remainingRequests === 0` with an active reset window). + * - Sorts available providers by remaining request quota descending. + * - Delegates to sequential fallback execution across the sorted provider list. + * + * @internal + */ +async function executeAdaptiveMode( + providers: AiProvider[], + task: ProviderTask, + options?: ExecuteModeOptions, +): Promise> { + if (providers.length <= 1) return executeFallbackMode(providers, task, options); + + const now = Date.now(); + const scored = providers.map((provider, index) => { + const limits = _state.providerLimits.get(provider.id); + + // Providers with no rate-limit telemetry (local LLMs, first-call remotes) have + // no quota ceiling — assign Infinity so they naturally sort above constrained providers. + const remaining = typeof limits?.remainingRequests === 'number' ? limits.remainingRequests : Infinity; + let isExhausted = false; + + if (limits) { + const resetMs = limits.resetAt?.epoch?.ms ?? now; + isExhausted = limits.remainingRequests === 0 && resetMs > now; + } + + return { + provider, + index, + isExhausted, + remaining, + } + }); + + scored.sort((a, b) => { + // Non-exhausted providers before exhausted ones + if (a.isExhausted !== b.isExhausted) return a.isExhausted ? 1 : -1; + // Higher remaining requests first + if (a.remaining !== b.remaining) return b.remaining - a.remaining; + // Preserve original order + return a.index - b.index; + }); + + const sortedProviders = scored.map(s => s.provider); + return executeFallbackMode(sortedProviders, task, options); +} + +/** + * ## executeWithMode + * Central multi-provider execution orchestrator for Tempo AI plugins. + * Routes task execution through the configured multi-provider strategy: + * - `Fallback`: Sequential cascade through providers until confidence threshold is met or highest confidence is found. + * - `Race`: Concurrent speculative requests with AbortSignal cancellation returning the fastest resolution. + * - `Consensus`: Dispatches concurrent calls across all providers and resolves matching/highest-confidence consensus. + * - `Hedged`: Staggered latency hedging dispatch with speculative cancellation. + * - `RoundRobin`: Cyclic provider rotation across the configured pool. + * - `Adaptive`: Real-time rate-limit telemetry-aware provider prioritization. + * + * @internal + * @template T - The domain-specific payload type. + * @param mode - Execution strategy (`AiMode.Fallback` | `AiMode.Race` | `AiMode.Consensus` | `AiMode.Hedged` | `AiMode.RoundRobin` | `AiMode.Adaptive`). + * @param providers - Array of available configured `AiProvider` definitions. + * @param task - Async task closure executed per provider. + * @param options - Execution options including `minConfidence`, `debug`, logging `tag`, and `hedgeDelay`. + * @returns Promise resolving to the winning `ModeCandidate`. + * @throws {TempoAiError} When the mode is invalid (400) or all providers fail (500/502). + */ +export async function executeWithMode( + mode: AiMode, + providers: AiProvider[], + task: ProviderTask, + options?: ExecuteModeOptions, +): Promise> { + switch (mode) { + case AiMode.Fallback: + return executeFallbackMode(providers, task, options); + + case AiMode.Race: + return executeRaceMode(providers, task, options); + + case AiMode.Consensus: + return executeConsensusMode(providers, task); + + case AiMode.Hedged: + return executeHedgedMode(providers, task, options); + + case AiMode.RoundRobin: + return executeRoundRobinMode(providers, task, options); + + case AiMode.Adaptive: + return executeAdaptiveMode(providers, task, options); + + default: + throw new TempoAiError(`Invalid execution mode: '${mode}'. Supported modes: ${Object.values(AiMode).map(m => `'${m}'`).join(', ')}.`, 400); + } +} diff --git a/packages/plugins/ai/src/core/error.ts b/packages/plugins/ai/src/core/error.ts index a9f6f32f..4dce5b55 100644 --- a/packages/plugins/ai/src/core/error.ts +++ b/packages/plugins/ai/src/core/error.ts @@ -22,6 +22,10 @@ export class TempoAiError extends Error { return this.#code; } + get status(): number { + return this.#code; + } + get retryAt(): Tempo | undefined { return this.#retryAt; } diff --git a/packages/plugins/ai/src/core/init.ts b/packages/plugins/ai/src/core/init.ts index 4af961d0..1ee7a9d1 100644 --- a/packages/plugins/ai/src/core/init.ts +++ b/packages/plugins/ai/src/core/init.ts @@ -1,41 +1,149 @@ import { Tempo } from '@magmacomputing/tempo'; -import { DEFAULT_PROVIDERS } from './config.js'; + +import { getResolvedProviderDefaults, loadRemoteManifest, resetManifestCache } from './manifest.js'; import { normalizeCacheInput, assertNoReservedProviderId } from './support.js'; -import type { AiConfig, AiRateLimits, AiProvider } from './types.js'; +import type { AiConfig, AiRateLimits, AiProvider } from '../types/index.js'; +/** + * Internal singleton state container for the AI plugin. + * @internal + */ export const _state: { config: AiConfig; + rawProviders?: AiProvider[] | undefined; limits: AiRateLimits | null; + providerLimits: Map; + revision: number; } = { config: {}, + rawProviders: undefined, limits: null, + providerLimits: new Map(), + revision: 0, } -export function initAI(config: AiConfig): void { +/** + * Initializes the Tempo AI plugin with the specified global configuration. + * Configures AI provider credentials, models, timeouts, caching options, + * and asynchronously resolves provider defaults against remote manifests. + * + * @param config - Global AI plugin configuration object + * @returns A Promise that resolves once initial configuration and background manifest synchronization is scheduled + * @example + * ```ts + * await initAI({ + * providers: [{ id: 'groq', key: 'gsk_...' }], + * mode: AiMode.Consensus + * }); + * ``` + */ +export function initAI(config: AiConfig): Promise { if (config.providers) assertNoReservedProviderId(config.providers); - const resolvedProviders = config.providers ? config.providers.map(p => { - const normalizedId = p.id?.toLowerCase() ?? ''; - const defaults = DEFAULT_PROVIDERS[normalizedId] || DEFAULT_PROVIDERS.openai; - return { - ...defaults, - ...p - } as AiProvider; - }) : _state.config.providers; + if (config.providers) + _state.rawProviders = config.providers; + + const currentRevision = ++_state.revision; + const remoteUrl = config.remoteConfigUrl ?? _state.config.remoteConfigUrl; + const callerProviders = config.providers ?? _state.rawProviders; + const resolveSyncProviders = (providers?: AiProvider[]) => { + if (!providers) return _state.config.providers; + return providers.map(p => { + const normalizedId = p.id?.toLowerCase() ?? ''; + const defaults = getResolvedProviderDefaults(normalizedId, remoteUrl, config.debug ?? _state.config.debug); + return { + ...defaults, + ...p + } as AiProvider; + }); + } + + // Synchronously update _state.config for immediate availability _state.config = { ..._state.config, ...config, - providers: resolvedProviders || [] + providers: resolveSyncProviders(callerProviders) || [] }; if (config.cache) { - Tempo.init({ cache: config.cache as any }); + Tempo.init({ cache: config.cache, silent: true }); } + + return (async () => { + if (remoteUrl !== false) { + try { + await loadRemoteManifest(remoteUrl, undefined, config.debug ?? _state.config.debug); + } catch { } + } + + if (_state.revision !== currentRevision) return; + + const fetchDefaults = config.fetchDefaults ?? _state.config.fetchDefaults; + const currentProviders = callerProviders; + + if (fetchDefaults && currentProviders) { + const asyncProviders = await Promise.all(currentProviders.map(async p => { + const normalizedId = p.id?.toLowerCase() ?? ''; + const defaults = getResolvedProviderDefaults(normalizedId, remoteUrl, config.debug ?? _state.config.debug); + let hookOptions: Partial | null = null; + try { + hookOptions = await fetchDefaults(normalizedId); + } catch { } + return { + ...defaults, + ...(hookOptions ?? {}), + ...p, + } as AiProvider; + })); + if (_state.revision === currentRevision) + _state.config.providers = asyncProviders; + } else if (currentProviders) { + if (_state.revision === currentRevision) + _state.config.providers = resolveSyncProviders(currentProviders); + } + })(); +} + +/** + * Resets the global AI state, cached rate limits, and remote manifest cache. + * Useful for test isolation and clean lifecycle teardown. + */ +export function resetAI(): void { + _state.config = {}; + _state.rawProviders = undefined; + _state.limits = null; + _state.providerLimits.clear(); + _state.revision++; + resetManifestCache(); } -export function clearAiCache(input: string | string[]): void { +/** + * Clears AI parsing results from the in-memory cache and any external storage adapters. + * If specific input strings or keys are provided, selectively purges only those entries. + * + * @param input - Optional string key, date string, or array of strings to purge from the cache + * @returns A Promise that resolves once cache eviction is completed + * @example + * ```ts + * await clearAiCache('next tuesday'); + * await clearAiCache(); // Clears all cached AI entries + * ``` + */ +export async function clearAiCache(input?: string | string[]): Promise { + const adapter = _state.config.cacheAdapter; + + if (!input) { + Tempo.cache.clear(); + if (adapter?.clear) { + try { + await Promise.resolve(adapter.clear()).catch(() => { }); + } catch { } + } + return; + } + const inputs = Array.isArray(input) ? input : [input]; for (const i of inputs) { const normalized = normalizeCacheInput(i); @@ -43,13 +151,67 @@ export function clearAiCache(input: string | string[]): void { Tempo.cache.delete(normalized); Tempo.cache.delete(i); Tempo.cache.deletePrefix(prefix); + + if (adapter) { + try { + if (adapter.delete) { + await Promise.resolve(adapter.delete(normalized)).catch(() => { }); + await Promise.resolve(adapter.delete(i)).catch(() => { }); + } + if (adapter.clear) { + await Promise.resolve(adapter.clear(prefix)).catch(() => { }); + } + } catch { } + } } } +/** + * Retrieves the latest observed rate limits across all provider responses. + * + * @returns The current rate limits snapshot containing remaining requests/tokens and reset timestamp, or `null` if none recorded + */ export function getAiRateLimits(): AiRateLimits | null { return _state.limits; } +/** + * Retrieves the latest observed rate limits for a specific AI provider. + * + * @param providerId - The provider ID to look up + * @returns The provider rate limits snapshot, or `null` if none recorded + */ +export function getAiProviderRateLimits(providerId: string): AiRateLimits | null { + return _state.providerLimits.get(providerId) ?? null; +} + +/** + * Returns a shallowly frozen, sanitized snapshot of the active AI configuration. + * Sensitive provider API keys are redacted for safety. + * + * @returns A frozen, read-only configuration object with frozen providers array and redacted API keys + */ +export function getAiConfig(): Readonly { + const sanitizedProviders: AiProvider[] = _state.config.providers?.map(p => { + const clone = { ...p }; + if (clone.key) + clone.key = '[REDACTED]'; + return clone; + }) ?? []; + + return Object.freeze({ + ..._state.config, + providers: Object.freeze(sanitizedProviders) as unknown as AiProvider[] + }); +} + +/** + * Parses HTTP rate-limit reset headers into a `Tempo` instance. + * Supports raw seconds, UNIX timestamps, compound durations (e.g. '1m30s'), and HTTP-date strings. + * + * @param resetHeader - The raw reset header string value (e.g. from `Retry-After` or `x-ratelimit-reset-*`) + * @returns A `Tempo` instance pointing to the reset time, or `null` if unparseable + */ export function parseResetHeaderToTempo(resetHeader: string): Tempo | null { const trimmed = resetHeader.trim(); if (!trimmed) return null; @@ -110,6 +272,13 @@ export function parseResetHeaderToTempo(resetHeader: string): Tempo | null { return null; } +/** + * Extracts and parses rate-limiting metadata from an HTTP response's headers. + * Inspects `x-ratelimit-remaining-requests`, `x-ratelimit-remaining-tokens`, and `retry-after`/`reset` headers. + * + * @param response - The Fetch `Response` object to inspect + * @returns An `AiRateLimits` structure, or `null` if no rate-limit headers are present + */ export function parseRateLimitsFromResponse(response: Response): AiRateLimits | null { const remReqHeader = response.headers.get('x-ratelimit-remaining-requests'); const remTokHeader = response.headers.get('x-ratelimit-remaining-tokens'); @@ -137,6 +306,12 @@ export function parseRateLimitsFromResponse(response: Response): AiRateLimits | } } +/** + * Inspects an HTTP response and extracts updated rate-limit statistics. + * + * @param response - The Fetch `Response` object to inspect + * @returns An `AiRateLimits` structure, or `null` if no rate-limit headers are present + */ export function updateRateLimitsFromResponse(response: Response): AiRateLimits | null { return parseRateLimitsFromResponse(response); } diff --git a/packages/plugins/ai/src/core/manifest.ts b/packages/plugins/ai/src/core/manifest.ts new file mode 100644 index 00000000..f47b8506 --- /dev/null +++ b/packages/plugins/ai/src/core/manifest.ts @@ -0,0 +1,147 @@ +import { DEFAULT_PROVIDERS } from './config.js'; +import type { AiProvider } from '../types/index.js'; + +export const DEFAULT_REMOTE_MANIFEST_URL = 'https://tempo.magmacomputing.com.au/providers.v1.json'; +export const DEFAULT_MANIFEST_TIMEOUT_MS = 1500; + +let _cachedManifestMap = new Map>>(); +let _fetchPromiseMap = new Map> | null>>(); + +/** + * Resets the in-memory manifest cache (used primarily for unit testing). + */ +export function resetManifestCache(): void { + _cachedManifestMap.clear(); + _fetchPromiseMap.clear(); +} + +/** + * Validates that a manifest or provider URL uses HTTPS or HTTP localhost. + */ +function isValidManifestUrl(urlStr: string): boolean { + try { + const parsed = new URL(urlStr); + return parsed.protocol === 'https:' || (parsed.protocol === 'http:' && (parsed.hostname === 'localhost' || parsed.hostname === '127.0.0.1')); + } catch { + return false; + } +} + +/** + * Fetches the remote AI provider manifest. Remote defaults are loaded during initialization. + * Fail-open: if network fails or times out, returns null and allows fallback to local DEFAULT_PROVIDERS. + */ +export async function loadRemoteManifest( + remoteConfigUrl: string | false = DEFAULT_REMOTE_MANIFEST_URL, + timeoutMs: number = DEFAULT_MANIFEST_TIMEOUT_MS, + debug: boolean = false +): Promise> | null> { + if (remoteConfigUrl === false) + return null; + + const targetUrl = typeof remoteConfigUrl === 'string' && remoteConfigUrl.trim().length > 0 + ? remoteConfigUrl.trim() + : DEFAULT_REMOTE_MANIFEST_URL; + + if (!isValidManifestUrl(targetUrl)) { + if (debug) + console.warn(`[tempo-plugin-ai] Rejected remote manifest URL '${targetUrl}' - must be HTTPS or localhost HTTP.`); + return null; + } + + if (_cachedManifestMap.has(targetUrl)) + return _cachedManifestMap.get(targetUrl)!; + + if (_fetchPromiseMap.has(targetUrl)) + return _fetchPromiseMap.get(targetUrl)!; + + const fetchPromise = (async () => { + let timer: ReturnType | undefined; + try { + const controller = new AbortController(); + timer = setTimeout(() => controller.abort(), timeoutMs); + + const response = await fetch(targetUrl, { + signal: controller.signal, + headers: { Accept: 'application/json' }, + redirect: 'error', + }); + + if (!response.ok) { + if (debug) + console.warn(`[tempo-plugin-ai] Remote manifest fetch failed with status ${response.status}`); + const empty = {}; + _cachedManifestMap.set(targetUrl, empty); + return null; + } + + const data = await response.json(); + if (data && typeof data === 'object' && data.providers && typeof data.providers === 'object') { + const manifest = data.providers as Record>; + _cachedManifestMap.set(targetUrl, manifest); + return manifest; + } + + if (debug) + console.warn('[tempo-plugin-ai] Remote manifest missing valid "providers" object structure'); + + const empty = {}; + _cachedManifestMap.set(targetUrl, empty); + return null; + } catch (err: any) { + if (debug) + console.warn(`[tempo-plugin-ai] Remote manifest fetch error: ${err?.message || err}`); + + // Fail-open: store empty object so we fallback to DEFAULT_PROVIDERS without hanging subsequent calls + const empty = {}; + _cachedManifestMap.set(targetUrl, empty); + return null; + } finally { + if (timer !== undefined) clearTimeout(timer); + _fetchPromiseMap.delete(targetUrl); + } + })(); + + _fetchPromiseMap.set(targetUrl, fetchPromise); + return fetchPromise; +} + +/** + * Resolves the default settings for a given provider ID by combining compiled DEFAULT_PROVIDERS + * with remote manifest entries if available. + */ +export function getResolvedProviderDefaults( + providerId: string, + remoteConfigUrl?: string | false, + debug: boolean = false +): Partial { + const normalizedId = providerId?.toLowerCase() ?? ''; + const localDefaults = DEFAULT_PROVIDERS[normalizedId] || {}; + + if (remoteConfigUrl === false) + return localDefaults; + + const targetUrl = typeof remoteConfigUrl === 'string' && remoteConfigUrl.trim().length > 0 + ? remoteConfigUrl.trim() + : DEFAULT_REMOTE_MANIFEST_URL; + + const cached = _cachedManifestMap.get(targetUrl); + if (!cached || !cached[normalizedId]) + return localDefaults; + + const manifestEntry = { ...cached[normalizedId] }; + + // Validate manifest-derived URL origin: must be HTTPS or localhost HTTP + if (manifestEntry.url) { + if (!isValidManifestUrl(manifestEntry.url)) { + if (debug) + console.warn(`[tempo-plugin-ai] Rejected manifest provider URL '${manifestEntry.url}' - invalid HTTPS origin.`); + delete manifestEntry.url; + } + } + + return { + ...localDefaults, + ...manifestEntry + }; +} diff --git a/packages/plugins/ai/src/core/patterns.ts b/packages/plugins/ai/src/core/patterns.ts new file mode 100644 index 00000000..b146b825 --- /dev/null +++ b/packages/plugins/ai/src/core/patterns.ts @@ -0,0 +1,29 @@ +/** + * ## AI Plugin — Shared Regex Patterns + * Single source of truth for all regular expressions used across AI function modules. + * Import from this module rather than defining inline patterns in function files. + */ + +/** Strips an LLM markdown JSON fence prefix (```json\n) from a raw response string. */ +export const RE_MARKDOWN_JSON_PREFIX = /^```json\s*/i; + +/** Strips an LLM markdown JSON fence suffix (\n```) from a raw response string. */ +export const RE_MARKDOWN_JSON_SUFFIX = /\s*```$/i; + +/** Matches a leading RFC 5545 "RRULE:" prefix to strip before further RRULE parsing. */ +export const RE_RRULE_PREFIX = /^RRULE:/i; + +/** Matches an ISO 8601 date string prefix (YYYY-MM-DD) to detect native-parseable inputs. */ +export const RE_ISO_DATE_PREFIX = /^\d{4}-\d{2}-\d{2}/; + +/** Matches a trailing UTC 'Z' suffix on an ISO string for timezone bracket replacement. */ +export const RE_ISO_Z_SUFFIX = /Z$/i; + +/** Extracts a minute-based duration from a natural language string (e.g. "30 mins"). */ +export const RE_DURATION_MINUTES = /(\d+)\s*(?:minutes?|mins?|m\b)/i; + +/** Extracts an hour-based duration from a natural language string (e.g. "1.5 hours"). */ +export const RE_DURATION_HOURS = /(\d+(?:\.\d+)?)\s*(?:hours?|hrs?|h\b)/i; + +/** Matches a single ISO weekday digit string (1–7). */ +export const RE_ISO_WEEKDAY_DIGIT = /^[1-7]$/; diff --git a/packages/plugins/ai/src/core/support.ts b/packages/plugins/ai/src/core/support.ts index 6a89c8c0..a43fa1f3 100644 --- a/packages/plugins/ai/src/core/support.ts +++ b/packages/plugins/ai/src/core/support.ts @@ -2,7 +2,7 @@ import { Tempo } from '@magmacomputing/tempo'; import { TempoAiError } from './error.js'; import { RESERVED_PROVIDER_IDS } from './config.js'; import { updateRateLimitsFromResponse, _state } from './init.js'; -import type { AiProvider, TempoAiMeta } from './types.js'; +import type { AiProvider, TempoAiMeta } from '../types/index.js'; export function assertNoReservedProviderId(providers: Partial[]): void { for (const p of providers) { @@ -22,6 +22,8 @@ export function getNamespacedCacheKey(namespace: string, key: string): string { export function attachAiMeta(instance: Tempo, meta: TempoAiMeta): Tempo { const frozenMeta = Object.freeze(meta); + const boundMethodCache = new Map(); + return new Proxy(instance, { get(target, prop, _receiver) { if (prop === 'ai') return frozenMeta; @@ -29,8 +31,18 @@ export function attachAiMeta(instance: Tempo, meta: TempoAiMeta): Tempo { if (meta.confidence === 0.0 || meta.rawIso === 'INVALID' || meta.ambiguous === true || !target.isValid) return false; } + if (prop === 'constructor') + return Reflect.get(target, prop, target); + + if (boundMethodCache.has(prop)) + return boundMethodCache.get(prop); + const val = Reflect.get(target, prop, target); - if (typeof val === 'function') return val.bind(target); + if (typeof val === 'function') { + const bound = val.bind(target); + boundMethodCache.set(prop, bound); + return bound; + } return val; }, has(target, prop) { @@ -61,12 +73,29 @@ export async function fetchFromProvider( str: string, contextString: string, isDebug: boolean, - parentSignal?: AbortSignal + parentSignal?: AbortSignal, + timeoutOverride?: number, + customSystemPrompt?: string ): Promise<{ rawContent: string; providerId: string; rateLimits: ReturnType }> { const url = provider.url!; const model = provider.model!; - const systemPrompt = `You are a high-performance date parser. Read the user's string and the provided context. Return ONLY a valid JSON object matching this exact schema: + if (!url || typeof url !== 'string') + throw new TempoAiError(`Provider ${provider.id} missing valid endpoint URL.`, 400); + + if (!model || typeof model !== 'string') + throw new TempoAiError(`Provider ${provider.id} missing valid model identifier.`, 400); + + try { + const parsed = new URL(url); + if (parsed.protocol !== 'https:' && !(parsed.protocol === 'http:' && (parsed.hostname === 'localhost' || parsed.hostname === '127.0.0.1'))) + throw new TempoAiError(`Provider ${provider.id} endpoint URL '${url}' must use secure HTTPS protocol.`, 400); + } catch (err: any) { + if (err instanceof TempoAiError) throw err; + throw new TempoAiError(`Provider ${provider.id} has invalid endpoint URL '${url}'.`, 400); + } + + const defaultSystemPrompt = `You are a high-performance date parser. Read the user's string and the provided context. Return ONLY a valid JSON object matching this exact schema: { "reasoning": "Step-by-step calendar math from Current Time.", "iso": "Local ISO 8601 string (YYYY-MM-DDThh:mm:ss) without offset or Z suffix, or 'INVALID' if ambiguous/unparseable.", @@ -84,68 +113,79 @@ Ambiguity Rules: Do not include markdown blocks or any text outside the JSON.`; - if (isDebug) - console.log(`[tempo-plugin-ai] Sending to ${provider.id}:`, { system: `${systemPrompt}\n${contextString}`, user: str }); - - const tokenParam = provider.tokenParam - || (provider.options?.max_completion_tokens !== undefined ? 'max_completion_tokens' : undefined) - || (provider.options?.max_tokens !== undefined ? 'max_tokens' : undefined) - || 'max_tokens'; - const tokenLimit = { [tokenParam]: 250 }; - - const controller = new AbortController(); - const timeoutMs = provider.options?.timeout ?? 15000; - const timeoutId = setTimeout(() => controller.abort(), timeoutMs); - - const onParentAbort = () => controller.abort(); - if (parentSignal) { - if (parentSignal.aborted) controller.abort(); - else parentSignal.addEventListener('abort', onParentAbort); - } - - try { - const response = await fetch(url, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${provider.key}` - }, - body: JSON.stringify({ - model: model, - messages: [ - { role: 'system', content: `${systemPrompt}\n${contextString}` }, - { role: 'user', content: str } - ], - temperature: 0, - ...tokenLimit, - response_format: { type: 'json_object' }, - ...provider.options - }), - signal: controller.signal - }); - - const limits = updateRateLimitsFromResponse(response); - - if (!response.ok) { - const errorText = await response.text(); - const resetTime = limits?.resetAt ?? undefined; - _state.limits = limits; - throw new TempoAiError(`Provider ${provider.id} failed with status ${response.status}. Details: ${errorText}`, response.status, resetTime); - } - - const data = await response.json(); - const rawContent = data?.choices?.[0]?.message?.content; - if (typeof rawContent !== 'string') - throw new TempoAiError(`Provider ${provider.id} returned invalid response payload.`, 422); - - if (isDebug) - console.log(`[tempo-plugin-ai] Received from ${provider.id}:`, rawContent); - - return { rawContent: rawContent.trim(), providerId: provider.id, rateLimits: limits }; - } finally { - clearTimeout(timeoutId); - if (parentSignal) { - parentSignal.removeEventListener('abort', onParentAbort); - } - } + const systemPrompt = customSystemPrompt ?? defaultSystemPrompt; + + if (isDebug) + console.log(`[tempo-plugin-ai] Querying provider '${provider.id}' (model: ${model})...`); + + const tokenParam = provider.tokenParam + || (provider.options?.max_completion_tokens !== undefined ? 'max_completion_tokens' : undefined) + || (provider.options?.max_tokens !== undefined ? 'max_tokens' : undefined) + || 'max_tokens'; + const tokenLimit = { [tokenParam]: 250 }; + + const { timeout: _unusedTimeout, ...bodyOptions } = provider.options ?? {}; + const controller = new AbortController(); + const timeoutMs = timeoutOverride ?? provider.timeout ?? provider.options?.timeout ?? _state.config.timeout ?? 15000; + const timeoutId = setTimeout(() => controller.abort(), timeoutMs); + + const onParentAbort = () => controller.abort(); + if (parentSignal) { + if (parentSignal.aborted) controller.abort(); + else parentSignal.addEventListener('abort', onParentAbort); + } + + const startTime = performance.now(); + + try { + const response = await fetch(url, { + method: 'POST', + redirect: 'error', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${provider.key}` + }, + body: JSON.stringify({ + model: model, + messages: [ + { role: 'system', content: `${systemPrompt}\n${contextString}` }, + { role: 'user', content: str } + ], + temperature: 0, + ...tokenLimit, + response_format: { type: 'json_object' }, + ...bodyOptions + }), + signal: controller.signal + }); + + const limits = updateRateLimitsFromResponse(response); + if (limits) + _state.providerLimits.set(provider.id, limits); + + if (!response.ok) { + const errorText = await response.text(); + const boundedError = errorText.length > 500 ? `${errorText.slice(0, 500)}... (truncated)` : errorText; + const resetTime = limits?.resetAt ?? undefined; + _state.limits = limits; + throw new TempoAiError(`Provider ${provider.id} failed with status ${response.status}. Details: ${boundedError}`, response.status, resetTime); + } + + const data = await response.json(); + const rawContent = data?.choices?.[0]?.message?.content; + if (typeof rawContent !== 'string') + throw new TempoAiError(`Provider ${provider.id} returned invalid response payload.`, 422); + + if (isDebug) { + const elapsed = Math.round(performance.now() - startTime); + console.log(`[tempo-plugin-ai] Received response from '${provider.id}' in ${elapsed}ms`); + } + + return { rawContent: rawContent.trim(), providerId: provider.id, rateLimits: limits }; + } finally { + clearTimeout(timeoutId); + if (parentSignal) { + parentSignal.removeEventListener('abort', onParentAbort); + } + } } diff --git a/packages/plugins/ai/src/functions/context.ts b/packages/plugins/ai/src/functions/context.ts index 4a48ae87..b9ce90b4 100644 --- a/packages/plugins/ai/src/functions/context.ts +++ b/packages/plugins/ai/src/functions/context.ts @@ -1,25 +1,231 @@ -export interface TempoContext { - timeZone: string; - locale: string; - calendar: string; - confidence?: number; +import { Tempo } from '@magmacomputing/tempo'; +import { TempoAiError } from '../core/error.js'; +import { AiMode } from '../core/config.js'; +import { _state } from '../core/init.js'; +import { executeWithMode } from '../core/dispatch.js'; +import { normalizeCacheInput, fetchFromProvider, assertNoReservedProviderId } from '../core/support.js'; +import { RE_MARKDOWN_JSON_PREFIX, RE_MARKDOWN_JSON_SUFFIX } from '../core/patterns.js'; +import type { TempoContext, AiContextOptions } from '../types/index.js'; + +async function contextSingleInput(text: string, options?: AiContextOptions): Promise { + const isDebug = options?.debug ?? _state.config.debug ?? false; + const normalizedStr = normalizeCacheInput(text); + + const { force, debug, mode: aiMode, providers, minConfidence, cache: aiCacheOption, timeout: callTimeout, ttl, cacheAdapter, hedgeDelay } = options || {}; + const effectiveMinConfidence = minConfidence ?? _state.config.minConfidence; + + const resolvedOptions = Tempo.options; + const tz = String(options?.timeZone || resolvedOptions.timeZone); + const cal = String(options?.calendar || resolvedOptions.calendar); + const loc = String(Array.isArray(options?.locale) ? options?.locale[0] : (options?.locale || resolvedOptions.locale)); + const sph = String(options?.sphere || resolvedOptions.sphere || 'north'); + + const cacheKey = `context::${normalizedStr}::${tz}::${loc}::${cal}::${sph}`; + const adapter = cacheAdapter ?? _state.config.cacheAdapter; + + let cachedVal: string | undefined; + if (!force && aiCacheOption !== false) { + if (adapter) { + try { + const val = await adapter.get(cacheKey); + if (val) { + cachedVal = val; + } + } catch (err: any) { + if (isDebug) console.log('[tempo-plugin-ai:context] Cache adapter read error:', err?.message); + } + } + + cachedVal ??= Tempo.cache.get(cacheKey); + } + + if (cachedVal) { + try { + const parsedCache = JSON.parse(cachedVal); + if (typeof parsedCache.timeZone === 'string') { + Intl.DateTimeFormat(undefined, { timeZone: parsedCache.timeZone }); + const cachedConfidence = typeof parsedCache.confidence === 'number' && Number.isFinite(parsedCache.confidence) + ? parsedCache.confidence + : 1.0; + if (effectiveMinConfidence === undefined || cachedConfidence >= effectiveMinConfidence) { + if (isDebug) console.log(`[tempo-plugin-ai:context] Cache hit: "${text}" -> ${cachedVal}`); + return { + timeZone: parsedCache.timeZone, + locale: parsedCache.locale, + calendar: parsedCache.calendar, + sphere: parsedCache.sphere === 'north' || parsedCache.sphere === 'south' ? parsedCache.sphere : undefined, + confidence: cachedConfidence, + provider: 'cache', + reasoning: parsedCache.reasoning, + } + } + } + } catch { + // If cached value is corrupted, invalid, or fails timezone validation, proceed to fetch + } + } + + const contextString = `Workstation baseline context - Timezone: ${tz}, Locale: ${loc}, Calendar: ${cal}, Hemisphere: ${sph}. Use these baseline settings as the default if the input text contains no geographic or regional clues.`; + + const availableProviders = providers || _state.config.providers; + if (!availableProviders || availableProviders.length === 0) + throw new TempoAiError('No AI providers configured. Please call initAI().', 400); + + assertNoReservedProviderId(availableProviders); + + const mode = aiMode || _state.config.mode || AiMode.Fallback; + const effectiveHedgeDelay = hedgeDelay ?? _state.config.hedgeDelay; + + const systemPrompt = `You are a locale and timezone inference engine. Analyze the provided text (like location names, user descriptions, meeting context) and infer the timezone (IANA format), locale (BCP 47 language/region tag), preferred calendar system (Unicode type, default 'gregory'), and hemisphere ('north' or 'south'). +Return ONLY a valid JSON object matching this exact schema: +{ + "timeZone": "IANA timezone name (e.g., 'America/New_York' or 'Asia/Tokyo')", + "locale": "BCP 47 locale tag (e.g., 'en-US' or 'ja-JP')", + "calendar": "Unicode calendar system type (default 'gregory')", + "sphere": "Hemisphere, must be either 'north' or 'south', or null if completely unknowable from the text", + "reasoning": "Step-by-step logic explaining the inference", + "confidence": 0.95 +} +Rules: +- "confidence": Float score between 0.0 (highly ambiguous) and 1.0 (certain). +- "sphere": Must be exactly 'north' or 'south', or null. Do not use other cardinal directions. +Do not include markdown blocks or text outside the JSON.`; + + const winningCandidate = await executeWithMode( + mode, + availableProviders, + async (provider, signal) => { + const { rawContent, providerId, rateLimits } = await fetchFromProvider( + provider, + text, + contextString, + isDebug, + signal, + callTimeout, + systemPrompt, + ); + const cleanContent = rawContent.replace(RE_MARKDOWN_JSON_PREFIX, '').replace(RE_MARKDOWN_JSON_SUFFIX, ''); + let parsedData: any; + try { + parsedData = JSON.parse(cleanContent); + } catch { + throw new TempoAiError(`Provider ${providerId} returned invalid JSON payload.`, 422); + } + + const timeZone = typeof parsedData?.timeZone === 'string' ? parsedData.timeZone.trim() : ''; + const locale = typeof parsedData?.locale === 'string' ? parsedData.locale.trim() : ''; + const calendar = typeof parsedData?.calendar === 'string' ? parsedData.calendar.trim() : 'gregory'; + const rawSphere = typeof parsedData?.sphere === 'string' ? parsedData.sphere.trim().toLowerCase() : null; + const sphere = rawSphere === 'north' || rawSphere === 'south' ? rawSphere : undefined; + const confidence = parsedData?.confidence; + + if (typeof confidence !== 'number' || !Number.isFinite(confidence) || confidence < 0 || confidence > 1) + throw new TempoAiError(`Provider ${providerId} returned invalid confidence score.`, 422); + + return { + data: { + timeZone, + locale, + calendar, + sphere, + reasoning: typeof parsedData?.reasoning === 'string' ? parsedData.reasoning : undefined, + }, + providerId, + rateLimits, + confidence, + consensusKey: `${timeZone}::${locale}::${calendar}::${sphere}`, + } + }, + { minConfidence: effectiveMinConfidence, debug: isDebug, tag: 'tempo-plugin-ai:context', hedgeDelay: effectiveHedgeDelay }, + ); + + _state.limits = winningCandidate.rateLimits ?? null; + + const { data: parsedData, providerId, rateLimits } = winningCandidate; + const confidence = typeof winningCandidate.confidence === 'number' ? winningCandidate.confidence : 1.0; + const reasoning = parsedData.reasoning; + + if (effectiveMinConfidence !== undefined && confidence < effectiveMinConfidence) + throw new TempoAiError(`Inferred context confidence (${confidence}) is below the required threshold of ${effectiveMinConfidence}.`, 422); + + // Validate IANA timezone + try { + Intl.DateTimeFormat(undefined, { timeZone: parsedData.timeZone }); + } catch { + throw new TempoAiError(`Inferred context returned an invalid or unsupported IANA timeZone: '${parsedData.timeZone}'`, 422); + } + + const finalResult: TempoContext = { + timeZone: parsedData.timeZone, + locale: parsedData.locale, + calendar: parsedData.calendar, + sphere: parsedData.sphere, + confidence, + provider: providerId, + reasoning, + }; + + if (aiCacheOption !== false) { + const providerTtl = providerId === AiMode.Consensus + ? availableProviders.reduce((min, p) => p.ttl === undefined ? min : (min === undefined ? p.ttl : Math.min(min, p.ttl)), undefined) + : availableProviders.find(p => p.id === providerId)?.ttl; + const resolvedTtl = ttl ?? providerTtl ?? _state.config.ttl ?? 86_400_000; // Default to 24 hours for context + + const cacheVal = JSON.stringify({ + timeZone: finalResult.timeZone, + locale: finalResult.locale, + calendar: finalResult.calendar, + sphere: finalResult.sphere, + confidence: finalResult.confidence, + reasoning: finalResult.reasoning, + }); + + if (adapter) { + try { + const res = adapter.set(cacheKey, cacheVal, resolvedTtl); + if (res instanceof Promise) await res; + } catch (err: any) { + if (isDebug) console.log('[tempo-plugin-ai:context] Cache adapter write error:', err?.message); + } + } + Tempo.cache.set(cacheKey, cacheVal); + } + + return finalResult; } /** - * @internal Draft implementation scaffolded for v0.3.0 roadmap. - * ## contextAI (Upcoming Export) - * Infers `timeZone`, `locale`, and preferred `calendar` system from ambiguous location strings, - * user bios, or context descriptions. - * - * ### Why it fits Tempo: - * Integrates directly into Tempo's core configuration parameters (`timeZone`, `locale`, `calendar`). - * - * ### Example Usage: + * ## contextAI + * Infers regional and calendar settings (`timeZone`, `locale`, `calendar`, `sphere`) from ambiguous text, + * user bios, or geographic descriptions using the configured AI provider farm. + * + * Supports single strings or batch arrays, hierarchical TTL/caching, execution modes (Fallback, Race, Consensus), + * and dynamic environment variable fallbacks. + * + * @param input - A geographic description, user bio, or context string (or an array of strings for batch resolution) + * @param options - Optional configuration including provider overrides, timeout, caching, and execution modes + * @returns Promise resolving to a TempoContext object (or an array of TempoContexts / TempoAiErrors for batch requests) + * @example * ```ts - * const context = await contextAI('We are meeting near Shibuya Station during Golden Week'); - * // returns: { timeZone: 'Asia/Tokyo', locale: 'ja-JP', calendar: 'gregory' } + * const context = await contextAI('We are meeting near Shibuya Crossing in Tokyo'); + * console.log(context.timeZone); // 'Asia/Tokyo' + * console.log(context.locale); // 'ja-JP' + * console.log(context.sphere); // 'north' * ``` */ -export async function contextAI(_text: string, _options?: Record): Promise { - throw new Error('contextAI is not yet implemented in tempo-plugin-ai.'); +export async function contextAI(input: string, options?: AiContextOptions): Promise; +export async function contextAI(input: string[], options?: AiContextOptions): Promise<(TempoContext | TempoAiError)[]>; +export async function contextAI( + input: string | string[], + options?: AiContextOptions, +): Promise { + if (Array.isArray(input)) { + if (options?.softErrors) { + const settled = await Promise.allSettled(input.map(str => contextSingleInput(str, options))); + return settled.map(s => s.status === 'fulfilled' ? s.value : (s.reason as TempoAiError)); + } + return Promise.all(input.map(str => contextSingleInput(str, options))); + } + + return contextSingleInput(input, options); } diff --git a/packages/plugins/ai/src/functions/diff.ts b/packages/plugins/ai/src/functions/diff.ts index ba7f309c..686bd33c 100644 --- a/packages/plugins/ai/src/functions/diff.ts +++ b/packages/plugins/ai/src/functions/diff.ts @@ -1,32 +1,315 @@ -import type { Tempo } from '@magmacomputing/tempo'; - -export interface TempoAiDiffResult { - formatted: string; - days?: number; - hours?: number; - businessDays?: number; - reasoning?: string; +import { Tempo } from '@magmacomputing/tempo'; +import { TempoAiError } from '../core/error.js'; +import { AiMode } from '../core/config.js'; +import { _state } from '../core/init.js'; +import { executeWithMode } from '../core/dispatch.js'; +import { normalizeCacheInput, fetchFromProvider, assertNoReservedProviderId } from '../core/support.js'; +import { RE_MARKDOWN_JSON_PREFIX, RE_MARKDOWN_JSON_SUFFIX } from '../core/patterns.js'; +import type { TempoAiDiffResult, AiDiffOptions, DiffPair } from '../types/index.js'; + +/** + * Calculates raw difference metrics and business days between two Tempo points. + */ +function calculateGroundingMetrics(startTempo: Tempo, endTempo: Tempo, holidays?: string[]) { + const calendarDays = Math.round(startTempo.until(endTempo, 'day') * 100) / 100; + const elapsedHours = Math.round(startTempo.until(endTempo, 'hour') * 100) / 100; + + const isReverse = startTempo.epoch.ms > endTempo.epoch.ms; + const from = isReverse ? endTempo : startTempo; + const to = isReverse ? startTempo : endTempo; + + const holidaySet = new Set(holidays ?? []); + const matchedHolidays: string[] = []; + + let curr = from.set({ start: 'day' }); + const limit = to.set({ start: 'day' }); + let businessDaysCount = 0; + + while (curr.epoch.ms < limit.epoch.ms) { + const dow = curr.dow; // 1 = Monday, 7 = Sunday + const dateStr = curr.format('{yyyy}-{mm}-{dd}'); + const isWeekend = dow === 6 || dow === 7; + const isHoliday = holidaySet.has(dateStr); + + if (isHoliday) matchedHolidays.push(dateStr); + + if (!isWeekend && !isHoliday) businessDaysCount++; + + curr = curr.add({ day: 1 }); + } + + const businessDays = isReverse ? -businessDaysCount : businessDaysCount; + + return { + calendarDays, + elapsedHours, + businessDays, + isReverse, + matchedHolidays, + }; +} + +async function diffSingleInput( + start: any, + end: any, + prompt?: string, + options?: AiDiffOptions, +): Promise { + const isDebug = options?.debug ?? _state.config.debug ?? false; + const resolvedOptions = Tempo.options; + const tz = String(options?.timeZone || (Tempo.isTempo(start) ? start.tz : undefined) || (Tempo.isTempo(end) ? end.tz : undefined) || resolvedOptions.timeZone || 'UTC'); + const loc = String(Array.isArray(options?.locale) ? options?.locale[0] : (options?.locale || resolvedOptions.locale || 'en-US')); + + const startTempo = Tempo.isTempo(start) ? (start.tz === tz ? start : start.set({ timeZone: tz })) : new Tempo(start, { timeZone: tz }); + const endTempo = Tempo.isTempo(end) ? (end.tz === tz ? end : end.set({ timeZone: tz })) : new Tempo(end, { timeZone: tz }); + + if (!startTempo.isValid) + throw new TempoAiError(`Invalid start date provided to diffAI: "${start}"`, 400); + if (!endTempo.isValid) + throw new TempoAiError(`Invalid end date provided to diffAI: "${end}"`, 400); + + const holidays = options?.holidays; + const region = String(options?.region || ''); + const grounding = calculateGroundingMetrics(startTempo, endTempo, holidays); + + const promptText = prompt?.trim() || 'Provide a natural summary of the temporal difference between these two dates.'; + const normalizedPrompt = normalizeCacheInput(promptText); + + const { force, mode: aiMode, providers, minConfidence, cache: aiCacheOption, timeout: callTimeout, ttl, cacheAdapter, hedgeDelay } = options || {}; + + const sortedHolidays = holidays ? [...holidays].sort().join(',') : ''; + const cacheKey = `diff::${startTempo.epoch.ms}::${endTempo.epoch.ms}::${normalizedPrompt}::${tz}::${loc}::${region}::${sortedHolidays}`; + const adapter = cacheAdapter ?? _state.config.cacheAdapter; + + const effectiveMinConfidence = minConfidence ?? _state.config.minConfidence; + const effectiveHedgeDelay = hedgeDelay ?? _state.config.hedgeDelay; + + let cachedVal: string | undefined; + if (!force && aiCacheOption !== false) { + if (adapter) { + try { + const val = await adapter.get(cacheKey); + if (val) cachedVal = val; + } catch (err: any) { + if (isDebug) console.log('[tempo-plugin-ai:diff] Cache adapter read error:', err?.message); + } + } + + cachedVal ??= Tempo.cache.get(cacheKey); + } + + if (cachedVal) { + try { + const parsedCache = JSON.parse(cachedVal); + if (typeof parsedCache?.formatted === 'string' && parsedCache.formatted.trim().length > 0) { + const cachedConfidence = typeof parsedCache.confidence === 'number' && Number.isFinite(parsedCache.confidence) + ? parsedCache.confidence + : 1.0; + if (effectiveMinConfidence === undefined || cachedConfidence >= effectiveMinConfidence) { + if (isDebug) console.log(`[tempo-plugin-ai:diff] Cache hit: "${cacheKey}" -> ${cachedVal}`); + return { + formatted: parsedCache.formatted, + days: parsedCache.days ?? grounding.calendarDays, + hours: parsedCache.hours ?? grounding.elapsedHours, + businessDays: parsedCache.businessDays ?? grounding.businessDays, + holidays: parsedCache.holidays ?? (grounding.matchedHolidays.length > 0 ? grounding.matchedHolidays : undefined), + confidence: cachedConfidence, + provider: 'cache', + reasoning: parsedCache.reasoning, + }; + } + } + } catch { + // If cached value is corrupted, proceed to fetch + } + } + + const availableProviders = providers || _state.config.providers; + if (!availableProviders || availableProviders.length === 0) + throw new TempoAiError('No AI providers configured. Please call initAI().', 400); + + assertNoReservedProviderId(availableProviders); + + const mode = aiMode || _state.config.mode || AiMode.Fallback; + + const contextString = `Grounding Context: +- Start Timestamp: ${startTempo.format('{yyyy}-{mm}-{dd}T{hh}:{mi}:{ss}')} (${startTempo.tz || tz}) +- End Timestamp: ${endTempo.format('{yyyy}-{mm}-{dd}T{hh}:{mi}:{ss}')} (${endTempo.tz || tz}) +- Direction: ${grounding.isReverse ? 'Past/Backward (End is earlier than Start)' : 'Future/Forward (Start is earlier than End)'} +- Computed Calendar Days: ${grounding.calendarDays} +- Computed Calendar Hours: ${grounding.elapsedHours} +- Computed Business Working Days: ${grounding.businessDays} (excluding weekends${holidays && holidays.length > 0 ? ' and specified holidays' : ''}) +${grounding.matchedHolidays.length > 0 ? `- Matching Excluded Holidays: ${grounding.matchedHolidays.join(', ')}` : ''} +${options?.region ? `- Region Context: ${options.region}` : ''} +- Target Locale: ${loc} +- User Instructions / Request: "${promptText}"`; + + const systemPrompt = `You are an expert temporal difference and natural language formatting engine. +Analyze the two timestamps and the pre-computed mathematical grounding metrics to express the difference according to the user's instructions. +Return ONLY a valid JSON object matching this exact schema: +{ + "formatted": "Concise, human-friendly natural summary of the difference according to the prompt instructions", + "days": ${grounding.calendarDays}, + "hours": ${grounding.elapsedHours}, + "businessDays": ${grounding.businessDays}, + "reasoning": "Step-by-step rationale for the formatting and tone", + "confidence": 0.95 +} +Rules: +- "formatted": Must directly fulfill the user's prompt (e.g. accounting terms, working days, human relative explanation, or business SLAs). +- "confidence": Float score between 0.0 (highly ambiguous) and 1.0 (certain). +- Do not contradict the pre-computed grounding metrics unless explicitly instructed otherwise. +Do not include markdown blocks or text outside the JSON.`; + + const winningCandidate = await executeWithMode( + mode, + availableProviders, + async (provider, signal) => { + const { rawContent, providerId, rateLimits } = await fetchFromProvider( + provider, + promptText, + contextString, + isDebug, + signal, + callTimeout, + systemPrompt, + ); + const cleanContent = rawContent.replace(RE_MARKDOWN_JSON_PREFIX, '').replace(RE_MARKDOWN_JSON_SUFFIX, ''); + let parsedData: any; + try { + parsedData = JSON.parse(cleanContent); + } catch { + throw new TempoAiError(`Provider ${providerId} returned invalid JSON payload.`, 422); + } + + const formatted = typeof parsedData?.formatted === 'string' ? parsedData.formatted.trim() : ''; + if (!formatted) + throw new TempoAiError(`Provider ${providerId} returned empty or missing 'formatted' text.`, 422); + + const days = grounding.calendarDays; + const hours = grounding.elapsedHours; + const businessDays = grounding.businessDays; + const confidence = typeof parsedData?.confidence === 'number' ? parsedData.confidence : 0.9; + const reasoning = typeof parsedData?.reasoning === 'string' ? parsedData.reasoning : undefined; + + return { + data: { + formatted, + days, + hours, + businessDays, + reasoning, + }, + providerId, + rateLimits, + confidence, + consensusKey: `${formatted}::${businessDays}`, + }; + }, + { minConfidence: effectiveMinConfidence, debug: isDebug, tag: 'tempo-plugin-ai:diff', hedgeDelay: effectiveHedgeDelay }, + ); + + _state.limits = winningCandidate.rateLimits ?? null; + + const { data: parsedData, providerId } = winningCandidate; + const confidence = typeof winningCandidate.confidence === 'number' ? winningCandidate.confidence : 0.9; + + if (effectiveMinConfidence !== undefined && confidence < effectiveMinConfidence) + throw new TempoAiError(`diffAI confidence (${confidence}) is below the required threshold of ${effectiveMinConfidence}.`, 422); + + const finalResult: TempoAiDiffResult = { + formatted: parsedData.formatted, + days: parsedData.days, + hours: parsedData.hours, + businessDays: parsedData.businessDays, + holidays: grounding.matchedHolidays.length > 0 ? grounding.matchedHolidays : undefined, + confidence, + provider: providerId, + reasoning: parsedData.reasoning, + }; + + if (aiCacheOption !== false) { + const providerTtl = providerId === AiMode.Consensus + ? availableProviders.reduce((min, p) => p.ttl === undefined ? min : (min === undefined ? p.ttl : Math.min(min, p.ttl)), undefined) + : availableProviders.find(p => p.id === providerId)?.ttl; + const resolvedTtl = ttl ?? providerTtl ?? _state.config.ttl ?? 86_400_000; + + const cacheVal = JSON.stringify({ + formatted: finalResult.formatted, + days: finalResult.days, + hours: finalResult.hours, + businessDays: finalResult.businessDays, + holidays: finalResult.holidays, + confidence: finalResult.confidence, + reasoning: finalResult.reasoning, + }); + + if (adapter) { + try { + const res = adapter.set(cacheKey, cacheVal, resolvedTtl); + if (res instanceof Promise) await res; + } catch (err: any) { + if (isDebug) console.log('[tempo-plugin-ai:diff] Cache adapter write error:', err?.message); + } + } + Tempo.cache.set(cacheKey, cacheVal); + } + + return finalResult; } /** - * @internal Draft implementation scaffolded for v0.3.0 roadmap. - * ## diffAI (Upcoming Export) - * Expresses the delta between two timestamps or `Tempo` instances in human, business, - * or operational terms. - * - * ### Why it fits Tempo: - * Fills the gap between raw numeric milliseconds/days calculations in `Tempo.diff()` - * and domain-specific human summaries (accounting, shipping SLAs, project planning). - * - * ### Example Usage: + * ## diffAI + * Calculates and expresses the temporal delta between two `Tempo` instances, dates, or timestamps + * in human, business, or domain-specific language. + * + * Utilizes pre-computed grounding metrics (calendar days, total hours, business days with weekend & holiday exclusion) + * to provide reliable arithmetic backing for the LLM formatting. + * + * @param start - Start date-time point (Tempo instance, Date, ISO string, or timestamp) + * @param end - End date-time point (Tempo instance, Date, ISO string, or timestamp) + * @param prompt - Formatting instruction or explanation goal (e.g. 'explain in working business days') + * @param options - Configuration options (holidays, timeZone, locale, execution mode, caching, minConfidence) + * @returns Promise resolving to TempoAiDiffResult + * + * @example * ```ts - * const start = new Tempo('2026-08-01T09:00:00'); - * const end = new Tempo('2026-08-10T17:00:00'); - * - * const diff = await diffAI(start, end, 'explain in terms of business working days excluding weekends'); - * // returns: { formatted: "6 business days (48 working hours)", businessDays: 6 } + * const start = new Tempo('2026-08-01T09:00:00Z'); + * const end = new Tempo('2026-08-10T17:00:00Z'); + * + * const result = await diffAI(start, end, 'explain in terms of business working days'); + * console.log(result.formatted); // '5 business days (or ~224 calendar hours)' + * console.log(result.businessDays); // 5 * ``` */ -export async function diffAI(_start: any, _end: any, _prompt?: string, _options?: Record): Promise { - throw new Error('diffAI is not yet implemented in tempo-plugin-ai.'); +export async function diffAI(pairs: DiffPair[], options?: AiDiffOptions): Promise<(TempoAiDiffResult | TempoAiError)[]>; +export async function diffAI(start: any, end: any, prompt?: string, options?: AiDiffOptions): Promise; +export async function diffAI( + startOrPairs: any, + endOrOptions?: any, + promptOrOptions?: string | AiDiffOptions, + options?: AiDiffOptions, +): Promise { + if (Array.isArray(startOrPairs)) { + const resolvedOptions = (endOrOptions as AiDiffOptions) || {}; + if (resolvedOptions.softErrors) { + const settled = await Promise.allSettled( + startOrPairs.map(async pair => diffSingleInput(pair?.start, pair?.end, pair?.prompt, resolvedOptions)) + ); + return settled.map(s => { + if (s.status === 'fulfilled') return s.value; + return s.reason instanceof TempoAiError + ? s.reason + : new TempoAiError(s.reason?.message || String(s.reason), 500); + }); + } + return Promise.all( + startOrPairs.map(async pair => diffSingleInput(pair?.start, pair?.end, pair?.prompt, resolvedOptions)) + ); + } + + const promptStr = typeof promptOrOptions === 'string' ? promptOrOptions : undefined; + const resolvedOptions = (typeof promptOrOptions === 'object' ? promptOrOptions : options) || {}; + + return diffSingleInput(startOrPairs, endOrOptions, promptStr, resolvedOptions); } diff --git a/packages/plugins/ai/src/functions/extract.ts b/packages/plugins/ai/src/functions/extract.ts index 175ad093..64579084 100644 --- a/packages/plugins/ai/src/functions/extract.ts +++ b/packages/plugins/ai/src/functions/extract.ts @@ -1,17 +1,85 @@ import type { Tempo } from '@magmacomputing/tempo'; +import type { TempoAiError } from '../core/error.js'; +import type { AiMode } from '../core/config.js'; +import type { AiCacheAdapter, AiProvider } from '../types/common.type.js'; -export interface TempoEvent { - label: string; - start: Tempo; - end?: Tempo | undefined; - type?: 'event' | 'deadline' | 'milestone' | 'reminder' | string; +export type TempoEventType = 'point' | 'interval' | 'deadline' | 'recurrence' | 'tentative'; + +export interface TempoExtractedEvent { + /** Short descriptive label or title of the extracted event/activity. */ + label: string; + /** Start date-time point as an instantiated Tempo instance. */ + start: Tempo; + /** Optional end date-time point (if an interval or duration was mentioned). */ + end?: Tempo | undefined; + /** Classification category of the temporal mention. */ + type: TempoEventType; + /** Raw text snippet extracted from the source document. */ + rawText?: string | undefined; + /** Confidence score for this specific entity extraction (0.0 to 1.0). */ + confidence: number; +} + +/** + * Backward compatibility alias for TempoExtractedEvent. + */ +export type TempoEvent = TempoExtractedEvent; + +export interface TempoAiExtractResult { + /** Array of extracted events with instantiated Tempo objects. */ + events: TempoExtractedEvent[]; + /** Overall confidence score. */ + confidence: number; + /** Provider ID that fulfilled the request (or 'cache'). */ + provider: string; + /** Optional summary or reasoning from the LLM. */ + reasoning?: string | undefined; +} + +export interface AiExtractOptions { + /** Reference anchor date-time for relative expressions (defaults to now). */ + anchor?: Tempo | Date | string | number | undefined; + /** Reference IANA timezone (defaults to global options or 'UTC'). */ + timeZone?: string | undefined; + /** Reference BCP 47 locale (defaults to global options or 'en-US'). */ + locale?: string | string[] | undefined; + /** Preferred calendar system (e.g. 'gregory', 'islamic', 'hebrew'). */ + calendar?: string | undefined; + /** Optional category filter to restrict extracted entities (e.g. ['meeting', 'deadline']). */ + categories?: string[] | undefined; + /** Optional regional context (e.g. 'US-NY', 'GB'). */ + region?: string | undefined; + /** If true, bypasses cache to force a fresh LLM fetch */ + force?: boolean | undefined; + /** If false, disables reading and writing to cache */ + cache?: boolean | undefined; + /** Optional custom cache adapter engine (e.g. Redis, KV store) for this request */ + cacheAdapter?: AiCacheAdapter | undefined; + /** Optional TTL override in milliseconds for cached result */ + ttl?: number | undefined; + /** If true, logs prompt context and LLM payloads to console */ + debug?: boolean | undefined; + /** Execution mode across provider farm (`AiMode.Fallback` | `AiMode.Race` | `AiMode.Consensus` | `AiMode.Hedged` | `AiMode.RoundRobin` | `AiMode.Adaptive` or string literal) */ + mode?: AiMode | undefined; + /** Per-request provider configuration overrides */ + providers?: AiProvider[] | undefined; + /** Strict minimum confidence threshold (0.0 to 1.0). Throws TempoAiError(422) if score is lower */ + minConfidence?: number | undefined; + /** If true, returns TempoAiError into array index position instead of rejecting batch */ + softErrors?: boolean | undefined; + /** Optional request timeout in milliseconds (overrides provider and global timeout) */ + timeout?: number | undefined; + /** Optional delay in milliseconds before initiating speculative hedging in AiMode.Hedged (default: 800ms) */ + hedgeDelay?: number | undefined; + /** Allow extra custom properties */ + [key: string]: any; } /** - * @internal Draft implementation scaffolded for v0.3.0 roadmap. + * @internal Draft implementation scaffolded for future releases. * ## extractAI (Upcoming Export) * Scans unstructured text (emails, transcripts, task notes) and extracts all - * embedded temporal entities, deadlines, and events into structured `TempoEvent` records. + * embedded temporal entities, deadlines, and events into structured `TempoAiExtractResult` records. * * ### Why it fits Tempo: * Essential for calendar apps and document processing workflows where temporal references @@ -20,10 +88,15 @@ export interface TempoEvent { * ### Example Usage: * ```ts * const text = "Let's meet tomorrow at 10am. Final deliverables due next Friday EOD."; - * const events = await extractAI(text, { anchor: new Tempo() }); - * // returns array of TempoEvent records with parsed Tempo instances + * const result = await extractAI(text, { anchor: new Tempo() }); + * // returns TempoAiExtractResult with parsed Tempo instances in result.events * ``` */ -export async function extractAI(_text: string, _options?: Record): Promise { - throw new Error('extractAI is not yet implemented in tempo-plugin-ai.'); +export async function extractAI(texts: string[], options?: AiExtractOptions): Promise<(TempoAiExtractResult | TempoAiError)[]>; +export async function extractAI(text: string, options?: AiExtractOptions): Promise; +export async function extractAI( + textOrTexts: string | string[], + _options?: AiExtractOptions, +): Promise { + throw new Error('extractAI is not yet implemented in tempo-plugin-ai.'); } diff --git a/packages/plugins/ai/src/functions/format.ts b/packages/plugins/ai/src/functions/format.ts index a3eaea45..1b16a57a 100644 --- a/packages/plugins/ai/src/functions/format.ts +++ b/packages/plugins/ai/src/functions/format.ts @@ -1,7 +1,65 @@ import type { Tempo } from '@magmacomputing/tempo'; +import type { TempoAiError } from '../core/error.js'; +import type { AiMode } from '../core/config.js'; +import type { AiCacheAdapter, AiProvider } from '../types/common.type.js'; + +export interface FormatItem { + /** Date-time instance or string to format. */ + date: Tempo | Date | string | number; + /** Prompt instructions guiding the output narrative. */ + prompt?: string | undefined; +} + +export interface TempoAiFormatResult { + /** Formatted narrative string. */ + formatted: string; + /** Confidence score between 0.0 and 1.0. */ + confidence: number; + /** ID of the provider that fulfilled the request (or 'cache'). */ + provider: string; + /** Optional step-by-step rationale from the LLM. */ + reasoning?: string | undefined; +} + +export interface AiFormatOptions { + /** Reference anchor date for relative calculations (defaults to now). */ + anchor?: Tempo | Date | string | number | undefined; + /** Target IANA timezone (defaults to Tempo instance timezone or global options). */ + timeZone?: string | undefined; + /** Target BCP 47 locale or language tag (defaults to global options or 'en-US'). */ + locale?: string | string[] | undefined; + /** Desired narrative tone or formatting style hint (e.g. 'casual', 'formal', 'compact', 'countdown'). */ + style?: string | undefined; + /** Custom regional context (e.g. 'AU-NSW', 'US-CA'). */ + region?: string | undefined; + /** If true, bypasses cache to force a fresh LLM fetch */ + force?: boolean | undefined; + /** If false, disables reading and writing to cache */ + cache?: boolean | undefined; + /** Optional custom cache adapter engine (e.g. Redis, KV store) for this request */ + cacheAdapter?: AiCacheAdapter | undefined; + /** Optional TTL override in milliseconds for cached result */ + ttl?: number | undefined; + /** If true, logs prompt context and LLM payloads to console */ + debug?: boolean | undefined; + /** Execution mode across provider farm (`AiMode.Fallback` | `AiMode.Race` | `AiMode.Consensus` | `AiMode.Hedged` | `AiMode.RoundRobin` | `AiMode.Adaptive` or string literal) */ + mode?: AiMode | undefined; + /** Per-request provider configuration overrides */ + providers?: AiProvider[] | undefined; + /** Strict minimum confidence threshold (0.0 to 1.0). Throws TempoAiError(422) if score is lower */ + minConfidence?: number | undefined; + /** If true, returns TempoAiError into array index position instead of rejecting batch */ + softErrors?: boolean | undefined; + /** Optional request timeout in milliseconds (overrides provider and global timeout) */ + timeout?: number | undefined; + /** Optional delay in milliseconds before initiating speculative hedging in AiMode.Hedged (default: 800ms) */ + hedgeDelay?: number | undefined; + /** Allow extra custom properties */ + [key: string]: any; +} /** - * @internal Draft implementation scaffolded for v0.3.0 roadmap. + * @internal Draft implementation scaffolded for future releases. * ## formatAI (Upcoming Export) * Formats a `Tempo` instance into human-friendly, contextual narrative text * tailored to specific UI tones, relative time frames, or business domains. @@ -15,9 +73,16 @@ import type { Tempo } from '@magmacomputing/tempo'; * const t = new Tempo('2026-08-07T17:00:00[America/New_York]'); * * // "this Friday at 5:00 PM EST (in 5 days)" - * const friendly = await formatAI(t, 'friendly reminder tone with relative countdown'); + * const result = await formatAI(t, 'friendly reminder tone with relative countdown'); + * console.log(result.formatted); * ``` */ -export async function formatAI(_tempo: Tempo, _prompt: string, _options?: Record): Promise { - throw new Error('formatAI is not yet implemented in tempo-plugin-ai.'); +export async function formatAI(items: FormatItem[], options?: AiFormatOptions): Promise<(TempoAiFormatResult | TempoAiError)[]>; +export async function formatAI(date: any, prompt?: string, options?: AiFormatOptions): Promise; +export async function formatAI( + dateOrItems: any, + _promptOrOptions?: string | AiFormatOptions, + _options?: AiFormatOptions, +): Promise { + throw new Error('formatAI is not yet implemented in tempo-plugin-ai.'); } diff --git a/packages/plugins/ai/src/functions/parse.ts b/packages/plugins/ai/src/functions/parse.ts index 309c0b99..b9f7ed33 100644 --- a/packages/plugins/ai/src/functions/parse.ts +++ b/packages/plugins/ai/src/functions/parse.ts @@ -1,257 +1,230 @@ import { Tempo } from '@magmacomputing/tempo'; import { TempoAiError } from '../core/error.js'; import { AiMode } from '../core/config.js'; -import type { AiParseOptions } from '../core/types.js'; import { _state } from '../core/init.js'; +import { executeWithMode } from '../core/dispatch.js'; import { normalizeCacheInput, attachAiMeta, fetchFromProvider, assertNoReservedProviderId } from '../core/support.js'; +import { RE_MARKDOWN_JSON_PREFIX, RE_MARKDOWN_JSON_SUFFIX, RE_ISO_DATE_PREFIX, RE_ISO_Z_SUFFIX } from '../core/patterns.js'; +import type { AiParseOptions } from '../types/index.js'; async function parseSingleInput(str: string, options?: AiParseOptions): Promise { - const isDebug = options?.debug ?? _state.config.debug ?? false; - const normalizedStr = normalizeCacheInput(str); - - const { force, debug, mode: aiMode, providers, minConfidence, softErrors, cache: aiCacheOption, ...coreOptions } = options || {}; - - let tz: string, cal: string, loc: string, sph: string, anchorStr: string; - if (Tempo.isTempo(options?.anchor)) { - tz = String(options!.timeZone || options!.anchor.config.timeZone); - cal = String(options!.calendar || options!.anchor.config.calendar); - loc = String(Array.isArray(options!.locale) ? options!.locale[0] : (options!.locale || options!.anchor.config.locale)); - sph = String(options!.sphere || options!.anchor.config.sphere || 'north'); - anchorStr = options!.anchor.toString(); - } else { - const resolvedOptions = Tempo.options; - tz = String(options?.timeZone || resolvedOptions.timeZone); - cal = String(options?.calendar || resolvedOptions.calendar); - loc = String(Array.isArray(options?.locale) ? options?.locale[0] : (options?.locale || resolvedOptions.locale)); - sph = String(options?.sphere || resolvedOptions.sphere || 'north'); - anchorStr = String(options?.anchor || new Tempo().toString()); - } - - const anchorTempo = new Tempo(anchorStr, { ...coreOptions, timeZone: tz, calendar: cal, locale: loc, sphere: sph as any }); - const cacheSalt = anchorTempo.format('{yyyy}-{mm}-{dd}'); - const cacheKey = `${normalizedStr}::${cacheSalt}::${tz}::${cal}::${loc}::${sph}`; - - let cachedIso: string | undefined; - if (!force && aiCacheOption !== false) { - if (Tempo.cache.has(cacheKey)) { - cachedIso = Tempo.cache.get(cacheKey); - } else if (Tempo.cache.has(normalizedStr)) { - cachedIso = Tempo.cache.get(normalizedStr); - } else if (Tempo.cache.has(str)) { - cachedIso = Tempo.cache.get(str); - } - } - - if (cachedIso) { - if (isDebug) console.log(`[tempo-plugin-ai] Cache hit for "${str}":`, cachedIso); - const cachedInstance = new Tempo(cachedIso, coreOptions); - return attachAiMeta(cachedInstance, { - provider: 'cache', - cached: true, - confidence: 1.0, - ambiguous: false, - granularity: 'day', - rawIso: cachedIso, - rawPrompt: isDebug ? str : undefined, - normalizedPrompt: isDebug ? normalizedStr : undefined - }); - } - - if (!force) { - try { - const native = new Tempo(str, { ...coreOptions, silent: true }); - const hasNativeMatches = Tempo.cache.has(str) - || Tempo.cache.has(normalizedStr) - || /^\d{4}-\d{2}-\d{2}/.test(str.trim()) - || native.isValid; - - if (native.isValid && hasNativeMatches) { - if (isDebug) console.log(`[parseAI] Resolved natively: "${str}"`); - return attachAiMeta(native, { - provider: 'native', - cached: false, - confidence: 1.0, - ambiguous: false, - granularity: 'day', - rawPrompt: isDebug ? str : undefined, - normalizedPrompt: isDebug ? normalizedStr : undefined - }); - } - } catch { - // Fallback - } - } - - const contextString = `Current Time: ${anchorTempo.format('{wkd}, {yyyy}-{mm}-{dd} {hh}:{mi}:{ss}')}, Timezone: ${tz}, Calendar: ${cal}, Locale: ${loc}, Hemisphere: ${sph}.`; - - const availableProviders = providers || _state.config.providers; - if (!availableProviders || availableProviders.length === 0) - throw new TempoAiError('No AI providers configured. Please call initAI().', 400); - - assertNoReservedProviderId(availableProviders); - - const mode = aiMode || AiMode.Fallback; - let successfulResult: { parsedData: any; providerId: string; rateLimits?: any } | null = null; - - if (mode === AiMode.Fallback) { - let lastError: any = null; - let bestCandidate: { parsedData: any; providerId: string; rateLimits?: any } | null = null; - - for (const provider of availableProviders) { - try { - const { rawContent, providerId, rateLimits } = await fetchFromProvider(provider, str, contextString, isDebug); - const cleanContent = rawContent.replace(/^```json\s*/i, '').replace(/\s*```$/i, ''); - const parsedData = JSON.parse(cleanContent); - - const candidateConfidence = typeof parsedData?.confidence === 'number' ? parsedData.confidence : (parsedData?.iso === 'INVALID' ? 0.0 : 1.0); - - if (!bestCandidate || candidateConfidence > (bestCandidate.parsedData?.confidence ?? 0)) { - bestCandidate = { parsedData, providerId, rateLimits }; - } - - if (minConfidence === undefined || candidateConfidence >= minConfidence) { - successfulResult = { parsedData, providerId, rateLimits }; - break; - } - - if (isDebug) { - console.log(`[parseAI] Provider ${providerId} confidence (${candidateConfidence}) below minConfidence (${minConfidence}). Cascading to next provider...`); - } - } catch (err: any) { - lastError = err; - if (err instanceof TempoAiError && err.code === 422 && minConfidence === undefined) break; - } - } - - if (!successfulResult) { - if (bestCandidate) { - successfulResult = bestCandidate; - } else { - throw lastError || new TempoAiError('All configured AI providers failed.', 500); - } - } - - } else if (mode === AiMode.Race) { - const parentController = new AbortController(); - try { - const promises = availableProviders.map(async (provider) => { - const { rawContent, providerId, rateLimits } = await fetchFromProvider(provider, str, contextString, isDebug, parentController.signal); - const cleanContent = rawContent.replace(/^```json\s*/i, '').replace(/\s*```$/i, ''); - return { parsedData: JSON.parse(cleanContent), providerId, rateLimits }; - }); - successfulResult = await Promise.race(promises); - parentController.abort(); - } catch (err: any) { - parentController.abort(); - throw err instanceof TempoAiError ? err : new TempoAiError(`Provider race failed: ${err.message}`, 500); - } - - } else if (mode === AiMode.Consensus) { - const promises = availableProviders.map(async (provider) => { - const { rawContent, providerId, rateLimits } = await fetchFromProvider(provider, str, contextString, isDebug); - const cleanContent = rawContent.replace(/^```json\s*/i, '').replace(/\s*```$/i, ''); - return { parsedData: JSON.parse(cleanContent), providerId, rateLimits }; - }); - - const settled = await Promise.allSettled(promises); - const fulfilled = settled - .filter((s): s is PromiseFulfilledResult<{ parsedData: any; providerId: string; rateLimits: any }> => s.status === 'fulfilled') - .map(s => s.value); - - if (fulfilled.length === 0) { - const firstRejected = settled.find(s => s.status === 'rejected') as PromiseRejectedResult | undefined; - throw firstRejected?.reason || new TempoAiError('Consensus failed: all providers rejected.', 500); - } - - if (fulfilled.length === 1) { - successfulResult = fulfilled[0]; - } else { - const firstIso = fulfilled[0].parsedData?.iso; - const allMatch = fulfilled.every(f => f.parsedData?.iso === firstIso); - - if (allMatch) { - successfulResult = { - parsedData: { - ...fulfilled[0].parsedData, - confidence: 1.0, - ambiguous: false - }, - providerId: AiMode.Consensus, - rateLimits: fulfilled[0].rateLimits - }; - } else { - const sorted = [...fulfilled].sort((a, b) => (b.parsedData?.confidence ?? 0) - (a.parsedData?.confidence ?? 0)); - successfulResult = { - parsedData: { - ...sorted[0].parsedData, - ambiguous: true - }, - providerId: sorted[0].providerId, - rateLimits: sorted[0].rateLimits - }; - } - } - } - - _state.limits = successfulResult?.rateLimits ?? null; - - const { parsedData, providerId } = successfulResult!; - const rawIso = typeof parsedData?.iso === 'string' ? parsedData.iso : 'INVALID'; - const confidence = typeof parsedData?.confidence === 'number' ? parsedData.confidence : (rawIso === 'INVALID' ? 0.0 : 1.0); - const ambiguous = Boolean(parsedData?.ambiguous || rawIso === 'INVALID'); - const granularity = typeof parsedData?.granularity === 'string' ? parsedData.granularity : 'unknown'; - const reasoning = typeof parsedData?.reasoning === 'string' ? parsedData.reasoning : undefined; - - const isBelowMinConfidence = minConfidence !== undefined && confidence < minConfidence; - - if (rawIso === 'INVALID' || isBelowMinConfidence) { - const invalidInstance = new Tempo('INVALID', { ...coreOptions, catch: true }); - return attachAiMeta(invalidInstance, { - provider: providerId, - cached: false, - confidence, - ambiguous: true, - granularity, - rawIso: rawIso === 'INVALID' ? 'INVALID' : rawIso, - reasoning: isDebug ? reasoning : undefined, - rawPrompt: isDebug ? str : undefined, - normalizedPrompt: isDebug ? normalizedStr : undefined - }); - } - - const parsedIso = `${rawIso.replace(/Z$/i, '')}[${tz}]`; - - if (aiCacheOption !== false) { - Tempo.cache.set(cacheKey, parsedIso); - } - - const finalInstance = new Tempo(parsedIso, coreOptions); - return attachAiMeta(finalInstance, { - provider: providerId, - cached: false, - confidence, - ambiguous, - granularity, - rawIso, - reasoning: isDebug ? reasoning : undefined, - rawPrompt: isDebug ? str : undefined, - normalizedPrompt: isDebug ? normalizedStr : undefined - }); + const isDebug = options?.debug ?? _state.config.debug ?? false; + const normalizedStr = normalizeCacheInput(str); + + const { force, debug, mode: aiMode, providers, minConfidence, softErrors, cache: aiCacheOption, timeout: callTimeout, ttl, cacheAdapter, anchor, hedgeDelay, ...coreOptions } = options || {}; + + let tz: string, cal: string, loc: string, sph: string, anchorStr: string; + if (Tempo.isTempo(options?.anchor)) { + tz = String(options!.timeZone || options!.anchor.config.timeZone); + cal = String(options!.calendar || options!.anchor.config.calendar); + loc = String(Array.isArray(options!.locale) ? options!.locale[0] : (options!.locale || options!.anchor.config.locale)); + sph = String(options!.sphere || options!.anchor.config.sphere || 'north'); + anchorStr = options!.anchor.toString(); + } else { + const resolvedOptions = Tempo.options; + tz = String(options?.timeZone || resolvedOptions.timeZone); + cal = String(options?.calendar || resolvedOptions.calendar); + loc = String(Array.isArray(options?.locale) ? options?.locale[0] : (options?.locale || resolvedOptions.locale)); + sph = String(options?.sphere || resolvedOptions.sphere || 'north'); + anchorStr = String(options?.anchor || new Tempo().toString()); + } + + const anchorTempo = new Tempo(anchorStr, { ...coreOptions, timeZone: tz, calendar: cal, locale: loc, sphere: sph as any }); + const cacheSalt = anchorTempo.format('{yyyy}-{mm}-{dd}'); + const cacheKey = `${normalizedStr}::${cacheSalt}::${tz}::${cal}::${loc}::${sph}`; + const adapter = cacheAdapter ?? _state.config.cacheAdapter; + + let cachedIso: string | undefined; + if (!force && aiCacheOption !== false) { + if (adapter) { + try { + const val = await adapter.get(cacheKey); + if (val) { + cachedIso = val; + } + } catch (err: any) { + if (isDebug) console.log('[tempo-plugin-ai] Cache adapter read error:', err?.message); + } + } + + cachedIso ??= Tempo.cache.get(cacheKey); + } + + if (cachedIso) { + if (isDebug) console.log(`[tempo-plugin-ai] Cache hit: "${str}" -> ${cachedIso}`); + const cachedInstance = new Tempo(cachedIso, coreOptions); + return attachAiMeta(cachedInstance, { + provider: 'cache', + cached: true, + confidence: 1.0, + ambiguous: false, + granularity: 'day', + rawIso: cachedIso, + rawPrompt: isDebug ? str : undefined, + normalizedPrompt: isDebug ? normalizedStr : undefined, + }); + } + + if (!force) { + try { + const native = new Tempo(str, { ...coreOptions, silent: true }); + const hasNativeMatches = Tempo.cache.has(str) + || Tempo.cache.has(normalizedStr) + || RE_ISO_DATE_PREFIX.test(str.trim()) + || native.isValid; + + if (native.isValid && hasNativeMatches) { + if (isDebug) console.log(`[tempo-plugin-ai] Resolved natively: "${str}"`); + return attachAiMeta(native, { + provider: 'native', + cached: false, + confidence: 1.0, + ambiguous: false, + granularity: 'day', + rawPrompt: isDebug ? str : undefined, + normalizedPrompt: isDebug ? normalizedStr : undefined, + }); + } + } catch { + // Fallback + } + } + + const contextString = `Current Time: ${anchorTempo.format('{wkd}, {yyyy}-{mm}-{dd} {hh}:{mi}:{ss}')}, Timezone: ${tz}, Calendar: ${cal}, Locale: ${loc}, Hemisphere: ${sph}.`; + + const availableProviders = providers || _state.config.providers; + if (!availableProviders || availableProviders.length === 0) + throw new TempoAiError('No AI providers configured. Please call initAI().', 400); + + assertNoReservedProviderId(availableProviders); + + const mode = aiMode || _state.config.mode || AiMode.Fallback; + const effectiveMinConfidence = minConfidence ?? _state.config.minConfidence; + const effectiveHedgeDelay = hedgeDelay ?? _state.config.hedgeDelay; + + const winningCandidate = await executeWithMode( + mode, + availableProviders, + async (provider, signal) => { + const { rawContent, providerId, rateLimits } = await fetchFromProvider(provider, str, contextString, isDebug, signal, callTimeout); + const cleanContent = rawContent.replace(RE_MARKDOWN_JSON_PREFIX, '').replace(RE_MARKDOWN_JSON_SUFFIX, ''); + let parsedData: any; + try { + parsedData = JSON.parse(cleanContent); + } catch { + throw new TempoAiError(`Provider ${providerId} returned invalid JSON payload.`, 422); + } + const confidence = typeof parsedData?.confidence === 'number' ? parsedData.confidence : (parsedData?.iso === 'INVALID' ? 0.0 : 1.0); + + return { + data: parsedData, + providerId, + rateLimits, + confidence, + consensusKey: parsedData?.iso, + ambiguous: Boolean(parsedData?.ambiguous || parsedData?.iso === 'INVALID'), + }; + }, + { minConfidence: effectiveMinConfidence, debug: isDebug, tag: 'tempo-plugin-ai:parse', hedgeDelay: effectiveHedgeDelay }, + ); + + _state.limits = winningCandidate.rateLimits ?? null; + + const { data: parsedData, providerId, rateLimits, ambiguous: modeAmbiguous } = winningCandidate; + const rawIso = typeof parsedData?.iso === 'string' ? parsedData.iso : 'INVALID'; + const confidence = typeof winningCandidate.confidence === 'number' ? winningCandidate.confidence : (rawIso === 'INVALID' ? 0.0 : 1.0); + const ambiguous = Boolean(modeAmbiguous || parsedData?.ambiguous || rawIso === 'INVALID'); + const granularity = typeof parsedData?.granularity === 'string' ? parsedData.granularity : 'unknown'; + const reasoning = typeof parsedData?.reasoning === 'string' ? parsedData.reasoning : undefined; + + const isBelowMinConfidence = effectiveMinConfidence !== undefined && confidence < effectiveMinConfidence; + + if (rawIso === 'INVALID' || isBelowMinConfidence) { + const invalidInstance = new Tempo('INVALID', { ...coreOptions, catch: true }); + return attachAiMeta(invalidInstance, { + provider: providerId, + cached: false, + confidence, + ambiguous: true, + granularity, + rawIso: rawIso === 'INVALID' ? 'INVALID' : rawIso, + reasoning: isDebug ? reasoning : undefined, + rawPrompt: isDebug ? str : undefined, + normalizedPrompt: isDebug ? normalizedStr : undefined, + limits: rateLimits ?? undefined, + }); + } + + const parsedIso = `${rawIso.replace(RE_ISO_Z_SUFFIX, '')}[${tz}]`; + + // Determine TTL hierarchy: options.ttl > provider.ttl > global config.ttl > 3600000 (1 hour) + // In Consensus mode, providerId is the synthetic sentinel 'consensus' (not a real provider id), + // so use the minimum TTL across all participating providers as the conservative policy. + const providerTtl = providerId === AiMode.Consensus + ? availableProviders.reduce((min, p) => p.ttl === undefined ? min : (min === undefined ? p.ttl : Math.min(min, p.ttl)), undefined) + : availableProviders.find(p => p.id === providerId)?.ttl; + const resolvedTtl = ttl ?? providerTtl ?? _state.config.ttl ?? 3_600_000; + + if (aiCacheOption !== false) { + if (adapter) { + try { + const res = adapter.set(cacheKey, parsedIso, resolvedTtl); + if (res instanceof Promise) await res; + } catch (err: any) { + if (isDebug) console.log('[tempo-plugin-ai] Cache adapter write error:', err?.message); + } + } + Tempo.cache.set(cacheKey, parsedIso); + } + + const finalInstance = new Tempo(parsedIso, coreOptions); + + return attachAiMeta(finalInstance, { + provider: providerId, + cached: false, + confidence, + ambiguous, + granularity, + rawIso, + reasoning: isDebug ? reasoning : undefined, + rawPrompt: isDebug ? str : undefined, + normalizedPrompt: isDebug ? normalizedStr : undefined, + limits: rateLimits ?? undefined, + }); } +/** + * ## parseAI + * Parses natural language temporal expressions, relative dates, and unstructured prompt strings + * into high-precision `Tempo` instances using configured LLM providers. + * + * Supports single strings or batch arrays, hierarchical TTL/caching, execution modes (Fallback, Race, Consensus), + * and attached AI metadata (confidence score, provider info, reasoning, and ambiguity flags). + * + * @param input - A natural language date/time prompt string or an array of prompt strings for batch resolution + * @param options - Optional parsing configuration including provider overrides, timeout, caching, and execution modes + * @returns Promise resolving to a Tempo instance (or an array of Tempo instances / TempoAiErrors for batch requests) + * @example + * ```ts + * const tempo = await parseAI('the Friday after Thanksgiving next year'); + * console.log(tempo.format('{yyyy}-{mm}-{dd}')); + * console.log(tempo.ai?.provider); // 'groq' + * console.log(tempo.ai?.confidence); // 0.98 + * ``` + */ export async function parseAI(input: string, options?: AiParseOptions): Promise; export async function parseAI(input: string[], options?: AiParseOptions): Promise<(Tempo | TempoAiError)[]>; export async function parseAI( - input: string | string[], - options?: AiParseOptions + input: string | string[], + options?: AiParseOptions ): Promise { - if (Array.isArray(input)) { - if (options?.softErrors) { - const settled = await Promise.allSettled(input.map(str => parseSingleInput(str, options))); - return settled.map(s => s.status === 'fulfilled' ? s.value : (s.reason as TempoAiError)); - } - return Promise.all(input.map(str => parseSingleInput(str, options))); - } - - return parseSingleInput(input, options); + if (Array.isArray(input)) { + if (options?.softErrors) { + const settled = await Promise.allSettled(input.map(str => parseSingleInput(str, options))); + return settled.map(s => s.status === 'fulfilled' ? s.value : (s.reason as TempoAiError)); + } + return Promise.all(input.map(str => parseSingleInput(str, options))); + } + + return parseSingleInput(input, options); } diff --git a/packages/plugins/ai/src/functions/recurrence.ts b/packages/plugins/ai/src/functions/recurrence.ts index 52375aad..365ec4a9 100644 --- a/packages/plugins/ai/src/functions/recurrence.ts +++ b/packages/plugins/ai/src/functions/recurrence.ts @@ -1,28 +1,221 @@ -import type { Tempo } from '@magmacomputing/tempo'; +import { Tempo } from '@magmacomputing/tempo'; +import { isRRuleString, isFiniteRRule, parseRRule, expandRRuleEpochs } from '@magmacomputing/tempo/library'; +import { TempoAiError } from '../core/error.js'; +import { AiMode } from '../core/config.js'; +import { _state } from '../core/init.js'; +import { executeWithMode } from '../core/dispatch.js'; +import { fetchFromProvider, assertNoReservedProviderId } from '../core/support.js'; +import { RE_MARKDOWN_JSON_PREFIX, RE_MARKDOWN_JSON_SUFFIX, RE_RRULE_PREFIX } from '../core/patterns.js'; +import type { TempoRecurrenceOptions, TempoRecurrenceResult } from '../types/index.js'; -export interface TempoRecurrenceRule { - rrule: string; - next(count?: number): Tempo[]; +function expandOccurrences(rrule: string, anchor: Tempo, options?: { count?: number; after?: any; before?: any }): Tempo[] { + const afterTempo = options?.after ? new Tempo(options.after, anchor.config) : undefined; + const beforeTempo = options?.before ? new Tempo(options.before, anchor.config) : undefined; + const epochs = expandRRuleEpochs(rrule, anchor.epoch.ms, { + count: options?.count, + afterMs: afterTempo ? afterTempo.epoch.ms : undefined, + beforeMs: beforeTempo ? beforeTempo.epoch.ms : undefined, + }); + + return epochs.map(ms => new Tempo(ms, anchor.config)); +} + +function createRecurrenceResult( + rruleStr: string, + summaryText: string, + confidence: number, + providerId: string, + reasoning: string | undefined, + anchorTempo: Tempo, + defaultBatchSize: number, + options?: TempoRecurrenceOptions +): TempoRecurrenceResult { + const rule = parseRRule(rruleStr); + const hasWindow = Boolean(options?.after || options?.before); + const isFinite = isFiniteRRule(rruleStr) || Boolean(options?.before); + let sizeLimit: number; + if (hasWindow && isFinite) { + const windowOccurrences = expandOccurrences(rruleStr, anchorTempo, { count: rule.count ?? 1000, after: options?.after, before: options?.before }); + sizeLimit = rule.count !== undefined ? Math.min(rule.count, windowOccurrences.length) : windowOccurrences.length; + } else if (rule.count !== undefined) { + sizeLimit = rule.count; + } else if (isFinite) { + sizeLimit = expandOccurrences(rruleStr, anchorTempo, { count: 1000, after: options?.after, before: options?.before }).length; + } else { + sizeLimit = Number.POSITIVE_INFINITY; + } + + const cachedOccurrences: Tempo[] = []; + let offsetCursor = 0; + let fullyExpanded = false; + + const ensureCached = (neededCount: number): void => { + if (fullyExpanded || cachedOccurrences.length >= neededCount) return; + const missingCount = neededCount - cachedOccurrences.length; + const fetchCount = Math.max(missingCount, cachedOccurrences.length, defaultBatchSize); + const lastOccurrence = cachedOccurrences.length > 0 ? cachedOccurrences[cachedOccurrences.length - 1] : undefined; + const fresh = expandOccurrences(rruleStr, anchorTempo, { + count: fetchCount, + after: lastOccurrence ?? options?.after, + before: options?.before, + }); + cachedOccurrences.push(...fresh); + if (fresh.length < fetchCount) + fullyExpanded = true; + }; + + const take = (count?: number): Tempo[] => { + const fetchCount = count ?? defaultBatchSize; + if (isFinite && offsetCursor >= sizeLimit) return []; + const actualCount = isFinite ? Math.min(fetchCount, sizeLimit - offsetCursor) : fetchCount; + if (actualCount <= 0) return []; + + const needed = offsetCursor + actualCount; + ensureCached(needed); + + const batch = cachedOccurrences.slice(offsetCursor, offsetCursor + actualCount); + offsetCursor += batch.length; + return batch; + }; + + function* createIterator(): Generator { + let index = 0; + const maxYield = isFinite ? sizeLimit : defaultBatchSize; + while (index < maxYield) { + ensureCached(index + 1); + if (index >= cachedOccurrences.length) break; + yield cachedOccurrences[index]; + index++; + } + } + + return { + rrule: rruleStr, + summary: summaryText, + isFinite, + size: sizeLimit, + take, + [Symbol.iterator]: () => createIterator(), + confidence, + provider: providerId, + reasoning + }; } /** - * @internal Draft implementation scaffolded for v0.3.0 roadmap. - * ## recurrenceAI (Upcoming Export) - * Translates natural language descriptions of complex repeating schedules into - * structured RRULE strings and `Tempo` date generators. - * - * ### Why it fits Tempo: - * RRULE strings are notoriously complex to craft manually. `recurrenceAI` turns plain text - * into deterministic `Tempo` date sequences. - * - * ### Example Usage: - * ```ts - * const rule = await recurrenceAI('every 2nd and 4th Thursday of the month except company holidays', { - * timeZone: 'Europe/London' - * }); - * const nextDates = rule.next(5); // Returns array of 5 upcoming Tempo instances - * ``` + * ## recurrenceAI + * Multi-directional recurrence rule parser and translator. + * Accepts either a natural language schedule (e.g. "Every 2nd Tuesday of the month at 3pm") + * or a raw RFC 5545 RRULE string (e.g. "FREQ=MONTHLY;BYDAY=2TU;BYHOUR=15"). */ -export async function recurrenceAI(_prompt: string, _options?: Record): Promise { - throw new Error('recurrenceAI is not yet implemented in tempo-plugin-ai.'); +export async function recurrenceAI( + input: string, + options?: TempoRecurrenceOptions, +): Promise { + const isDebug = options?.debug ?? _state.config.debug ?? false; + const isRRule = isRRuleString(input); + + // Resolve full Tempo context hierarchy + const tz = options?.timeZone || (options?.anchor instanceof Tempo ? options.anchor.config.timeZone : undefined) || Tempo.options.timeZone; + const cal = options?.calendar || (options?.anchor instanceof Tempo ? options.anchor.config.calendar : undefined) || Tempo.options.calendar; + const loc = options?.locale || (options?.anchor instanceof Tempo ? options.anchor.config.locale : undefined) || Tempo.options.locale; + const sph = options?.sphere || (options?.anchor instanceof Tempo ? options.anchor.config.sphere : undefined) || Tempo.options.sphere; + + const contextConfig = { timeZone: tz, calendar: cal, locale: loc, sphere: sph }; + const anchorTempo = new Tempo(options?.anchor, contextConfig); + const defaultBatchSize = options?.count ?? 5; + + if (isRRule) { + const cleanRRule = input.trim().replace(RE_RRULE_PREFIX, ''); + if (isDebug) + console.log(`[tempo-plugin-ai:recurrence] Detected raw RRULE string: "${cleanRRule}"`); + + return createRecurrenceResult( + cleanRRule, + `Recurring schedule (${cleanRRule})`, + 1.0, + 'rrule-parser', + 'Parsed natively from RFC 5545 RRULE string input.', + anchorTempo, + defaultBatchSize, + options, + ); + } + + const availableProviders = options?.providers || _state.config.providers; + if (!availableProviders || availableProviders.length === 0) + throw new TempoAiError('No AI providers configured. Please call initAI().', 400); + + assertNoReservedProviderId(availableProviders); + + const mode = options?.mode || _state.config.mode || AiMode.Fallback; + const effectiveMinConfidence = options?.minConfidence ?? _state.config.minConfidence; + const callTimeout = options?.timeout; + const contextString = `Current Time: ${anchorTempo.format('{wkd}, {yyyy}-{mm}-{dd} {hh}:{mi}:{ss}')}, Timezone: ${tz}, Calendar: ${cal}, Locale: ${loc}, Hemisphere: ${sph}.`; + + const systemPrompt = `You are a calendar recurrence compiler. Read the user's natural language schedule and context. Return ONLY a valid JSON object matching this exact schema: +{ + "rrule": "Standard RFC 5545 RRULE string without RRULE: prefix (e.g., 'FREQ=WEEKLY;BYDAY=TU;BYHOUR=15')", + "summary": "Clear, concise human-friendly description localized to locale '${loc}' (e.g., 'Every Tuesday at 15:00')", + "reasoning": "Step-by-step calendar math explanation", + "confidence": 0.95 +} + +Rules: +- Standard RFC 5545 uppercase keys: FREQ (DAILY|WEEKLY|MONTHLY|YEARLY), BYDAY (MO,TU,WE,TH,FR,SA,SU), BYHOUR, BYMINUTE, BYMONTH, BYSETPOS, INTERVAL. +- "confidence": Float score between 0.0 (unparseable) and 1.0 (certain). +Do not include markdown blocks or text outside the JSON.`; + + const winningCandidate = await executeWithMode( + mode, + availableProviders, + async (provider, signal) => { + const { rawContent, providerId, rateLimits } = await fetchFromProvider( + provider, + input, + contextString, + isDebug, + signal, + callTimeout, + systemPrompt, + ); + const cleanContent = rawContent.replace(RE_MARKDOWN_JSON_PREFIX, '').replace(RE_MARKDOWN_JSON_SUFFIX, ''); + const parsedData = JSON.parse(cleanContent); + const confidence = typeof parsedData?.confidence === 'number' ? parsedData.confidence : 0.9; + + return { + data: parsedData, + providerId, + rateLimits, + confidence, + consensusKey: parsedData?.rrule, + }; + }, + { minConfidence: effectiveMinConfidence, debug: isDebug, tag: 'tempo-plugin-ai:recurrence', hedgeDelay: options?.hedgeDelay ?? _state.config.hedgeDelay }, + ); + + _state.limits = winningCandidate.rateLimits ?? null; + + const { data: parsedData, providerId } = winningCandidate; + if (typeof parsedData?.rrule !== 'string' || !parsedData.rrule.trim()) + throw new TempoAiError('Invalid recurrence response from AI provider: missing or empty rrule string.', 422); + + const rruleStr = parsedData.rrule.trim(); + const summaryText = typeof parsedData?.summary === 'string' ? parsedData.summary : (typeof parsedData?.humanReadable === 'string' ? parsedData.humanReadable : input); + const confidence = typeof winningCandidate.confidence === 'number' ? winningCandidate.confidence : 0.9; + const reasoning = typeof parsedData?.reasoning === 'string' ? parsedData.reasoning : undefined; + + if (effectiveMinConfidence !== undefined && confidence < effectiveMinConfidence) + throw new TempoAiError(`Recurrence rule confidence (${confidence}) is below the required threshold of ${effectiveMinConfidence}.`, 422); + + return createRecurrenceResult( + rruleStr, + summaryText, + confidence, + providerId, + reasoning, + anchorTempo, + defaultBatchSize, + options, + ); } diff --git a/packages/plugins/ai/src/functions/schedule.ts b/packages/plugins/ai/src/functions/schedule.ts index 0e41e9c1..cff5952d 100644 --- a/packages/plugins/ai/src/functions/schedule.ts +++ b/packages/plugins/ai/src/functions/schedule.ts @@ -1,27 +1,409 @@ -import type { Tempo } from '@magmacomputing/tempo'; +import { Tempo, Interval } from '@magmacomputing/tempo'; +import { isString, isNumber, isFunction, DAY_MAP, ISO_WEEKDAY_NAMES, type DayKey } from '@magmacomputing/tempo/library'; +import { TempoAiError } from '../core/error.js'; +import { AiMode } from '../core/config.js'; +import { _state } from '../core/init.js'; +import { executeWithMode } from '../core/dispatch.js'; +import { fetchFromProvider, assertNoReservedProviderId } from '../core/support.js'; +import { RE_DURATION_MINUTES, RE_DURATION_HOURS, RE_MARKDOWN_JSON_PREFIX, RE_MARKDOWN_JSON_SUFFIX, RE_ISO_WEEKDAY_DIGIT } from '../core/patterns.js'; +import type { TempoScheduleOptions, TempoScheduleResult, TempoWorkingHours, TempoInterval, TempoScheduleMeta, AiProvider } from '../types/index.js'; -export interface TempoInterval { - start: Tempo; - end: Tempo; +function normalizeBusyEvents(rawEvents?: any[], timeZone = 'UTC'): Array<{ start: Tempo; end: Tempo; title?: string | undefined }> { + if (!Array.isArray(rawEvents)) return []; + + const parsePoint = (val: any): Tempo => { + if (!val) return new Tempo({ timeZone }); + if (Tempo.isTempo(val)) return val; + return new Tempo(val, { timeZone }); + } + + return rawEvents.map(evt => { + let start: Tempo; + let end: Tempo; + let title = 'Busy'; + + if (evt && typeof evt === 'object') { + if ('start' in evt && 'end' in evt) { + start = parsePoint((evt as any).start); + end = parsePoint((evt as any).end); + if ('title' in evt && (evt as any).title !== undefined && String((evt as any).title).trim().length > 0) + title = String((evt as any).title); + else if ('label' in evt && (evt as any).label !== undefined && String((evt as any).label).trim().length > 0) + title = String((evt as any).label); + } else if (Array.isArray(evt) && evt.length >= 2) { + start = parsePoint(evt[0]); + end = parsePoint(evt[1]); + } else { + start = parsePoint(evt); + end = start.add('1 hour'); + } + } else { + start = parsePoint(evt); + end = start.add('1 hour'); + } + + return { start, end, title }; + }); +} + +function parseDurationMinutes(prompt: string, fallback?: number): number { + if (isNumber(fallback) && fallback > 0) return fallback; + const match = prompt.match(RE_DURATION_MINUTES); + if (match) return parseInt(match[1], 10); + const hourMatch = prompt.match(RE_DURATION_HOURS); + if (hourMatch) return Math.round(parseFloat(hourMatch[1]) * 60); + return 30; // default 30 minutes +} + +function formatActiveDays(days?: Array): string { + const active = days ?? [1, 2, 3, 4, 5]; + return active.map(d => { + if (typeof d === 'number' && (ISO_WEEKDAY_NAMES as any)[d]) return (ISO_WEEKDAY_NAMES as any)[d]; + if (typeof d === 'string') { + const upper = d.toUpperCase() as DayKey; + const num = (DAY_MAP as any)[upper]; + if (num && (ISO_WEEKDAY_NAMES as any)[num]) return (ISO_WEEKDAY_NAMES as any)[num]; + } + return String(d); + }).join(', '); +} + +function buildContextPrompt( + anchorTempo: Tempo, + timeZone: string, + workingHours: TempoWorkingHours, + busyEvents: Array<{ start: Tempo; end: Tempo; title?: string | undefined }>, + durationMinutes: number +): string { + const activeDays = formatActiveDays(workingHours.days); + const whStart = workingHours.start ?? '09:00'; + const whEnd = workingHours.end ?? '17:00'; + + let busySummary = 'None'; + if (busyEvents.length > 0) { + busySummary = busyEvents.map(b => + `- ${b.title || 'Busy'}: ${b.start.format('{yyyy}-{mm}-{dd}T{hh}:{mi}:{ss}')} to ${b.end.format('{yyyy}-{mm}-{dd}T{hh}:{mi}:{ss}')} [${b.start.tz}]` + ).join('\n'); + } + + return `Reference Anchor Time: ${anchorTempo.format('{yyyy}-{mm}-{dd}T{hh}:{mi}:{ss}')} (${timeZone}) +Target TimeZone: ${timeZone} +Working Hours: ${whStart} to ${whEnd} (${activeDays}) in ${workingHours.timeZone || timeZone} +Required Slot Duration: ${durationMinutes} minutes +Existing Booked Busy Slots to Avoid: +${busySummary}`; +} + +const SCHEDULE_SYSTEM_PROMPT = `You are a high-precision calendar scheduling engine. +Your task is to analyze a natural language scheduling prompt and resolve the single optimal start and end time interval. + +Instructions: +- Calculate an available interval that falls strictly within working hours and active weekdays. +- Ensure the slot does NOT overlap with any existing booked busy slots. +- Output ONLY valid JSON containing: + "start": ISO 8601 string with timeZone offset (e.g. "2026-08-11T14:15:00-07:00") + "end": ISO 8601 string with timeZone offset (e.g. "2026-08-11T15:00:00-07:00") + "durationMinutes": number + "summary": string (human-friendly summary of the slot) + "reasoning": string (explanation of why this slot was selected) + "confidence": number between 0.0 and 1.0 + "alternatives": array of secondary { "start": "...", "end": "..." } options if available`; + +function wrapScheduleInterval(interval: Interval, meta: TempoScheduleMeta): TempoScheduleResult { + const frozenMeta = Object.freeze(meta); + const boundMethodCache = new Map(); + + return new Proxy(interval, { + get(target, prop) { + if (Object.hasOwn(frozenMeta, prop)) + return (frozenMeta as any)[prop]; + + if (prop === 'constructor') + return Reflect.get(target, prop, target); + + if (boundMethodCache.has(prop)) + return boundMethodCache.get(prop); + + const val = Reflect.get(target, prop, target); + if (isFunction(val)) { + const bound = val.bind(target); + boundMethodCache.set(prop, bound); + return bound; + } + return val; + }, + has(target, prop) { + if (Object.hasOwn(frozenMeta, prop)) return true; + return Reflect.has(target, prop); + }, + getOwnPropertyDescriptor(target, prop) { + if (Object.hasOwn(frozenMeta, prop)) { + return { + value: (frozenMeta as any)[prop], + writable: false, + configurable: true, + enumerable: true, + }; + } + return Reflect.getOwnPropertyDescriptor(target, prop); + }, + ownKeys(target) { + const keys = Reflect.ownKeys(target); + for (const k of Object.keys(frozenMeta)) { + if (!keys.includes(k)) keys.push(k); + } + return keys; + }, + }) as unknown as TempoScheduleResult; } /** - * @internal Draft implementation scaffolded for v0.3.0 roadmap. - * ## scheduleAI (Upcoming Export) - * Resolves natural language scheduling prompts against working hours, existing calendar - * events, and timezones into an optimal start/end `Tempo` interval. - * - * ### Why it fits Tempo: - * Solves non-trivial calendar slot finding while producing strongly typed `Tempo` interval boundaries. - * - * ### Example Usage: - * ```ts - * const slot = await scheduleAI('Find 45 minutes next Tuesday afternoon after 2pm PST excluding lunch', { - * workingHours: { start: '09:00', end: '17:00', timeZone: 'America/Los_Angeles' } - * }); - * console.log(slot.start.toString()); // "2026-08-04T14:15:00[America/Los_Angeles]" - * ``` + * ## scheduleAI + * Resolves natural language scheduling prompts against working hours, existing calendar + * events, and timezones into an optimal `TempoScheduleResult` (implementing `Interval`). + * + * @param prompt - Natural language scheduling prompt + * @param options - Scheduling configuration including working hours, existing busy events, anchor date, and timeZone + * @returns Promise resolving to an Interval instance decorated with AI scheduling metadata */ -export async function scheduleAI(_prompt: string, _options?: Record): Promise { - throw new Error('scheduleAI is not yet implemented in tempo-plugin-ai.'); +export async function scheduleAI( + prompt: string, + options?: TempoScheduleOptions +): Promise { + if (!isString(prompt) || prompt.trim() === '') { + throw new TempoAiError('Invalid scheduling prompt provided to scheduleAI', 400); + } + + const state = _state; + const availableProviders = options?.providers ?? state.config.providers; + + if (!availableProviders || availableProviders.length === 0) + throw new TempoAiError('No AI providers configured for scheduleAI. Call initAI() or supply providers in options.', 400); + + assertNoReservedProviderId(availableProviders); + + const resolvedTz = options?.timeZone + || (options?.anchor instanceof Tempo ? options.anchor.config?.timeZone || options.anchor.tz : undefined) + || Tempo.options?.timeZone + || 'UTC'; + const anchorTempo = new Tempo(options?.anchor, { timeZone: resolvedTz }); + const timeZone = options?.timeZone || anchorTempo.tz || 'UTC'; + const workingHours: TempoWorkingHours = { + start: options?.workingHours?.start ?? '09:00', + end: options?.workingHours?.end ?? '17:00', + days: options?.workingHours?.days ?? [1, 2, 3, 4, 5], + timeZone: options?.workingHours?.timeZone ?? timeZone, + } + + const rawBusy = options?.events ?? options?.intervals; + const busyEvents = normalizeBusyEvents(rawBusy, timeZone); + const durationMinutes = parseDurationMinutes(prompt, options?.durationMinutes); + + const contextString = buildContextPrompt(anchorTempo, timeZone, workingHours, busyEvents, durationMinutes); + const isDebug = Boolean(options?.debug ?? state.config.debug); + const mode = options?.mode || state.config.mode || AiMode.Fallback; + const callTimeout = options?.timeout ?? state.config.timeout ?? 15000; + + const winningCandidate = await executeWithMode( + mode, + availableProviders, + async (provider, signal) => { + const { rawContent, providerId, rateLimits } = await fetchFromProvider( + provider, + prompt, + contextString, + isDebug, + signal, + callTimeout, + SCHEDULE_SYSTEM_PROMPT, + ); + + const cleanContent = rawContent.replace(RE_MARKDOWN_JSON_PREFIX, '').replace(RE_MARKDOWN_JSON_SUFFIX, ''); + let parsed: any; + try { + parsed = JSON.parse(cleanContent); + } catch { + throw new TempoAiError(`Provider ${provider.id} returned invalid JSON payload.`, 422); + } + + if (!parsed.start || !parsed.end) + throw new TempoAiError(`Provider ${provider.id} missing start or end ISO timestamp.`, 422); + + let finalStart: Tempo; + let finalEnd: Tempo; + try { + finalStart = new Tempo(parsed.start, { timeZone }); + finalEnd = new Tempo(parsed.end, { timeZone }); + } catch { + throw new TempoAiError(`Provider ${provider.id} returned unparseable start or end timestamp.`, 422); + } + + if (finalEnd.epoch.ms <= finalStart.epoch.ms) + throw new TempoAiError(`Provider ${provider.id} proposed end time before or equal to start time.`, 422); + + const startKey = finalStart.format('{yyyy}-{mm}-{dd}T{hh}:{mi}:{ss}'); + const endKey = finalEnd.format('{yyyy}-{mm}-{dd}T{hh}:{mi}:{ss}'); + + return { + data: { + parsed, + startTempo: finalStart, + endTempo: finalEnd, + summary: parsed.summary || `Scheduled slot ${finalStart.format('{yyyy}-{mm}-{dd} {hh}:{mi}')} to ${finalEnd.format('{hh}:{mi}')}`, + reasoning: parsed.reasoning || 'Resolved slot via AI scheduler.', + alternatives: Array.isArray(parsed.alternatives) ? parsed.alternatives : [], + }, + providerId, + rateLimits, + confidence: isNumber(parsed.confidence) ? parsed.confidence : 0.9, + consensusKey: `${startKey}::${endKey}`, + }; + }, + { minConfidence: options?.minConfidence ?? state.config.minConfidence, debug: isDebug, tag: 'tempo-plugin-ai:schedule', hedgeDelay: options?.hedgeDelay ?? state.config.hedgeDelay }, + ); + + _state.limits = winningCandidate.rateLimits ?? null; + + const { data: scheduleData, providerId } = winningCandidate; + const confidence = typeof winningCandidate.confidence === 'number' ? winningCandidate.confidence : 0.9; + + const minConf = options?.minConfidence ?? state.config.minConfidence ?? 0.0; + if (confidence < minConf) + throw new TempoAiError(`scheduleAI confidence (${confidence}) is below the required threshold of ${minConf}`, 422); + + let finalStart = scheduleData.startTempo; + let finalEnd = scheduleData.endTempo; + let conflictBumped = false; + let originalSlot: TempoInterval | undefined; + + const whStartStr = workingHours.start ?? '09:00'; + const whEndStr = workingHours.end ?? '17:00'; + const [whStartH, whStartM] = whStartStr.split(':').map(v => parseInt(v, 10) || 0); + const [whEndH, whEndM] = whEndStr.split(':').map(v => parseInt(v, 10) || 0); + const whTz = workingHours.timeZone || timeZone; + + const rawDays = Array.isArray(workingHours.days) ? workingHours.days : [1, 2, 3, 4, 5]; + const validDays = rawDays + .map(d => { + if (typeof d === 'number' && Number.isInteger(d) && d >= 1 && d <= 7) return d; + if (typeof d === 'string') { + const trimmed = d.trim(); + const upper = trimmed.toUpperCase() as DayKey; + if (upper in DAY_MAP) return (DAY_MAP as any)[upper]; + if (RE_ISO_WEEKDAY_DIGIT.test(trimmed)) return Number(trimmed); + } + return null; + }) + .filter((d): d is number => d !== null); + + const activeDaysSet = new Set(validDays.length > 0 ? validDays : [1, 2, 3, 4, 5]); + + const advanceToNextActiveDay = (curZdt: Temporal.ZonedDateTime): Temporal.ZonedDateTime => { + let next = curZdt.add({ days: 1 }).startOfDay().add({ hours: whStartH, minutes: whStartM }); + while (!activeDaysSet.has(next.dayOfWeek)) { + next = next.add({ days: 1 }); + } + return next; + } + + // Deterministic Conflict & Working Hours Validation using core Interval.overlaps() + const MAX_ADJUSTMENT_ITERATIONS = 50; + let iterations = 0; + let lastConflictingEvent: any = null; + let slotSatisfied = false; + + while (iterations < MAX_ADJUSTMENT_ITERATIONS) { + iterations++; + + // Validate and adjust against working hours and active days + const startZdt = finalStart.toDateTime().withTimeZone(whTz); + const endZdt = finalEnd.toDateTime().withTimeZone(whTz); + + if (!activeDaysSet.has(startZdt.dayOfWeek)) { + conflictBumped = true; + if (!originalSlot) originalSlot = { start: scheduleData.startTempo, end: scheduleData.endTempo }; + const nextZdt = advanceToNextActiveDay(startZdt); + finalStart = new Tempo(nextZdt, { timeZone }); + finalEnd = finalStart.add(`${durationMinutes} minutes`); + continue; + } + + const dayStart = startZdt.startOfDay().add({ hours: whStartH, minutes: whStartM }); + const dayEnd = startZdt.startOfDay().add({ hours: whEndH, minutes: whEndM }); + + if (startZdt.epochNanoseconds < dayStart.epochNanoseconds) { + conflictBumped = true; + if (!originalSlot) originalSlot = { start: scheduleData.startTempo, end: scheduleData.endTempo }; + finalStart = new Tempo(dayStart, { timeZone }); + finalEnd = finalStart.add(`${durationMinutes} minutes`); + continue; + } else if (endZdt.epochNanoseconds > dayEnd.epochNanoseconds) { + conflictBumped = true; + if (!originalSlot) originalSlot = { start: scheduleData.startTempo, end: scheduleData.endTempo }; + const nextZdt = advanceToNextActiveDay(startZdt); + finalStart = new Tempo(nextZdt, { timeZone }); + finalEnd = finalStart.add(`${durationMinutes} minutes`); + continue; + } + + // Re-check resulting interval against all busyEvents + const currentInterval = new Interval(finalStart, finalEnd); + const nextConflict = busyEvents.find(b => currentInterval.overlaps(new Interval(b.start, b.end))); + + if (nextConflict) { + conflictBumped = true; + if (!originalSlot) originalSlot = { start: scheduleData.startTempo, end: scheduleData.endTempo }; + lastConflictingEvent = nextConflict; + finalStart = new Tempo(nextConflict.end, { timeZone }); + finalEnd = finalStart.add(`${durationMinutes} minutes`); + continue; + } + + // Slot is valid against all busyEvents and workingHours + slotSatisfied = true; + break; + } + + if (!slotSatisfied) + throw new TempoAiError(`Unable to find an available schedule slot within ${MAX_ADJUSTMENT_ITERATIONS} iterations`, 422); + + let reasoning = scheduleData.reasoning; + if (conflictBumped) { + const conflictTitle = lastConflictingEvent?.title || 'Busy'; + reasoning = `[Adjusted for conflict] Shifted slot past conflicting event "${conflictTitle}" to ${finalStart.format('{yyyy}-{mm}-{dd} {hh}:{mi}')}. ${scheduleData.reasoning}`; + } + + // Create actual Interval instance + const rawInterval = new Interval(finalStart, finalEnd); + + // Process alternative slots into Interval instances + const alternatives: Array> = scheduleData.alternatives + .map((alt: any) => { + try { + const s = new Tempo(alt.start, { timeZone }); + const e = new Tempo(alt.end, { timeZone }); + return new Interval(s, e); + } catch { + return null; + } + }) + .filter((i: any): i is Interval => i !== null); + + const actualDuration = Math.round((finalEnd.epoch.ms - finalStart.epoch.ms) / 60000); + + return wrapScheduleInterval(rawInterval, { + durationMinutes: actualDuration, + summary: scheduleData.summary, + reasoning, + confidence, + provider: providerId, + alternatives, + ai: { + provider: providerId, + confidence, + conflictBumped, + originalSlot, + reasoning, + }, + }); } diff --git a/packages/plugins/ai/src/index.ts b/packages/plugins/ai/src/index.ts index fccea421..30c1ec71 100644 --- a/packages/plugins/ai/src/index.ts +++ b/packages/plugins/ai/src/index.ts @@ -1,11 +1,20 @@ // Core Infrastructure & Configuration export { TempoAiError } from './core/error.js'; -export * from './core/types.js'; +export * from './types/index.js'; export * from './core/config.js'; -export { initAI, clearAiCache, getAiRateLimits } from './core/init.js'; + +// AI Manifest Support +export { loadRemoteManifest, resetManifestCache, DEFAULT_REMOTE_MANIFEST_URL } from './core/manifest.js'; + +// AI Core Functions +export { initAI, resetAI, clearAiCache, getAiRateLimits, getAiProviderRateLimits, getAiConfig } from './core/init.js'; // AI Function Handlers export { parseAI } from './functions/parse.js'; +export { recurrenceAI } from './functions/recurrence.js'; +export { scheduleAI } from './functions/schedule.js'; +export { contextAI } from './functions/context.js'; +export { diffAI } from './functions/diff.js'; /* * ============================================================================ @@ -16,19 +25,7 @@ export { parseAI } from './functions/parse.js'; */ // /** Formats a Tempo instance into human-friendly, contextual narrative text */ -// export { formatAI } from './functions/format.js'; +// export { formatAI, type TempoAiFormatResult, type FormatItem, type AiFormatOptions } from './functions/format.js'; // /** Scans unstructured text and extracts embedded temporal entities & events */ -// export { extractAI, type TempoEvent } from './functions/extract.js'; - -// /** Expresses the delta between two dates in human, business, or operational terms */ -// export { diffAI, type TempoAiDiffResult } from './functions/diff.js'; - -// /** Resolves natural language scheduling prompts into optimal Tempo intervals */ -// export { scheduleAI, type TempoInterval } from './functions/schedule.js'; - -// /** Translates natural language descriptions of repeating schedules into RRULEs */ -// export { recurrenceAI, type TempoRecurrenceRule } from './functions/recurrence.js'; - -// /** Infers timeZone, locale, and calendar from ambiguous location or text strings */ -// export { contextAI, inferContextAI, type TempoContext } from './functions/context.js'; +// export { extractAI, type TempoAiExtractResult, type TempoExtractedEvent, type TempoEvent, type AiExtractOptions } from './functions/extract.js'; diff --git a/packages/plugins/ai/src/core/types.ts b/packages/plugins/ai/src/types/common.type.ts similarity index 50% rename from packages/plugins/ai/src/core/types.ts rename to packages/plugins/ai/src/types/common.type.ts index 83e5e1f6..6baf84bc 100644 --- a/packages/plugins/ai/src/core/types.ts +++ b/packages/plugins/ai/src/types/common.type.ts @@ -1,36 +1,42 @@ import type { Tempo } from '@magmacomputing/tempo'; -import type { AiMode } from './config.js'; +import type { AiMode } from '../core/config.js'; /** - * ## TempoAiMeta - * Frozen metadata object attached to Tempo instances produced by `parseAI`. + * ## TempoBaseAiMeta + * Fundamental AI resolution telemetry and metadata shared across all AI functions. */ -export interface TempoAiMeta { +export interface TempoBaseAiMeta { /** Resolution source ('native', 'cache', or provider ID like 'groq', 'openai', 'ollama') */ readonly provider: string; - /** Whether the result was retrieved from cache */ - readonly cached: boolean; /** Confidence rating from 0.0 (unparseable) to 1.0 (certain) */ readonly confidence: number; - /** Whether the input prompt had multiple possible interpretations */ - readonly ambiguous: boolean; - /** Granularity level of the parsed date ('year' | 'month' | 'day' | 'hour' | 'minute' | 'second' | 'unknown') */ - readonly granularity: string; - /** Raw un-augmented ISO 8601 string returned by the LLM (if applicable) */ - readonly rawIso?: string | undefined; - /** Step-by-step calendar math reasoning (included when debug: true or when provided by LLM) */ + /** Whether the result was retrieved from cache */ + readonly cached?: boolean | undefined; + /** Step-by-step reasoning or justification provided by the engine/LLM */ readonly reasoning?: string | undefined; + /** Rate limit snapshot returned by the provider HTTP headers for this request */ + readonly limits?: AiRateLimits | undefined; /** Raw prompt input (only included when debug: true) */ readonly rawPrompt?: string | undefined; /** Normalized prompt input (only included when debug: true) */ readonly normalizedPrompt?: string | undefined; + /** Arbitrary provider-specific extra metadata */ + readonly [key: string]: any; } -declare module '@magmacomputing/tempo' { - interface Tempo { - /** Frozen AI resolution metadata attached when parsed via parseAI */ - ai?: TempoAiMeta | undefined; - } +/** + * ## AiCacheAdapter + * Interface for synchronous or asynchronous custom storage engines (e.g. Redis, Cloudflare KV, Memcached). + */ +export interface AiCacheAdapter { + /** Retrieve a value by key */ + get(key: string): Promise | string | undefined; + /** Store a value by key with optional TTL in milliseconds */ + set(key: string, value: string, ttlMs?: number): Promise | void; + /** Delete a specific entry by key */ + delete?(key: string): Promise | void; + /** Clear entries, optionally matching a key prefix */ + clear?(prefix?: string): Promise | void; } /** @@ -48,43 +54,14 @@ export interface AiProvider { model?: string; /** Optional parameter name for max token limit (e.g. 'max_tokens' or 'max_completion_tokens') */ tokenParam?: string | undefined; + /** Optional cache TTL override in milliseconds for entries produced by this provider */ + ttl?: number | undefined; + /** Optional HTTP request timeout override in milliseconds for requests to this provider */ + timeout?: number | undefined; /** Optional LLM parameters (e.g. temperature, max_tokens, top_p) */ options?: Record; } -/** - * ## AiParseOptions - * Options passed to `parseAI(input, options)`. - */ -export interface AiParseOptions { - /** Reference anchor date/time instance or string */ - anchor?: any; - /** Target timeZone override */ - timeZone?: string; - /** Target calendar override */ - calendar?: string; - /** Target locale override */ - locale?: string; - /** Target sphere override */ - sphere?: string; - /** If true, bypasses cache and native parsing to force an LLM fetch */ - force?: boolean; - /** If false, disables reading and writing to cache */ - cache?: boolean; - /** If true, logs prompt context and LLM payloads to console */ - debug?: boolean; - /** Execution mode across provider farm (`AiMode.Fallback` | `AiMode.Race` | `AiMode.Consensus` or string literal) */ - mode?: AiMode; - /** Per-request provider configuration overrides */ - providers?: AiProvider[]; - /** Strict minimum confidence threshold (0.0 to 1.0). Throws TempoAiError(422) if score is lower */ - minConfidence?: number; - /** If true, places TempoAiError into array index position instead of rejecting batch */ - softErrors?: boolean; - /** Allow extra options */ - [key: string]: any; -} - /** * ## AiConfig * Configuration options for the AI parsing plugin. @@ -92,8 +69,24 @@ export interface AiParseOptions { export interface AiConfig { /** An array of fallback providers to use for routing */ providers?: AiProvider[] | undefined; + /** Execution mode across provider farm (`AiMode.Fallback` | `AiMode.Race` | `AiMode.Consensus` or string literal) */ + mode?: AiMode | undefined; + /** Strict minimum confidence threshold (0.0 to 1.0) */ + minConfidence?: number | undefined; /** Optional custom cache implementation for storing parsed strings */ cache?: Map | undefined; + /** Optional custom cache storage engine (e.g., Redis, KV store) for storing parsed strings */ + cacheAdapter?: AiCacheAdapter | undefined; + /** Optional global cache TTL in milliseconds for AI parsing entries (default: 3600000ms / 1 hour) */ + ttl?: number | undefined; + /** Optional global timeout in milliseconds for AI requests (default: 15000ms) */ + timeout?: number | undefined; + /** Optional remote manifest URL or false to disable remote defaults (default: 'https://tempo.magmacomputing.com.au/providers.v1.json') */ + remoteConfigUrl?: string | false | undefined; + /** Optional custom resolver hook to fetch provider default options by ID */ + fetchDefaults?: ((providerId: string) => Promise | null>) | undefined; + /** Optional delay in milliseconds before initiating speculative hedging in AiMode.Hedged (default: 800ms) */ + hedgeDelay?: number | undefined; /** If true, logs the spoon-fed LLM context prompt and raw LLM response to the console */ debug?: boolean | undefined; } diff --git a/packages/plugins/ai/src/types/context.type.ts b/packages/plugins/ai/src/types/context.type.ts new file mode 100644 index 00000000..ef4f00eb --- /dev/null +++ b/packages/plugins/ai/src/types/context.type.ts @@ -0,0 +1,52 @@ +import type { AiMode } from '../core/config.js'; +import type { AiCacheAdapter, AiProvider } from './common.type.js'; + +/** + * ## TempoContext + * The inferred regional and calendar settings resolved by `contextAI`. + */ +export interface TempoContext { + /** Inferred IANA time zone identifier (e.g. 'America/New_York') */ + timeZone: string; + /** Inferred BCP 47 language/region tag (e.g. 'en-US') */ + locale: string; + /** Inferred Unicode calendar system type (e.g. 'gregory') */ + calendar: string; + /** Inferred hemisphere, constrained strictly to 'north' or 'south' (omitted if unknowable) */ + sphere?: 'north' | 'south' | undefined; + /** Confidence score between 0.0 (highly ambiguous) and 1.0 (certain) */ + confidence: number; + /** The identifier of the AI provider that successfully produced this context */ + provider: string; + /** Step-by-step reasoning explaining the inference */ + reasoning?: string | undefined; +} + +/** + * ## AiContextOptions + * Configuration options passed to `contextAI(text, options)`. + */ +export interface AiContextOptions { + /** If true, bypasses cache to force a fresh LLM fetch */ + force?: boolean | undefined; + /** If false, disables reading and writing to cache */ + cache?: boolean | undefined; + /** Optional custom cache adapter engine (e.g. Redis, KV store) for this request */ + cacheAdapter?: AiCacheAdapter | undefined; + /** Optional TTL override in milliseconds for cached result */ + ttl?: number | undefined; + /** If true, logs prompt context and LLM payloads to console */ + debug?: boolean | undefined; + /** Execution mode across provider farm (`AiMode.Fallback` | `AiMode.Race` | `AiMode.Consensus` or string literal) */ + mode?: AiMode | undefined; + /** Per-request provider configuration overrides */ + providers?: AiProvider[] | undefined; + /** Strict minimum confidence threshold (0.0 to 1.0). Throws TempoAiError(422) if score is lower */ + minConfidence?: number | undefined; + /** Optional request timeout in milliseconds (overrides provider and global timeout) */ + timeout?: number | undefined; + /** Optional delay in milliseconds before initiating speculative hedging in AiMode.Hedged (default: 800ms) */ + hedgeDelay?: number | undefined; + /** Allow extra custom properties */ + [key: string]: any; +} diff --git a/packages/plugins/ai/src/types/diff.type.ts b/packages/plugins/ai/src/types/diff.type.ts new file mode 100644 index 00000000..7d3c1e7e --- /dev/null +++ b/packages/plugins/ai/src/types/diff.type.ts @@ -0,0 +1,74 @@ +import type { AiMode } from '../core/config.js'; +import type { AiCacheAdapter, AiProvider } from './common.type.js'; + +/** + * ## TempoAiDiffResult + * The calculated and AI-formatted natural difference between two date-time points. + */ +export interface TempoAiDiffResult { + /** Human-friendly, contextual narrative text summarizing the difference */ + formatted: string; + /** Total calendar days between start and end */ + days?: number | undefined; + /** Total elapsed calendar hours between start and end */ + hours?: number | undefined; + /** Total business working days (excluding weekends and matching holidays) between start and end */ + businessDays?: number | undefined; + /** List of holiday dates (YYYY-MM-DD) encountered within the interval */ + holidays?: string[] | undefined; + /** Confidence rating from 0.0 (unparseable) to 1.0 (certain) */ + confidence: number; + /** Resolution source ('cache' or provider ID like 'groq', 'gemini', 'openai') */ + provider: string; + /** Step-by-step reasoning or justification provided by the engine/LLM */ + reasoning?: string | undefined; +} + +/** + * ## DiffPair + * Input pair representation for batch diff processing. + */ +export interface DiffPair { + start: any; + end: any; + prompt?: string | undefined; +} + +/** + * ## AiDiffOptions + * Configuration options passed to `diffAI(start, end, prompt, options)`. + */ +export interface AiDiffOptions { + /** Optional target timeZone for relative calculation and business day boundaries */ + timeZone?: string | undefined; + /** Optional target locale override for language/formatting specific output */ + locale?: string | string[] | undefined; + /** Explicit array of holiday dates to exclude from business days (format: 'YYYY-MM-DD') */ + holidays?: string[] | undefined; + /** Expected country/region code (e.g. 'AU', 'US') */ + region?: string | undefined; + /** If true, bypasses cache to force a fresh LLM fetch */ + force?: boolean | undefined; + /** If false, disables reading and writing to cache */ + cache?: boolean | undefined; + /** Optional custom cache adapter engine (e.g. Redis, KV store) for this request */ + cacheAdapter?: AiCacheAdapter | undefined; + /** Optional TTL override in milliseconds for cached result */ + ttl?: number | undefined; + /** If true, logs prompt context and LLM payloads to console */ + debug?: boolean | undefined; + /** Execution mode across provider farm (`AiMode.Fallback` | `AiMode.Race` | `AiMode.Consensus` | `AiMode.Hedged` | `AiMode.RoundRobin` | `AiMode.Adaptive` or string literal) */ + mode?: AiMode | undefined; + /** Per-request provider configuration overrides */ + providers?: AiProvider[] | undefined; + /** Strict minimum confidence threshold (0.0 to 1.0). Throws TempoAiError(422) if score is lower */ + minConfidence?: number | undefined; + /** If true, returns TempoAiError into array index position instead of rejecting batch */ + softErrors?: boolean | undefined; + /** Optional request timeout in milliseconds (overrides provider and global timeout) */ + timeout?: number | undefined; + /** Optional delay in milliseconds before initiating speculative hedging in AiMode.Hedged (default: 800ms) */ + hedgeDelay?: number | undefined; + /** Allow extra custom properties */ + [key: string]: any; +} diff --git a/packages/plugins/ai/src/types/index.ts b/packages/plugins/ai/src/types/index.ts new file mode 100644 index 00000000..9e85801a --- /dev/null +++ b/packages/plugins/ai/src/types/index.ts @@ -0,0 +1,7 @@ +export * from './common.type.js'; +export * from './parse.type.js'; +export * from './recurrence.type.js'; +export * from './schedule.type.js'; +export * from './context.type.js'; +export * from './diff.type.js'; + diff --git a/packages/plugins/ai/src/types/parse.type.ts b/packages/plugins/ai/src/types/parse.type.ts new file mode 100644 index 00000000..045e3ebe --- /dev/null +++ b/packages/plugins/ai/src/types/parse.type.ts @@ -0,0 +1,69 @@ +import type { Tempo } from '@magmacomputing/tempo'; +import type { AiMode } from '../core/config.js'; +import type { AiCacheAdapter, AiProvider, TempoBaseAiMeta } from './common.type.js'; + +declare module '@magmacomputing/tempo' { + interface Tempo { + /** Frozen AI resolution metadata attached when parsed via parseAI */ + ai?: TempoParseAiMeta | undefined; + } +} + +/** + * ## TempoParseAiMeta + * Frozen AI resolution metadata attached to Tempo instances produced by `parseAI`. + */ +export interface TempoParseAiMeta extends TempoBaseAiMeta { + /** Whether the result was retrieved from cache */ + readonly cached: boolean; + /** Whether the input prompt had multiple possible interpretations */ + readonly ambiguous: boolean; + /** Granularity level of the parsed date ('year' | 'month' | 'day' | 'hour' | 'minute' | 'second' | 'unknown') */ + readonly granularity: string; + /** Raw un-augmented ISO 8601 string returned by the LLM (if applicable) */ + readonly rawIso?: string | undefined; +} + +/** Backward-compatible alias for TempoParseAiMeta */ +export type TempoAiMeta = TempoParseAiMeta; + +/** + * ## AiParseOptions + * Options passed to `parseAI(input, options)`. + */ +export interface AiParseOptions { + /** Reference anchor date/time instance or string */ + anchor?: Tempo | Date | string | number | undefined; + /** Target timeZone override */ + timeZone?: string; + /** Target calendar override */ + calendar?: string; + /** Target locale override */ + locale?: string | string[]; + /** Target sphere override */ + sphere?: string; + /** If true, bypasses cache and native parsing to force an LLM fetch */ + force?: boolean; + /** If false, disables reading and writing to cache */ + cache?: boolean; + /** Optional custom cache adapter engine (e.g. Redis, KV store) for this request */ + cacheAdapter?: AiCacheAdapter; + /** Optional TTL override in milliseconds for cached result */ + ttl?: number; + /** If true, logs prompt context and LLM payloads to console */ + debug?: boolean; + /** Execution mode across provider farm (`AiMode.Fallback` | `AiMode.Race` | `AiMode.Consensus` or string literal) */ + mode?: AiMode; + /** Per-request provider configuration overrides */ + providers?: AiProvider[]; + /** Strict minimum confidence threshold (0.0 to 1.0). Throws TempoAiError(422) if score is lower */ + minConfidence?: number; + /** If true, places TempoAiError into array index position instead of rejecting batch */ + softErrors?: boolean; + /** Optional request timeout in milliseconds (overrides provider and global timeout) */ + timeout?: number; + /** Optional delay in milliseconds before initiating speculative hedging in AiMode.Hedged (default: 800ms) */ + hedgeDelay?: number; + /** Allow extra options */ + [key: string]: any; +} diff --git a/packages/plugins/ai/src/types/recurrence.type.ts b/packages/plugins/ai/src/types/recurrence.type.ts new file mode 100644 index 00000000..cb9dc5c5 --- /dev/null +++ b/packages/plugins/ai/src/types/recurrence.type.ts @@ -0,0 +1,42 @@ +import type { Tempo } from '@magmacomputing/tempo'; +import type { AiParseOptions } from './parse.type.js'; + +/** + * ## TempoRecurrenceOptions + * Options passed to `recurrenceAI(input, options)`. + */ +export interface TempoRecurrenceOptions extends AiParseOptions { + /** Start date/time window for occurrence expansion */ + after?: Tempo | Date | string | number | undefined; + /** End date/time window for occurrence expansion */ + before?: Tempo | Date | string | number | undefined; + /** Number of occurrences to pull per batch (default: 5) */ + count?: number; + /** Preferred BCP 47 locale tag for summary output (e.g. 'en-US', 'fr-FR', 'es-ES') */ + locale?: string; +} + +/** + * ## TempoRecurrenceResult + * Structured multi-directional recurrence result returned by `recurrenceAI`. + */ +export interface TempoRecurrenceResult { + /** Standard RFC 5545 RRULE string (e.g. 'FREQ=WEEKLY;BYDAY=TU') */ + rrule: string; + /** Localized human-friendly summary of the schedule (e.g. 'Every Tuesday at 15:00') */ + summary: string; + /** True if schedule has an explicit end date or count limit; false if infinite */ + isFinite: boolean; + /** Total count of occurrences if finite, or Infinity (Number.POSITIVE_INFINITY) */ + size: number; + /** Advances cursor and returns the next batch of N Tempo instances (default: 5) */ + take(count?: number): Tempo[]; + /** Lazy generator yielding Tempo instances on demand */ + [Symbol.iterator](): Generator; + /** Confidence rating from 0.0 (unparseable) to 1.0 (certain) */ + confidence: number; + /** Provider ID responsible for processing or 'rrule-parser' for native RRULE inputs */ + provider: string; + /** Reasoning / explanation of the recurrence pattern */ + reasoning?: string | undefined; +} diff --git a/packages/plugins/ai/src/types/schedule.type.ts b/packages/plugins/ai/src/types/schedule.type.ts new file mode 100644 index 00000000..c45c730f --- /dev/null +++ b/packages/plugins/ai/src/types/schedule.type.ts @@ -0,0 +1,97 @@ +import type { Tempo, Interval } from '@magmacomputing/tempo'; +import type { DayKey } from '@magmacomputing/tempo/library'; +import type { TempoBaseAiMeta } from './common.type.js'; +import type { AiParseOptions } from './parse.type.js'; + +/** + * ## TempoWorkingHours + * Defines daily working hour boundaries and active weekdays for scheduling. + */ +export interface TempoWorkingHours { + /** Start time of working day in HH:mm format (default: '09:00') */ + start?: string; + /** End time of working day in HH:mm format (default: '17:00') */ + end?: string; + /** Active working weekdays (1 = Monday, ... 7 = Sunday; or tokens like 'MO', 'MON'; default: [1, 2, 3, 4, 5]) */ + days?: Array; + /** Target timeZone for working hours (defaults to anchor or options timeZone) */ + timeZone?: string; +} + +/** + * ## TempoInterval + * Continuous date-time interval boundary pair. + */ +export interface TempoInterval { + /** Start boundary as a Tempo instance */ + start: Tempo; + /** End boundary as a Tempo instance */ + end: Tempo; +} + +/** + * ## TempoScheduleOptions + * Options passed to `scheduleAI(prompt, options)`. + */ +export interface TempoScheduleOptions extends AiParseOptions { + /** Target slot duration in minutes (if not explicitly specified in prompt) */ + durationMinutes?: number; + /** Working hours configuration for slot resolution */ + workingHours?: TempoWorkingHours; + /** Existing booked events or busy intervals to avoid */ + events?: Array<{ start: any; end: any; title?: string }> | Array>; + /** Alias for events */ + intervals?: Array<{ start: any; end: any; title?: string }> | Array>; + /** Search window start constraint */ + after?: any; + /** Search window end constraint */ + before?: any; + /** Preferred slot positioning ('earliest' | 'latest' | 'morning' | 'afternoon' | string) */ + preference?: string; + /** Number of alternative slots to return if requesting multiple options */ + count?: number; +} + +/** + * ## TempoScheduleAiMeta + * Extended AI execution metadata attached to scheduling results and intervals. + */ +export interface TempoScheduleAiMeta extends TempoBaseAiMeta { + /** Whether this slot was bumped due to a conflict */ + readonly conflictBumped?: boolean | undefined; + /** The original un-bumped candidate slot if conflict bumping occurred */ + readonly originalSlot?: TempoInterval | Interval | undefined; +} + +/** + * ## TempoScheduleMeta + * Metadata overlay attached to the Interval instance by `wrapScheduleInterval`. + */ +export interface TempoScheduleMeta { + /** Target slot duration in minutes */ + durationMinutes: number; + /** Human-friendly summary of the scheduled slot */ + summary: string; + /** Reasoning / explanation of why this slot was selected */ + reasoning?: string | undefined; + /** Confidence rating from 0.0 (unparseable) to 1.0 (certain) */ + confidence: number; + /** Provider ID responsible for processing or 'native-scheduler' */ + provider: string; + /** Alternative backup intervals identified during scheduling */ + alternatives?: Interval[] | undefined; + /** Extended AI execution metadata */ + ai: TempoScheduleAiMeta; +} + +/** + * ## TempoScheduleResult + * Structured scheduling result returned by `scheduleAI`. + */ +export interface TempoScheduleResult extends Interval, TempoScheduleMeta { + /** Resolved start boundary as a Tempo instance */ + start: Tempo; + /** Resolved end boundary as a Tempo instance */ + end: Tempo; +} + diff --git a/packages/plugins/ai/test/cache.test.ts b/packages/plugins/ai/test/cache.test.ts new file mode 100644 index 00000000..0790fdb1 --- /dev/null +++ b/packages/plugins/ai/test/cache.test.ts @@ -0,0 +1,146 @@ +import { parseAI, initAI, clearAiCache, type AiCacheAdapter } from '../src/index.js'; +import { Tempo } from '@magmacomputing/tempo'; + +describe('Advanced Cache TTL & Async Storage Adapters', () => { + beforeEach(async () => { + vi.restoreAllMocks(); + Tempo.cache.clear(); + await initAI({ + providers: [{ id: 'groq', key: 'mock-test-key' }], + remoteConfigUrl: false, + }); + }); + + afterEach(() => { + vi.restoreAllMocks(); + Tempo.cache.clear(); + }); + + it('should read from and write to custom async AiCacheAdapter', async () => { + const store = new Map(); + const ttlLogs: number[] = []; + + const mockAdapter: AiCacheAdapter = { + get: vi.fn(async (key: string) => store.get(key)), + set: vi.fn(async (key: string, value: string, ttlMs?: number) => { + store.set(key, value); + if (ttlMs) ttlLogs.push(ttlMs); + }), + delete: vi.fn(async (key: string) => { + store.delete(key); + }), + clear: vi.fn(async () => { + store.clear(); + }), + } + + await initAI({ + remoteConfigUrl: false, + providers: [{ id: 'groq', key: 'mock-test-key' }], + cacheAdapter: mockAdapter, + ttl: 120000, + }); + + // Mock LLM fetch response for first call + vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce( + new Response(JSON.stringify({ + choices: [{ message: { content: '{"reasoning":"Custom adapter test", "iso":"2026-11-26T00:00:00", "confidence":0.95}' } }], + }), { status: 200 }), + ); + + const result1 = await parseAI('Thanksgiving 2026'); + expect(result1.isValid).toBe(true); + expect(result1.ai?.provider).toBe('groq'); + expect(mockAdapter.set).toHaveBeenCalled(); + expect(ttlLogs).toContain(120000); + + // Second call should hit the custom adapter + const result2 = await parseAI('Thanksgiving 2026'); + expect(result2.isValid).toBe(true); + expect(result2.ai?.provider).toBe('cache'); + expect(result2.ai?.cached).toBe(true); + expect(mockAdapter.get).toHaveBeenCalled(); + }); + + it('should observe TTL resolution hierarchy (options.ttl > provider.ttl > global config.ttl)', async () => { + const setTtlLogs: number[] = []; + + const mockAdapter: AiCacheAdapter = { + get: vi.fn(() => undefined), + set: vi.fn((_key: string, _val: string, ttlMs?: number) => { + if (ttlMs) setTtlLogs.push(ttlMs); + }), + }; + + await initAI({ + remoteConfigUrl: false, + providers: [{ id: 'groq', key: 'mock-key', ttl: 60000 }], + cacheAdapter: mockAdapter, + ttl: 300000, + }); + + vi.spyOn(globalThis, 'fetch').mockImplementation(() => Promise.resolve( + new Response(JSON.stringify({ + choices: [{ message: { content: '{"reasoning":"TTL test", "iso":"2026-12-25T00:00:00", "confidence":0.95}' } }], + }), { status: 200 }), + )); + + // Call 1: Inherits provider.ttl (60000) + await parseAI('Christmas 2026'); + expect(setTtlLogs[0]).toBe(60000); + + // Call 2: Call-site options.ttl (15000) overrides provider and global TTL + await parseAI('Christmas 2026', { force: true, ttl: 15000 }); + expect(setTtlLogs[1]).toBe(15000); + }); + + it('should fail-open and fetch from LLM if custom cacheAdapter throws a network error', async () => { + const faultyAdapter: AiCacheAdapter = { + get: vi.fn(async () => { + throw new Error('Redis connection refused'); + }), + set: vi.fn(async () => { + throw new Error('Redis write error'); + }), + }; + + await initAI({ + remoteConfigUrl: false, + providers: [{ id: 'groq', key: 'mock-key' }], + cacheAdapter: faultyAdapter, + }); + + vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce( + new Response(JSON.stringify({ + choices: [{ message: { content: '{"reasoning":"Faulty adapter test", "iso":"2026-07-04T00:00:00", "confidence":0.95}' } }], + }), { status: 200 }), + ); + + // parseAI should NOT throw Redis error; it should fail open to LLM fetch + const result = await parseAI('Independence Day 2026'); + expect(result.isValid).toBe(true); + expect(result.ai?.provider).toBe('groq'); + }); + + it('should clear custom cacheAdapter entries when clearAiCache is invoked', async () => { + const mockAdapter: AiCacheAdapter = { + get: vi.fn(), + set: vi.fn(), + delete: vi.fn(), + clear: vi.fn(), + }; + + await initAI({ + remoteConfigUrl: false, + cacheAdapter: mockAdapter, + }); + + await clearAiCache('Easter 2026'); + expect(mockAdapter.delete).toHaveBeenCalledWith('easter 2026'); + expect(mockAdapter.delete).toHaveBeenCalledWith('Easter 2026'); + expect(mockAdapter.clear).toHaveBeenCalledWith('easter 2026::'); + + await clearAiCache(); + expect(mockAdapter.clear).toHaveBeenCalledTimes(2); + }); +}); diff --git a/packages/plugins/ai/test/context.test.ts b/packages/plugins/ai/test/context.test.ts new file mode 100644 index 00000000..da4a438f --- /dev/null +++ b/packages/plugins/ai/test/context.test.ts @@ -0,0 +1,296 @@ +import { Tempo } from '@magmacomputing/tempo'; +import { contextAI, initAI } from '../src/index.js'; + +describe('AI Context Plugin (contextAI)', () => { + beforeEach(async () => { + vi.spyOn(console, 'warn').mockImplementation(() => { }); + vi.spyOn(console, 'error').mockImplementation(() => { }); + vi.spyOn(console, 'log').mockImplementation(() => { }); + await initAI({ remoteConfigUrl: false, providers: [{ id: 'groq', key: 'mock-key-for-unit-testing' }] }); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('should infer timezone, locale, calendar, and hemisphere from descriptive text', async () => { + const fetchSpy = vi.spyOn(globalThis, 'fetch'); + fetchSpy.mockResolvedValueOnce(new Response(JSON.stringify({ + choices: [{ + message: { + content: JSON.stringify({ + timeZone: 'Asia/Tokyo', + locale: 'ja-JP', + calendar: 'gregory', + sphere: 'north', + reasoning: 'Kyoto is in Japan (JST timezone, ja-JP locale, northern hemisphere).', + confidence: 0.98, + }), + }, + }], + }), { status: 200, headers: { 'Content-Type': 'application/json' } })); + + const result = await contextAI('I am a freelancer working out of Kyoto, Japan.'); + expect(result).toBeDefined(); + expect(result.timeZone).toBe('Asia/Tokyo'); + expect(result.locale).toBe('ja-JP'); + expect(result.calendar).toBe('gregory'); + expect(result.sphere).toBe('north'); + expect(result.confidence).toBe(0.98); + expect(result.provider).toBe('groq'); + + expect(fetchSpy).toHaveBeenCalledTimes(1); + const requestBody = JSON.parse(fetchSpy.mock.calls[0][1]?.body as string); + const systemPrompt = requestBody.messages[0].content; + expect(systemPrompt).toContain('You are a locale and timezone inference engine.'); + expect(systemPrompt).toContain('"sphere": "Hemisphere'); + }); + + it('should omit sphere (set to undefined) if it is unknowable or null', async () => { + const fetchSpy = vi.spyOn(globalThis, 'fetch'); + fetchSpy.mockResolvedValueOnce(new Response(JSON.stringify({ + choices: [{ + message: { + content: JSON.stringify({ + timeZone: 'UTC', + locale: 'en-US', + calendar: 'gregory', + sphere: null, + reasoning: 'Cannot determine hemisphere.', + confidence: 0.7, + }), + }, + }], + }), { status: 200, headers: { 'Content-Type': 'application/json' } })); + + const result = await contextAI('Meeting online somewhere.'); + expect(result.sphere).toBeUndefined(); + expect(result.timeZone).toBe('UTC'); + }); + + it('should check cache and skip network fetch on cache hits', async () => { + const fetchSpy = vi.spyOn(globalThis, 'fetch'); + + // Seed cache + const tz = String(Tempo.options.timeZone); + const cal = String(Tempo.options.calendar); + const loc = String(Array.isArray(Tempo.options.locale) ? Tempo.options.locale[0] : Tempo.options.locale); + const sph = String(Tempo.options.sphere || 'north'); + Tempo.cache.set(`context::cached prompt::${tz}::${loc}::${cal}::${sph}`, JSON.stringify({ + timeZone: 'Europe/London', + locale: 'en-GB', + calendar: 'gregory', + sphere: 'north', + confidence: 0.95, + reasoning: 'Pre-cached location', + })); + + const result = await contextAI('cached prompt'); + expect(result.timeZone).toBe('Europe/London'); + expect(result.locale).toBe('en-GB'); + expect(result.sphere).toBe('north'); + expect(result.provider).toBe('cache'); + expect(fetchSpy).not.toHaveBeenCalled(); + }); + + it('should throw TempoAiError if resolved confidence is below minConfidence', async () => { + const fetchSpy = vi.spyOn(globalThis, 'fetch'); + fetchSpy.mockResolvedValueOnce(new Response(JSON.stringify({ + choices: [{ + message: { + content: JSON.stringify({ + timeZone: 'Europe/Paris', + locale: 'fr-FR', + calendar: 'gregory', + sphere: 'north', + confidence: 0.5, + }), + }, + }], + }), { status: 200, headers: { 'Content-Type': 'application/json' } })); + + await expect(contextAI('Ambiguous query', { minConfidence: 0.8 })) + .rejects.toThrow(/inferred context confidence \(0.5\) is below the required threshold of 0.8/i); + }); + + it('should throw TempoAiError if provider returns an invalid IANA timezone name', async () => { + const fetchSpy = vi.spyOn(globalThis, 'fetch'); + fetchSpy.mockResolvedValueOnce(new Response(JSON.stringify({ + choices: [{ + message: { + content: JSON.stringify({ + timeZone: 'Fake/TimeZone_Name', + locale: 'en-US', + calendar: 'gregory', + sphere: 'north', + confidence: 0.9, + }), + }, + }], + }), { status: 200, headers: { 'Content-Type': 'application/json' } })); + + await expect(contextAI('Query with invalid timezone response')) + .rejects.toThrow(/invalid or unsupported IANA timeZone/i); + }); + + it('should support provider race execution mode', async () => { + let slowWasAborted = false; + const fetchSpy = vi.spyOn(globalThis, 'fetch'); + fetchSpy.mockImplementation(async (_url, init) => { + const body = JSON.parse(init?.body as string); + const signal = init?.signal as AbortSignal | undefined; + if (body.model === 'fast-model') { + return new Response(JSON.stringify({ + choices: [{ + message: { + content: JSON.stringify({ + timeZone: 'Europe/London', + locale: 'en-GB', + calendar: 'gregory', + sphere: 'north', + confidence: 0.95, + }), + }, + }], + }), { status: 200, headers: { 'Content-Type': 'application/json' } }); + } + return new Promise((_resolve, reject) => { + if (signal?.aborted) { + slowWasAborted = true; + reject(new DOMException('Aborted', 'AbortError')); + return; + } + signal?.addEventListener('abort', () => { + slowWasAborted = true; + reject(new DOMException('Aborted', 'AbortError')); + }); + }); + }); + + const result = await contextAI('London office', { + mode: 'race', + providers: [ + { id: 'slow-provider', key: 'key-1', url: 'https://api.openai.com/v1/chat/completions', model: 'slow-model' }, + { id: 'fast-provider', key: 'key-2', url: 'https://api.groq.com/v1/chat/completions', model: 'fast-model' }, + ], + }); + + expect(result.timeZone).toBe('Europe/London'); + expect(result.provider).toBe('fast-provider'); + expect(slowWasAborted).toBe(true); + }); + + it('should support consensus mode selecting matching values', async () => { + const fetchSpy = vi.spyOn(globalThis, 'fetch'); + fetchSpy + .mockResolvedValueOnce(new Response(JSON.stringify({ + choices: [{ + message: { + content: JSON.stringify({ + timeZone: 'Australia/Sydney', + locale: 'en-AU', + calendar: 'gregory', + sphere: 'south', + confidence: 0.8, + }), + }, + }], + }), { status: 200, headers: { 'Content-Type': 'application/json' } })) + .mockResolvedValueOnce(new Response(JSON.stringify({ + choices: [{ + message: { + content: JSON.stringify({ + timeZone: 'Australia/Sydney', + locale: 'en-AU', + calendar: 'gregory', + sphere: 'south', + confidence: 0.92, + }), + }, + }], + }), { status: 200, headers: { 'Content-Type': 'application/json' } })); + + const result = await contextAI('Sydney Harbour', { + mode: 'consensus', + providers: [ + { id: 'p1', key: 'key-1', url: 'https://api.groq.com/v1/chat/completions', model: 'm1' }, + { id: 'p2', key: 'key-2', url: 'https://api.openai.com/v1/chat/completions', model: 'm2' }, + ], + }); + + expect(result.timeZone).toBe('Australia/Sydney'); + expect(result.sphere).toBe('south'); + expect(result.confidence).toBe(1.0); // Consensus vote elevates confidence to 1.0 + }); + + it('should treat cached context with confidence below minConfidence as a cache miss', async () => { + const fetchSpy = vi.spyOn(globalThis, 'fetch'); + const tz = String(Tempo.options.timeZone); + const cal = String(Tempo.options.calendar); + const loc = String(Array.isArray(Tempo.options.locale) ? Tempo.options.locale[0] : Tempo.options.locale); + const sph = String(Tempo.options.sphere || 'north'); + + Tempo.cache.set(`context::low confidence::${tz}::${loc}::${cal}::${sph}`, JSON.stringify({ + timeZone: 'Europe/Berlin', + locale: 'de-DE', + calendar: 'gregory', + sphere: 'north', + confidence: 0.6, + })); + + fetchSpy.mockResolvedValueOnce(new Response(JSON.stringify({ + choices: [{ + message: { + content: JSON.stringify({ + timeZone: 'Europe/Berlin', + locale: 'de-DE', + calendar: 'gregory', + sphere: 'north', + confidence: 0.95, + }), + }, + }], + }), { status: 200, headers: { 'Content-Type': 'application/json' } })); + + const result = await contextAI('low confidence', { minConfidence: 0.8 }); + expect(fetchSpy).toHaveBeenCalledTimes(1); + expect(result.confidence).toBe(0.95); + expect(result.provider).toBe('groq'); + }); + + it('should reject candidates when provider returns out-of-range confidence', async () => { + const fetchSpy = vi.spyOn(globalThis, 'fetch'); + fetchSpy.mockResolvedValueOnce(new Response(JSON.stringify({ + choices: [{ + message: { + content: JSON.stringify({ + timeZone: 'Europe/Berlin', + locale: 'de-DE', + calendar: 'gregory', + confidence: 1.5, + }), + }, + }], + }), { status: 200, headers: { 'Content-Type': 'application/json' } })); + + await expect(contextAI('Berlin tech hub')).rejects.toThrow(/invalid confidence score/i); + }); + + it('should reject candidates when provider returns non-finite confidence score', async () => { + const fetchSpy = vi.spyOn(globalThis, 'fetch'); + fetchSpy.mockResolvedValueOnce(new Response(JSON.stringify({ + choices: [{ + message: { + content: JSON.stringify({ + timeZone: 'Europe/Berlin', + locale: 'de-DE', + calendar: 'gregory', + confidence: null, + }), + }, + }], + }), { status: 200, headers: { 'Content-Type': 'application/json' } })); + + await expect(contextAI('Berlin tech hub')).rejects.toThrow(/invalid confidence score/i); + }); +}); diff --git a/packages/plugins/ai/test/diff.test.ts b/packages/plugins/ai/test/diff.test.ts new file mode 100644 index 00000000..f71cad33 --- /dev/null +++ b/packages/plugins/ai/test/diff.test.ts @@ -0,0 +1,305 @@ +import { Tempo } from '@magmacomputing/tempo'; +import { diffAI, initAI, TempoAiError, type TempoAiDiffResult } from '../src/index.js'; + +describe('AI Diff Plugin (diffAI)', () => { + beforeEach(async () => { + vi.spyOn(console, 'warn').mockImplementation(() => { }); + vi.spyOn(console, 'error').mockImplementation(() => { }); + vi.spyOn(console, 'log').mockImplementation(() => { }); + await initAI({ remoteConfigUrl: false, providers: [{ id: 'groq', key: 'mock-key-for-unit-testing' }] }); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('should calculate native grounding metrics and format natural narrative diff', async () => { + const fetchSpy = vi.spyOn(globalThis, 'fetch'); + fetchSpy.mockResolvedValueOnce(new Response(JSON.stringify({ + choices: [{ + message: { + content: JSON.stringify({ + formatted: '5 business days (approx. 224 hours)', + days: 9.33, + hours: 224, + businessDays: 5, + reasoning: 'Spans one full working week (5 business days) across 10 calendar days.', + confidence: 0.96, + }), + }, + }], + }), { status: 200, headers: { 'Content-Type': 'application/json' } })); + + const start = '2026-08-01T09:00:00Z'; // Saturday + const end = '2026-08-10T17:00:00Z'; // Next Monday + + const result = await diffAI(start, end, 'explain in terms of business working days'); + expect(result).toBeDefined(); + expect(result.formatted).toBe('5 business days (approx. 224 hours)'); + expect(result.businessDays).toBe(5); + expect(result.hours).toBe(224); + expect(result.confidence).toBe(0.96); + expect(result.provider).toBe('groq'); + + expect(fetchSpy).toHaveBeenCalledTimes(1); + const requestBody = JSON.parse(fetchSpy.mock.calls[0][1]?.body as string); + const systemPrompt = requestBody.messages[0].content; + expect(systemPrompt).toContain('You are an expert temporal difference'); + expect(systemPrompt).toContain('"businessDays": 5'); + expect(systemPrompt).toContain('Grounding Context:'); + }); + + it('should exclude matching holidays from business day calculations', async () => { + const fetchSpy = vi.spyOn(globalThis, 'fetch'); + fetchSpy.mockResolvedValueOnce(new Response(JSON.stringify({ + choices: [{ + message: { + content: JSON.stringify({ + formatted: '3 business days (excluding Bank Holiday on Aug 4 & 5)', + days: 9.33, + hours: 224, + businessDays: 3, + reasoning: 'Excluded 2 weekend days and 2 public holidays.', + confidence: 0.98, + }), + }, + }], + }), { status: 200, headers: { 'Content-Type': 'application/json' } })); + + const start = new Tempo('2026-08-01T09:00:00Z'); + const end = new Tempo('2026-08-10T17:00:00Z'); + + const result = await diffAI(start, end, 'calculate working days minus regional holidays', { + holidays: ['2026-08-04', '2026-08-05'], + region: 'AU-NSW', + }); + + expect(result.businessDays).toBe(3); + expect(result.holidays).toEqual(['2026-08-04', '2026-08-05']); + + const requestBody = JSON.parse(fetchSpy.mock.calls[0][1]?.body as string); + const contextPrompt = requestBody.messages[0].content; + expect(contextPrompt).toContain('Computed Business Working Days: 3'); + expect(contextPrompt).toContain('Matching Excluded Holidays: 2026-08-04, 2026-08-05'); + expect(contextPrompt).toContain('Region Context: AU-NSW'); + }); + + it('should support reverse date intervals with negative business days', async () => { + const fetchSpy = vi.spyOn(globalThis, 'fetch'); + fetchSpy.mockResolvedValueOnce(new Response(JSON.stringify({ + choices: [{ + message: { + content: JSON.stringify({ + formatted: '5 business days ago', + days: -9.33, + hours: -224, + businessDays: -5, + reasoning: 'End date occurred 5 working days before start date.', + confidence: 0.94, + }), + }, + }], + }), { status: 200, headers: { 'Content-Type': 'application/json' } })); + + const start = '2026-08-10T17:00:00Z'; + const end = '2026-08-01T09:00:00Z'; + + const result = await diffAI(start, end, 'relative past difference'); + expect(result.businessDays).toBe(-5); + expect(result.formatted).toBe('5 business days ago'); + + const requestBody = JSON.parse(fetchSpy.mock.calls[0][1]?.body as string); + const contextPrompt = requestBody.messages[0].content; + expect(contextPrompt).toContain('Direction: Past/Backward'); + expect(contextPrompt).toContain('Computed Business Working Days: -5'); + }); + + it('should check cache and skip network fetch on cache hits', async () => { + const fetchSpy = vi.spyOn(globalThis, 'fetch'); + fetchSpy.mockResolvedValueOnce(new Response(JSON.stringify({ + choices: [{ + message: { + content: JSON.stringify({ + formatted: 'Pre-cached 5 business days', + days: 9.33, + hours: 224, + businessDays: 5, + reasoning: 'Calculated and formatted', + confidence: 0.95, + }), + }, + }], + }), { status: 200, headers: { 'Content-Type': 'application/json' } })); + + const start = new Tempo('2026-08-01T09:00:00Z'); + const end = new Tempo('2026-08-10T17:00:00Z'); + + const result1 = await diffAI(start, end, 'cached prompt'); + expect(result1.provider).toBe('groq'); + expect(fetchSpy).toHaveBeenCalledTimes(1); + + const result2 = await diffAI(start, end, 'cached prompt'); + expect(result2.formatted).toBe('Pre-cached 5 business days'); + expect(result2.provider).toBe('cache'); + expect(result2.confidence).toBe(0.95); + expect(fetchSpy).toHaveBeenCalledTimes(1); + }); + + it('should throw TempoAiError if confidence is below minConfidence', async () => { + const fetchSpy = vi.spyOn(globalThis, 'fetch'); + fetchSpy.mockResolvedValueOnce(new Response(JSON.stringify({ + choices: [{ + message: { + content: JSON.stringify({ + formatted: 'Uncertain difference', + days: 5, + hours: 120, + businessDays: 3, + confidence: 0.45, + }), + }, + }], + }), { status: 200, headers: { 'Content-Type': 'application/json' } })); + + await expect(diffAI('2026-08-01', '2026-08-06', 'test', { minConfidence: 0.8 })) + .rejects.toThrow(/diffAI confidence \(0.45\) is below the required threshold of 0.8/i); + }); + + it('should support multi-provider race execution mode', async () => { + let slowWasAborted = false; + const fetchSpy = vi.spyOn(globalThis, 'fetch'); + fetchSpy.mockImplementation(async (_url, init) => { + const body = JSON.parse(init?.body as string); + const signal = init?.signal as AbortSignal | undefined; + if (body.model === 'fast-model') { + return new Response(JSON.stringify({ + choices: [{ + message: { + content: JSON.stringify({ + formatted: 'Fast winner formatted diff', + days: 4, + hours: 96, + businessDays: 4, + confidence: 0.95, + }), + }, + }], + }), { status: 200, headers: { 'Content-Type': 'application/json' } }); + } + return new Promise((_resolve, reject) => { + if (signal?.aborted) { + slowWasAborted = true; + reject(new DOMException('Aborted', 'AbortError')); + return; + } + signal?.addEventListener('abort', () => { + slowWasAborted = true; + reject(new DOMException('Aborted', 'AbortError')); + }); + }); + }); + + const result = await diffAI('2026-08-03', '2026-08-07', 'quick race diff', { + mode: 'race', + providers: [ + { id: 'slow-provider', key: 'k1', url: 'https://api.openai.com/v1/chat/completions', model: 'slow-model' }, + { id: 'fast-provider', key: 'k2', url: 'https://api.groq.com/v1/chat/completions', model: 'fast-model' }, + ], + }); + + expect(result.formatted).toBe('Fast winner formatted diff'); + expect(result.provider).toBe('fast-provider'); + expect(slowWasAborted).toBe(true); + }); + + it('should support batch array processing with softErrors', async () => { + const fetchSpy = vi.spyOn(globalThis, 'fetch'); + fetchSpy + .mockResolvedValueOnce(new Response(JSON.stringify({ + choices: [{ + message: { + content: JSON.stringify({ + formatted: 'Pair 1 resolved', + days: 2, + hours: 48, + businessDays: 2, + confidence: 0.95, + }), + }, + }], + }), { status: 200, headers: { 'Content-Type': 'application/json' } })) + .mockResolvedValueOnce(new Response('Server Error', { status: 500 })); + + const pairs = [ + { start: '2026-08-03', end: '2026-08-05', prompt: 'pair 1' }, + { start: '2026-08-05', end: '2026-08-07', prompt: 'pair 2' }, + ]; + + const results = await diffAI(pairs, { softErrors: true }); + expect(results).toHaveLength(2); + expect((results[0] as TempoAiDiffResult).formatted).toBe('Pair 1 resolved'); + expect(results[1]).toBeInstanceOf(TempoAiError); + }); + + it('should produce distinct cache keys for different region options', async () => { + const fetchSpy = vi.spyOn(globalThis, 'fetch'); + fetchSpy + .mockResolvedValueOnce(new Response(JSON.stringify({ + choices: [{ + message: { + content: JSON.stringify({ + formatted: 'AU Region difference', + reasoning: 'AU formatting', + confidence: 0.95, + }), + }, + }], + }), { status: 200, headers: { 'Content-Type': 'application/json' } })) + .mockResolvedValueOnce(new Response(JSON.stringify({ + choices: [{ + message: { + content: JSON.stringify({ + formatted: 'US Region difference', + reasoning: 'US formatting', + confidence: 0.95, + }), + }, + }], + }), { status: 200, headers: { 'Content-Type': 'application/json' } })); + + const start = new Tempo('2026-08-03T09:00:00Z'); + const end = new Tempo('2026-08-07T17:00:00Z'); + + const resultAU = await diffAI(start, end, 'summarize', { region: 'AU-NSW' }); + const resultUS = await diffAI(start, end, 'summarize', { region: 'US' }); + + expect(resultAU.formatted).toBe('AU Region difference'); + expect(resultUS.formatted).toBe('US Region difference'); + expect(fetchSpy).toHaveBeenCalledTimes(2); + }); + + it('should normalize start and end Tempo instances to requested timezone', async () => { + const fetchSpy = vi.spyOn(globalThis, 'fetch'); + fetchSpy.mockResolvedValueOnce(new Response(JSON.stringify({ + choices: [{ + message: { + content: JSON.stringify({ + formatted: 'Timezone normalized difference', + reasoning: 'Normalized', + confidence: 0.95, + }), + }, + }], + }), { status: 200, headers: { 'Content-Type': 'application/json' } })); + + const start = new Tempo('2026-08-01T09:00:00', { timeZone: 'America/New_York' }); + const end = new Tempo('2026-08-05T17:00:00', { timeZone: 'Asia/Tokyo' }); + + const result = await diffAI(start, end, 'summarize', { timeZone: 'Europe/London' }); + expect(result.formatted).toBe('Timezone normalized difference'); + expect(fetchSpy).toHaveBeenCalledTimes(1); + + const requestBody = JSON.parse(fetchSpy.mock.calls[0][1]?.body as string); + expect(requestBody.messages[0].content).toContain('(Europe/London)'); + }); +}); diff --git a/packages/plugins/ai/test/dispatch.test.ts b/packages/plugins/ai/test/dispatch.test.ts new file mode 100644 index 00000000..5df9f94b --- /dev/null +++ b/packages/plugins/ai/test/dispatch.test.ts @@ -0,0 +1,382 @@ +import { executeWithMode } from '../src/core/dispatch.js'; +import { AiMode } from '../src/core/config.js'; +import { TempoAiError } from '../src/core/error.js'; +import { _state, resetAI } from '../src/core/init.js'; +import { Tempo } from '@magmacomputing/tempo'; +import type { AiProvider } from '../src/types/index.js'; + +describe('AI Dispatch Helper (executeWithMode)', () => { + beforeEach(() => { + resetAI(); + }); + + const mockProviders: AiProvider[] = [ + { id: 'provider-a', key: 'key-a' }, + { id: 'provider-b', key: 'key-b' }, + { id: 'provider-c', key: 'key-c' }, + ]; + + describe('AiMode.Fallback', () => { + it('should execute Fallback mode and stop on first provider meeting minConfidence', async () => { + const task = vi.fn() + .mockResolvedValueOnce({ data: { val: 1 }, providerId: 'provider-a', confidence: 0.7 }) + .mockResolvedValueOnce({ data: { val: 2 }, providerId: 'provider-b', confidence: 0.95 }); + + const winner = await executeWithMode( + AiMode.Fallback, + mockProviders, + task, + { minConfidence: 0.8 }, + ); + + expect(task).toHaveBeenCalledTimes(2); + expect(winner.providerId).toBe('provider-b'); + expect(winner.data).toEqual({ val: 2 }); + }); + + it('should return best candidate in Fallback mode if none meet minConfidence', async () => { + const task = vi.fn() + .mockResolvedValueOnce({ data: { val: 1 }, providerId: 'provider-a', confidence: 0.6 }) + .mockResolvedValueOnce({ data: { val: 2 }, providerId: 'provider-b', confidence: 0.75 }) + .mockResolvedValueOnce({ data: { val: 3 }, providerId: 'provider-c', confidence: 0.70 }); + + const winner = await executeWithMode( + AiMode.Fallback, + mockProviders, + task, + { minConfidence: 0.9 }, + ); + + expect(task).toHaveBeenCalledTimes(3); + expect(winner.providerId).toBe('provider-b'); + expect(winner.data).toEqual({ val: 2 }); + }); + }); + + describe('AiMode.Race', () => { + it('should execute Race mode and return fastest provider result', async () => { + const task = vi.fn().mockImplementation(async (provider: AiProvider) => { + if (provider.id === 'provider-a') { + await new Promise(resolve => setTimeout(resolve, 50)); + return { data: { fast: false }, providerId: 'provider-a', confidence: 0.8 }; + } + return { data: { fast: true }, providerId: 'provider-b', confidence: 0.9 }; + }); + + const winner = await executeWithMode( + AiMode.Race, + mockProviders.slice(0, 2), + task, + ); + + expect(winner.providerId).toBe('provider-b'); + expect(winner.data).toEqual({ fast: true }); + }); + + it('should keep race active and prefer slower qualifying candidate over faster low-confidence candidate', async () => { + const task = vi.fn().mockImplementation(async (provider: AiProvider) => { + if (provider.id === 'provider-a') { + await new Promise(resolve => setTimeout(resolve, 10)); + return { data: { val: 'fast-low' }, providerId: 'provider-a', confidence: 0.7 }; + } + if (provider.id === 'provider-b') { + await new Promise(resolve => setTimeout(resolve, 50)); + return { data: { val: 'slow-high' }, providerId: 'provider-b', confidence: 0.95 }; + } + await new Promise(resolve => setTimeout(resolve, 100)); + return { data: { val: 'slowest' }, providerId: 'provider-c', confidence: 0.8 }; + }); + + const winner = await executeWithMode( + AiMode.Race, + mockProviders, + task, + { minConfidence: 0.9 }, + ); + + expect(winner.providerId).toBe('provider-b'); + expect(winner.data).toEqual({ val: 'slow-high' }); + }); + + it('should return best candidate if all race candidates fall below minConfidence', async () => { + const task = vi.fn().mockImplementation(async (provider: AiProvider) => { + if (provider.id === 'provider-a') { + await new Promise(resolve => setTimeout(resolve, 10)); + return { data: { val: 'fast' }, providerId: 'provider-a', confidence: 0.7 }; + } + if (provider.id === 'provider-b') { + await new Promise(resolve => setTimeout(resolve, 30)); + return { data: { val: 'slow' }, providerId: 'provider-b', confidence: 0.8 }; + } + await new Promise(resolve => setTimeout(resolve, 50)); + return { data: { val: 'slowest' }, providerId: 'provider-c', confidence: 0.6 }; + }); + + const winner = await executeWithMode( + AiMode.Race, + mockProviders, + task, + { minConfidence: 0.9 }, + ); + + expect(winner.providerId).toBe('provider-b'); + expect(winner.data).toEqual({ val: 'slow' }); + }); + }); + + describe('AiMode.Consensus', () => { + it('should execute Consensus mode and mark consensus when keys match', async () => { + const task = vi.fn() + .mockResolvedValueOnce({ data: { rrule: 'FREQ=DAILY' }, providerId: 'provider-a', consensusKey: 'FREQ=DAILY', confidence: 0.9 }) + .mockResolvedValueOnce({ data: { rrule: 'FREQ=DAILY' }, providerId: 'provider-b', consensusKey: 'FREQ=DAILY', confidence: 0.9 }); + + const winner = await executeWithMode( + AiMode.Consensus, + mockProviders.slice(0, 2), + task, + ); + + expect(winner.providerId).toBe(AiMode.Consensus); + expect(winner.confidence).toBe(1.0); + expect(winner.ambiguous).toBe(false); + }); + + it('should sort by confidence in Consensus mode when keys differ', async () => { + const task = vi.fn() + .mockResolvedValueOnce({ data: { rrule: 'FREQ=DAILY' }, providerId: 'provider-a', consensusKey: 'FREQ=DAILY', confidence: 0.8 }) + .mockResolvedValueOnce({ data: { rrule: 'FREQ=WEEKLY' }, providerId: 'provider-b', consensusKey: 'FREQ=WEEKLY', confidence: 0.95 }); + + const winner = await executeWithMode( + AiMode.Consensus, + mockProviders.slice(0, 2), + task, + ); + + expect(winner.providerId).toBe('provider-b'); + expect(winner.confidence).toBe(0.95); + expect(winner.ambiguous).toBe(true); + }); + }); + + describe('AiMode.Hedged', () => { + it('should only call primary provider when it finishes before hedgeDelay', async () => { + const task = vi.fn().mockImplementation(async (provider: AiProvider) => { + await new Promise(resolve => setTimeout(resolve, 20)); + return { data: { hedged: false }, providerId: provider.id, confidence: 0.95 }; + }); + + const winner = await executeWithMode( + AiMode.Hedged, + mockProviders, + task, + { hedgeDelay: 100 }, + ); + + expect(winner.providerId).toBe('provider-a'); + expect(winner.data).toEqual({ hedged: false }); + expect(task).toHaveBeenCalledTimes(1); + expect(task).toHaveBeenCalledWith(mockProviders[0], expect.any(AbortSignal)); + }); + + it('should launch speculative secondary provider when primary exceeds hedgeDelay', async () => { + const task = vi.fn().mockImplementation(async (provider: AiProvider, signal?: AbortSignal) => { + if (provider.id === 'provider-a') { + await new Promise((resolve, reject) => { + const timer = setTimeout(resolve, 200); + signal?.addEventListener('abort', () => { + clearTimeout(timer); + reject(new Error('Aborted')); + }); + }); + return { data: { winner: 'a' }, providerId: 'provider-a', confidence: 0.9 }; + } + if (provider.id === 'provider-b') { + await new Promise(resolve => setTimeout(resolve, 20)); + return { data: { winner: 'b' }, providerId: 'provider-b', confidence: 0.95 }; + } + return { data: { winner: 'c' }, providerId: 'provider-c', confidence: 0.5 }; + }); + + const winner = await executeWithMode( + AiMode.Hedged, + mockProviders, + task, + { hedgeDelay: 50 }, + ); + + expect(winner.providerId).toBe('provider-b'); + expect(winner.data).toEqual({ winner: 'b' }); + // Provider A and Provider B were initiated, Provider C was not needed + expect(task).toHaveBeenCalledTimes(2); + }); + + it('should handle primary failure and resolve with secondary candidate', async () => { + const task = vi.fn().mockImplementation(async (provider: AiProvider) => { + if (provider.id === 'provider-a') { + throw new TempoAiError('Provider A Network Error', 502); + } + await new Promise(resolve => setTimeout(resolve, 30)); + return { data: { winner: 'b' }, providerId: 'provider-b', confidence: 0.9 }; + }); + + const winner = await executeWithMode( + AiMode.Hedged, + mockProviders, + task, + { hedgeDelay: 20 }, + ); + + expect(winner.providerId).toBe('provider-b'); + expect(winner.data).toEqual({ winner: 'b' }); + }); + + it('should reject when all hedged providers fail', async () => { + const task = vi.fn().mockImplementation(async () => { + throw new TempoAiError('Provider unavailable', 503); + }); + + await expect( + executeWithMode(AiMode.Hedged, mockProviders.slice(0, 2), task, { hedgeDelay: 10 }), + ).rejects.toThrow(TempoAiError); + }); + }); + + describe('AiMode.RoundRobin', () => { + it('should rotate starting provider cyclically on successive invocations', async () => { + const calledProviders: string[] = []; + const task = vi.fn().mockImplementation(async (provider: AiProvider) => { + calledProviders.push(provider.id); + return { data: { provider: provider.id }, providerId: provider.id, confidence: 0.9 }; + }); + + // Call 1 -> should start with provider-a + const win1 = await executeWithMode(AiMode.RoundRobin, mockProviders, task); + // Call 2 -> should start with provider-b + const win2 = await executeWithMode(AiMode.RoundRobin, mockProviders, task); + // Call 3 -> should start with provider-c + const win3 = await executeWithMode(AiMode.RoundRobin, mockProviders, task); + // Call 4 -> should wrap around to provider-a + const win4 = await executeWithMode(AiMode.RoundRobin, mockProviders, task); + + expect(win1.providerId).toBe('provider-a'); + expect(win2.providerId).toBe('provider-b'); + expect(win3.providerId).toBe('provider-c'); + expect(win4.providerId).toBe('provider-a'); + expect(calledProviders).toEqual(['provider-a', 'provider-b', 'provider-c', 'provider-a']); + }); + + it('should cascade to remaining providers in cyclic order when rotated primary fails', async () => { + const task = vi.fn().mockImplementation(async (provider: AiProvider) => { + if (provider.id === 'provider-a') { + throw new TempoAiError('Provider A down', 500); + } + return { data: { success: true }, providerId: provider.id, confidence: 0.9 }; + }); + + // Provider A is starting provider on this call, fails and cascades to Provider B + const winner = await executeWithMode(AiMode.RoundRobin, mockProviders.slice(0, 2), task); + expect(winner.providerId).toBe('provider-b'); + }); + }); + + describe('AiMode.Adaptive', () => { + it('should prioritize providers with active quota over exhausted providers in cooldown', async () => { + // Simulate rate limit telemetry in state + const resetFuture = new Tempo().add('2 minutes'); + _state.providerLimits.set('provider-a', { + remainingRequests: 0, + remainingTokens: 0, + resetAt: resetFuture, + }); + _state.providerLimits.set('provider-b', { + remainingRequests: 250, + remainingTokens: 50000, + resetAt: null, + }); + + const callSequence: string[] = []; + const task = vi.fn().mockImplementation(async (provider: AiProvider) => { + callSequence.push(provider.id); + return { data: { id: provider.id }, providerId: provider.id, confidence: 0.9 }; + }); + + const winner = await executeWithMode(AiMode.Adaptive, mockProviders.slice(0, 2), task); + + // provider-b should be prioritized before provider-a because provider-a has remainingRequests: 0 + expect(callSequence[0]).toBe('provider-b'); + expect(winner.providerId).toBe('provider-b'); + }); + + it('should sort providers by highest remaining request quota descending', async () => { + _state.providerLimits.set('provider-a', { + remainingRequests: 50, + remainingTokens: 1000, + resetAt: null, + }); + _state.providerLimits.set('provider-b', { + remainingRequests: 400, + remainingTokens: 80000, + resetAt: null, + }); + _state.providerLimits.set('provider-c', { + remainingRequests: 150, + remainingTokens: 30000, + resetAt: null, + }); + + const callSequence: string[] = []; + const task = vi.fn().mockImplementation(async (provider: AiProvider) => { + callSequence.push(provider.id); + return { data: { id: provider.id }, providerId: provider.id, confidence: 0.9 }; + }); + + const winner = await executeWithMode(AiMode.Adaptive, mockProviders, task); + + // Expected order: provider-b (400) -> provider-c (150) -> provider-a (50) + expect(callSequence[0]).toBe('provider-b'); + expect(winner.providerId).toBe('provider-b'); + }); + + it('should always prefer local providers (no rate-limit telemetry) over constrained remote providers', async () => { + // provider-a is a local LLM (e.g. Ollama) — never sets rate-limit headers, + // so providerLimits has no entry for it → remaining = Infinity. + // provider-b is a remote API with plenty of quota remaining. + // provider-a should still be preferred because Infinity > any finite value. + _state.providerLimits.set('provider-b', { + remainingRequests: 4500, + remainingTokens: 900000, + resetAt: null, + }); + // provider-c is a remote API that is quota-exhausted + const resetFuture = new Tempo().add('5 minutes'); + _state.providerLimits.set('provider-c', { + remainingRequests: 0, + remainingTokens: 0, + resetAt: resetFuture, + }); + + const callSequence: string[] = []; + const task = vi.fn().mockImplementation(async (provider: AiProvider) => { + callSequence.push(provider.id); + return { data: { id: provider.id }, providerId: provider.id, confidence: 0.9 }; + }); + + const winner = await executeWithMode(AiMode.Adaptive, mockProviders, task); + + // Expected order: provider-a (Infinity, local) → provider-b (4500, remote) → provider-c (exhausted) + expect(callSequence[0]).toBe('provider-a'); + expect(winner.providerId).toBe('provider-a'); + }); + }); + + describe('Invalid Modes', () => { + it('should throw TempoAiError with status 400 for invalid mode', async () => { + const task = vi.fn(); + const promise = executeWithMode('unsupported' as any, mockProviders, task); + await expect(promise).rejects.toThrow(TempoAiError); + await expect(promise).rejects.toSatisfy((err: unknown) => { + return err instanceof TempoAiError && err.status === 400 && err.code === 400; + }); + }); + }); +}); diff --git a/packages/plugins/ai/test/manifest.test.ts b/packages/plugins/ai/test/manifest.test.ts new file mode 100644 index 00000000..c1970b26 --- /dev/null +++ b/packages/plugins/ai/test/manifest.test.ts @@ -0,0 +1,280 @@ +import { + initAI, + resetAI, + getAiConfig, + loadRemoteManifest, + DEFAULT_REMOTE_MANIFEST_URL, + DEFAULT_PROVIDERS +} from '../src/index.js'; + +describe('Remote Provider Manifest & Dynamic Defaults', () => { + beforeEach(() => { + resetAI(); + vi.restoreAllMocks(); + }); + + afterEach(() => { + resetAI(); + vi.restoreAllMocks(); + }); + + it('should fetch remote manifest and cache in memory for subsequent calls', async () => { + const mockManifest = { + version: '1.0', + providers: { + groq: { model: 'llama-3.3-70b-versatile', tokenParam: 'max_tokens' }, + openai: { model: 'gpt-5.4-mini', tokenParam: 'max_completion_tokens' } + } + }; + + const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce( + new Response(JSON.stringify(mockManifest), { status: 200 }) + ); + + const result1 = await loadRemoteManifest(DEFAULT_REMOTE_MANIFEST_URL); + expect(fetchSpy).toHaveBeenCalledTimes(1); + expect(fetchSpy).toHaveBeenCalledWith( + DEFAULT_REMOTE_MANIFEST_URL, + expect.objectContaining({ headers: { Accept: 'application/json' }, redirect: 'error' }) + ); + expect(result1).toEqual(mockManifest.providers); + + // Second call should return cached object without second fetch + const result2 = await loadRemoteManifest(DEFAULT_REMOTE_MANIFEST_URL); + expect(fetchSpy).toHaveBeenCalledTimes(1); + expect(result2).toEqual(mockManifest.providers); + }); + + it('should reject non-HTTPS/non-localhost remoteConfigUrl before fetching', async () => { + const fetchSpy = vi.spyOn(globalThis, 'fetch'); + const result = await loadRemoteManifest('http://insecure-domain.com/providers.json'); + expect(fetchSpy).not.toHaveBeenCalled(); + expect(result).toBeNull(); + }); + + it('should allow http://localhost and http://127.0.0.1 remoteConfigUrl', async () => { + const mockManifest = { + version: '1.0', + providers: { groq: { model: 'local-test-model' } } + }; + + const fetchSpy = vi.spyOn(globalThis, 'fetch') + .mockResolvedValueOnce(new Response(JSON.stringify(mockManifest), { status: 200 })) + .mockResolvedValueOnce(new Response(JSON.stringify(mockManifest), { status: 200 })); + + const result1 = await loadRemoteManifest('http://localhost:3000/providers.json'); + expect(result1).toEqual(mockManifest.providers); + + const result2 = await loadRemoteManifest('http://127.0.0.1:8080/providers.json'); + expect(result2).toEqual(mockManifest.providers); + expect(fetchSpy).toHaveBeenCalledTimes(2); + }); + + it('should gracefully fail-open on network error (500) and return null', async () => { + const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce( + new Response(null, { status: 500, statusText: 'Internal Server Error' }) + ); + + const result = await loadRemoteManifest(DEFAULT_REMOTE_MANIFEST_URL); + expect(fetchSpy).toHaveBeenCalledTimes(1); + expect(result).toBeNull(); + }); + + it('should gracefully fail-open on network timeout / abort', async () => { + const fetchSpy = vi.spyOn(globalThis, 'fetch').mockImplementationOnce(() => { + const err = new Error('The operation was aborted'); + err.name = 'AbortError'; + return Promise.reject(err); + }); + + const result = await loadRemoteManifest(DEFAULT_REMOTE_MANIFEST_URL, 100); + expect(fetchSpy).toHaveBeenCalledTimes(1); + expect(result).toBeNull(); + }); + + it('should return null immediately if remoteConfigUrl is false', async () => { + const fetchSpy = vi.spyOn(globalThis, 'fetch'); + const result = await loadRemoteManifest(false); + expect(fetchSpy).not.toHaveBeenCalled(); + expect(result).toBeNull(); + }); + + it('should fetch custom remoteConfigUrl when provided', async () => { + const customUrl = 'https://custom-domain.net/providers.json'; + const mockManifest = { + version: '1.0', + providers: { + groq: { model: 'custom-groq-model' } + } + }; + + const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce( + new Response(JSON.stringify(mockManifest), { status: 200 }) + ); + + const result = await loadRemoteManifest(customUrl); + expect(fetchSpy).toHaveBeenCalledWith(customUrl, expect.anything()); + expect(result).toEqual(mockManifest.providers); + }); + + it('should resolve provider defaults from remote manifest when available in initAI', async () => { + const mockManifest = { + version: '1.0', + providers: { + groq: { + url: 'https://api.groq.com/openai/v1/chat/completions', + model: 'remote-llama-model', + tokenParam: 'max_tokens' + } + } + }; + + vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce( + new Response(JSON.stringify(mockManifest), { status: 200 }) + ); + + await initAI({ + providers: [{ id: 'groq', key: 'test-key' }] + }); + + // Check resolved providers in init state via getAiConfig + const config = getAiConfig(); + expect(config.providers).toHaveLength(1); + expect(config.providers?.[0].model).toBe('remote-llama-model'); + }); + + it('should fallback to compiled DEFAULT_PROVIDERS if remote manifest is missing provider ID', async () => { + const mockManifest = { + version: '1.0', + providers: {} + }; + + vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce( + new Response(JSON.stringify(mockManifest), { status: 200 }) + ); + + await initAI({ + providers: [{ id: 'openai', key: 'test-key' }] + }); + + const config = getAiConfig(); + expect(config.providers?.[0].model).toBe(DEFAULT_PROVIDERS.openai.model); + }); + + it('should return empty defaults for unrecognized provider IDs not in DEFAULT_PROVIDERS or manifest', async () => { + await initAI({ + remoteConfigUrl: false, + providers: [{ id: 'custom-unrecognized-llm', key: 'test-key' }] + }); + + const config = getAiConfig(); + expect(config.providers?.[0].id).toBe('custom-unrecognized-llm'); + expect(config.providers?.[0].model).toBeUndefined(); + expect(config.providers?.[0].url).toBeUndefined(); + }); + + it('should retain fetchDefaults hook results alongside remote manifest resolution', async () => { + const mockManifest = { + version: '1.0', + providers: { + groq: { model: 'remote-manifest-groq-model' } + } + }; + + vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce( + new Response(JSON.stringify(mockManifest), { status: 200 }) + ); + + await initAI({ + providers: [{ id: 'groq', key: 'test-key' }], + fetchDefaults: async () => ({ timeout: 5000, ttl: 9999 }) + }); + + const config = getAiConfig(); + expect(config.providers?.[0].model).toBe('remote-manifest-groq-model'); + expect(config.providers?.[0].timeout).toBe(5000); + expect(config.providers?.[0].ttl).toBe(9999); + }); + + it('should prevent older async initAI invocation from overwriting newer provider state via revision tracking', async () => { + let resolveManifest1: (value: any) => void; + const manifestPromise1 = new Promise(res => { resolveManifest1 = res; }); + + const fetchSpy = vi.spyOn(globalThis, 'fetch'); + fetchSpy + .mockImplementationOnce(() => manifestPromise1 as any) + .mockResolvedValueOnce(new Response(JSON.stringify({ providers: { groq: { model: 'invocation-2-model' } } }), { status: 200 })); + + // Start invocation 1 (which hangs on manifest resolution) + const initPromise1 = initAI({ + remoteConfigUrl: 'https://tempo.magmacomputing.com.au/manifest-1.json', + providers: [{ id: 'groq', key: 'key-invocation-1', model: 'invocation-1-model' }] + }); + + // Synchronously start invocation 2 (newer) + const initPromise2 = initAI({ + remoteConfigUrl: 'https://tempo.magmacomputing.com.au/manifest-2.json', + providers: [{ id: 'groq', key: 'key-invocation-2', model: 'invocation-2-model' }] + }); + await initPromise2; + + expect(getAiConfig().providers?.[0].model).toBe('invocation-2-model'); + + // Resolve slow invocation 1 + resolveManifest1!(new Response(JSON.stringify({ providers: { groq: { model: 'stale-model' } } }), { status: 200 })); + await initPromise1; + + // Verify state was NOT overwritten by stale invocation 1 + expect(getAiConfig().providers?.[0].model).toBe('invocation-2-model'); + }); + + it('should allow fetchDefaults to override resolved defaults when config.providers is omitted on re-init', async () => { + // First init with raw provider (no explicit model override) + await initAI({ + remoteConfigUrl: false, + providers: [{ id: 'groq', key: 'test-key' }] + }); + + expect(getAiConfig().providers?.[0].model).toBe(DEFAULT_PROVIDERS.groq.model); + + // Re-init with fetchDefaults hook and omitted providers + await initAI({ + fetchDefaults: async () => ({ model: 'hook-overridden-model', timeout: 7777 }) + }); + + const config = getAiConfig(); + expect(config.providers?.[0].key).toBe('[REDACTED]'); + expect(config.providers?.[0].model).toBe('hook-overridden-model'); + expect(config.providers?.[0].timeout).toBe(7777); + }); + + it('should invalidate in-flight initAI calls across resetAI', async () => { + let resolveManifest: (value: any) => void; + const manifestPromise = new Promise(res => { resolveManifest = res; }); + + vi.spyOn(globalThis, 'fetch').mockImplementationOnce(() => manifestPromise as any); + + const inFlightInit = initAI({ + remoteConfigUrl: 'https://tempo.magmacomputing.com.au/manifest-slow.json', + providers: [{ id: 'groq', key: 'slow-key' }] + }); + + // Reset AI while first init is in-flight + resetAI(); + + // Start a new init + await initAI({ + remoteConfigUrl: false, + providers: [{ id: 'openai', key: 'fresh-key' }] + }); + + expect(getAiConfig().providers?.[0].id).toBe('openai'); + + // Resolve slow in-flight manifest + resolveManifest!(new Response(JSON.stringify({ providers: { groq: { model: 'stale-groq-model' } } }), { status: 200 })); + await inFlightInit; + + // Config should remain the fresh one, not overwritten by stale in-flight init + expect(getAiConfig().providers?.[0].id).toBe('openai'); + }); +}); diff --git a/packages/plugins/ai/test/index.spec.ts b/packages/plugins/ai/test/parse.test.ts similarity index 75% rename from packages/plugins/ai/test/index.spec.ts rename to packages/plugins/ai/test/parse.test.ts index 21599b73..a72d8dd3 100644 --- a/packages/plugins/ai/test/index.spec.ts +++ b/packages/plugins/ai/test/parse.test.ts @@ -1,19 +1,25 @@ -import { parseAI, initAI, clearAiCache, getAiRateLimits, TempoAiError, AiMode } from '../src/index.js'; +import { parseAI, initAI, clearAiCache, getAiRateLimits, getAiConfig, TempoAiError, AiMode } from '../src/index.js'; import { BoundedCache } from '@magmacomputing/tempo/support'; import { Tempo } from '@magmacomputing/tempo'; -describe('AI Parsing Plugin', () => { +describe('AI Parsing Plugin (parseAI)', () => { const liveApiKey = process.env.GROQ_API_KEY ?? process.env.OPENAI_API_KEY; const liveProviderId = process.env.GROQ_API_KEY ? 'groq' : 'openai'; const isLiveTest = Boolean(process.env.LIVE_AI_TEST && liveApiKey); - beforeEach(() => { + beforeEach(async () => { + vi.spyOn(console, 'warn').mockImplementation(() => {}); + vi.spyOn(console, 'error').mockImplementation(() => {}); + vi.spyOn(console, 'log').mockImplementation(() => {}); + if (isLiveTest) { - initAI({ + return initAI({ + remoteConfigUrl: false, providers: [{ id: liveProviderId, key: liveApiKey! }] }); } else { - initAI({ + return initAI({ + remoteConfigUrl: false, providers: [{ id: 'groq', key: 'mock-key-for-unit-testing' }] }); } @@ -23,6 +29,26 @@ describe('AI Parsing Plugin', () => { vi.restoreAllMocks(); }); + it('should return current runtime configuration via getAiConfig', async () => { + await initAI({ + remoteConfigUrl: false, + providers: [{ id: 'groq', key: 'test-key-123' }], + mode: AiMode.Fallback, + timeout: 3000, + debug: true + }); + + const config = getAiConfig(); + expect(config).toBeDefined(); + expect(config.mode).toBe('fallback'); + expect(config.timeout).toBe(3000); + expect(config.debug).toBe(true); + expect(config.providers).toHaveLength(1); + expect(config.providers?.[0].id).toBe('groq'); + expect(config.providers?.[0].key).toBe('[REDACTED]'); + expect(config.providers?.[0].model).toBe('llama-3.3-70b-versatile'); + }); + it('should fall back to native parsing first and attach .ai metadata', async () => { const result = await parseAI('2026-05-10'); expect(result.isValid).toBe(true); @@ -35,12 +61,13 @@ describe('AI Parsing Plugin', () => { }); it('should throw TempoAiError if reserved provider ID "native" or "cache" is used in initAI', () => { - expect(() => initAI({ providers: [{ id: 'native', key: '123' }] })).toThrow(TempoAiError); - expect(() => initAI({ providers: [{ id: 'cache', key: '123' }] })).toThrow(TempoAiError); + expect(() => initAI({ remoteConfigUrl: false, providers: [{ id: 'native', key: '123' }] })).toThrow(TempoAiError); + expect(() => initAI({ remoteConfigUrl: false, providers: [{ id: 'cache', key: '123' }] })).toThrow(TempoAiError); }); it('should canonicalize Gemini provider ID and use gemini-3.6-flash model by default', async () => { - initAI({ + await initAI({ + remoteConfigUrl: false, providers: [{ id: 'Gemini', key: 'mock-gemini-key' }] }); const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce(new Response(JSON.stringify({ @@ -54,11 +81,23 @@ describe('AI Parsing Plugin', () => { }); it('should throw TempoAiError if no key is configured and AI is needed', async () => { - initAI({ providers: [] }); + await initAI({ remoteConfigUrl: false, providers: [] }); await expect(parseAI('Next Thanksgiving')).rejects.toThrow(TempoAiError); await expect(parseAI('Next Thanksgiving')).rejects.toThrow('No AI providers configured.'); }); + it('should throw TempoAiError with status 400 if an invalid mode is specified in parseAI', async () => { + await initAI({ remoteConfigUrl: false, providers: [{ id: 'openai', key: 'test-key' }] }); + try { + await parseAI('Next Thanksgiving', { mode: 'invalid-mode' as any, force: true }); + expect.unreachable('Should have thrown TempoAiError'); + } catch (err: any) { + expect(err).toBeInstanceOf(TempoAiError); + expect(err.code).toBe(400); + expect(err.message).toContain("Invalid execution mode: 'invalid-mode'"); + } + }); + it('should parse natural language successfully and attach secured .ai metadata', async () => { if (!isLiveTest) { vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce(new Response(JSON.stringify({ @@ -76,6 +115,9 @@ describe('AI Parsing Plugin', () => { const result = await parseAI('The Friday after Thanksgiving', { anchor: anchorDate, timeZone: 'UTC', force: true }); expect(result).toBeInstanceOf(Tempo); + expect(result.constructor).toBe(Tempo); + const formatRef = result.format; + expect(result.format).toBe(formatRef); expect(result.isValid).toBe(true); expect(result.format('{yyyy}-{mm}-{dd}')).toBe('2026-11-27'); @@ -167,7 +209,8 @@ describe('AI Parsing Plugin', () => { }); it('should cascade from low-confidence local provider to high-confidence online provider in Fallback mode', async () => { - initAI({ + await initAI({ + remoteConfigUrl: false, providers: [ { id: 'local-llm', key: 'key1', url: 'https://api.openai.com/v1/chat', model: 'local' }, { id: 'cloud-llm', key: 'key2', url: 'https://api.openai.com/v1/chat', model: 'cloud' } @@ -175,11 +218,9 @@ describe('AI Parsing Plugin', () => { }); const fetchSpy = vi.spyOn(globalThis, 'fetch'); - // Provider 1 (local-llm): Low confidence (0.4) fetchSpy.mockResolvedValueOnce(new Response(JSON.stringify({ choices: [{ message: { content: '{"reasoning":"Uncertain local guess", "iso":"2026-11-26T00:00:00", "confidence":0.4}' } }] }), { status: 200 })); - // Provider 2 (cloud-llm): High confidence (0.95) fetchSpy.mockResolvedValueOnce(new Response(JSON.stringify({ choices: [{ message: { content: '{"reasoning":"High confidence cloud result", "iso":"2026-11-26T00:00:00", "confidence":0.95}' } }] }), { status: 200 })); @@ -193,7 +234,8 @@ describe('AI Parsing Plugin', () => { }); it('should short-circuit and stop looking to other providers when a provider meets minConfidence', async () => { - initAI({ + await initAI({ + remoteConfigUrl: false, providers: [ { id: 'local-llm', key: 'key1', url: 'https://api.openai.com/v1/chat', model: 'local' }, { id: 'cloud-llm', key: 'key2', url: 'https://api.openai.com/v1/chat', model: 'cloud' } @@ -201,7 +243,6 @@ describe('AI Parsing Plugin', () => { }); const fetchSpy = vi.spyOn(globalThis, 'fetch'); - // Provider 1 (local-llm): High confidence (0.90 >= 0.85) -> Short circuit! fetchSpy.mockResolvedValueOnce(new Response(JSON.stringify({ choices: [{ message: { content: '{"reasoning":"Confident local result", "iso":"2026-11-26T00:00:00", "confidence":0.90}' } }] }), { status: 200 })); @@ -238,16 +279,15 @@ describe('AI Parsing Plugin', () => { }); it('should support softErrors in array batch processing', async () => { - initAI({ + await initAI({ + remoteConfigUrl: false, providers: [{ id: 'groq', key: 'test-key' }] }); const fetchSpy = vi.spyOn(globalThis, 'fetch'); - // Item 1 succeeds fetchSpy.mockResolvedValueOnce(new Response(JSON.stringify({ choices: [{ message: { content: '{"reasoning":"Date 1", "iso":"2026-01-01T00:00:00", "confidence":0.99}' } }] }), { status: 200 })); - // Item 2 fails with 500 fetchSpy.mockResolvedValueOnce(new Response(null, { status: 500, statusText: 'Internal Error' })); const results = await parseAI(['Valid Date Prompt', 'Failing Prompt'], { force: true, softErrors: true }); @@ -270,7 +310,6 @@ describe('AI Parsing Plugin', () => { choices: [{ message: { content: '{"reasoning":"Fast", "iso":"2026-06-01T00:00:00", "confidence":0.95}' } }] }), { status: 200 }); } - // Slow model delays await new Promise(resolve => setTimeout(resolve, 500)); return new Response(JSON.stringify({ choices: [{ message: { content: '{"reasoning":"Slow", "iso":"2026-06-01T00:00:00", "confidence":0.95}' } }] @@ -318,7 +357,7 @@ describe('AI Parsing Plugin', () => { describe('Mocked Network Failures', () => { it('should throw TempoAiError with 401 when API key is bad, expired, or revoked', async () => { - initAI({ providers: [{ id: 'openai', key: 'bad_key' }] }); + await initAI({ remoteConfigUrl: false, providers: [{ id: 'openai', key: 'bad_key' }] }); vi.spyOn(global, 'fetch').mockResolvedValueOnce(new Response(null, { status: 401, @@ -336,7 +375,8 @@ describe('AI Parsing Plugin', () => { }); it('should seamlessly fallback to the next provider if the first hits a 429 Exhausted Key rate limit', async () => { - initAI({ + await initAI({ + remoteConfigUrl: false, providers: [ { id: 'openai', key: 'exhausted_key' }, { id: 'openai', key: 'good_key' } @@ -370,6 +410,31 @@ describe('AI Parsing Plugin', () => { const limits = getAiRateLimits(); expect(limits?.remainingTokens).toBe(5000); }); + + it('should convert invalid JSON provider response into TempoAiError with status 422', async () => { + await initAI({ + remoteConfigUrl: false, + providers: [{ id: 'openai', key: 'test-key' }] + }); + + const fetchSpy = vi.spyOn(globalThis, 'fetch'); + fetchSpy.mockResolvedValueOnce(new Response(JSON.stringify({ + choices: [{ message: { content: 'not valid json at all {{' } }] + }), { status: 200 })); + + await expect(parseAI('Thanksgiving', { force: true })).rejects.toThrowError(TempoAiError); + + fetchSpy.mockResolvedValueOnce(new Response(JSON.stringify({ + choices: [{ message: { content: 'not valid json at all {{' } }] + }), { status: 200 })); + + try { + await parseAI('Thanksgiving', { force: true }); + } catch (err: any) { + expect(err.code).toBe(422); + expect(err.message).toContain('returned invalid JSON payload'); + } + }); }); describe('BoundedCache & Eviction', () => { @@ -398,24 +463,32 @@ describe('AI Parsing Plugin', () => { expect(Array.from(cache.keys())).not.toContain('tempKey'); }); - it('should preserve clearAiCache functionality', () => { + it('should preserve clearAiCache functionality', async () => { const cache = new BoundedCache(100); cache.set('Thanksgiving::2026-05-10', '2026-11-26T00:00:00Z'); cache.set('Christmas::2026-05-10', '2026-12-25T00:00:00Z'); - initAI({ cache }); + await initAI({ remoteConfigUrl: false, cache }); clearAiCache('Thanksgiving'); expect(cache.has('Thanksgiving::2026-05-10')).toBe(false); expect(cache.has('Christmas::2026-05-10')).toBe(true); }); - it('should resolve static un-salted user glossary terms without hitting network or expiring', async () => { + it('should resolve static user glossary terms with context salt without hitting network or expiring', async () => { + const resolvedOptions = Tempo.options; + const tz = resolvedOptions.timeZone; + const cal = resolvedOptions.calendar; + const loc = Array.isArray(resolvedOptions.locale) ? resolvedOptions.locale[0] : resolvedOptions.locale; + const sph = resolvedOptions.sphere || 'north'; + const cacheSalt = new Tempo().format('{yyyy}-{mm}-{dd}'); + const cacheKey = `my_custom_company_glossary_term::${cacheSalt}::${tz}::${cal}::${loc}::${sph}`; + const glossary = new Map([ - ['my_custom_company_glossary_term', '2026-11-01T00:00:00Z'] + [cacheKey, '2026-11-01T00:00:00Z'] ]); - initAI({ cache: glossary }); + await initAI({ remoteConfigUrl: false, cache: glossary }); const fetchSpy = vi.spyOn(globalThis, 'fetch'); @@ -429,10 +502,9 @@ describe('AI Parsing Plugin', () => { describe('Rate Limit & Reset Header Parsing Hardening', () => { it('should correctly parse compound reset duration strings like 4m12s and 1h30m', async () => { - initAI({ providers: [{ id: 'openai', key: 'test-key' }] }); + await initAI({ remoteConfigUrl: false, providers: [{ id: 'openai', key: 'test-key' }] }); const fetchSpy = vi.spyOn(globalThis, 'fetch'); - // Response 1 with compound duration header '4m12s' fetchSpy.mockResolvedValueOnce(new Response(null, { status: 429, statusText: 'Too Many Requests', @@ -456,10 +528,9 @@ describe('AI Parsing Plugin', () => { }); it('should replace rather than retain prior rate-limit state when subsequent response has no headers', async () => { - initAI({ providers: [{ id: 'openai', key: 'test-key' }] }); + await initAI({ remoteConfigUrl: false, providers: [{ id: 'openai', key: 'test-key' }] }); const fetchSpy = vi.spyOn(globalThis, 'fetch'); - // Request 1: Has rate limit headers fetchSpy.mockResolvedValueOnce(new Response(JSON.stringify({ choices: [{ message: { content: '{"iso":"2026-11-26T00:00:00"}' } }] }), { @@ -472,7 +543,6 @@ describe('AI Parsing Plugin', () => { await parseAI('Thanksgiving 2026', { force: true }); expect(getAiRateLimits()?.remainingTokens).toBe(5000); - // Request 2: Has NO rate limit headers fetchSpy.mockResolvedValueOnce(new Response(JSON.stringify({ choices: [{ message: { content: '{"iso":"2026-12-25T00:00:00"}' } }] }), { @@ -481,12 +551,11 @@ describe('AI Parsing Plugin', () => { await parseAI('Christmas 2026', { force: true }); - // State should now be null (replaced, not retained!) expect(getAiRateLimits()).toBeNull(); }); it('should parse HTTP-date format Retry-After header strings into valid Tempo resetAt', async () => { - initAI({ providers: [{ id: 'openai', key: 'test-key' }] }); + await initAI({ remoteConfigUrl: false, providers: [{ id: 'openai', key: 'test-key' }] }); const fetchSpy = vi.spyOn(globalThis, 'fetch'); const httpDateStr = 'Wed, 21 Oct 2026 07:28:00 GMT'; @@ -504,8 +573,28 @@ describe('AI Parsing Plugin', () => { expect(getAiRateLimits()?.resetAt?.isValid).toBe(true); }); + it('should attach limits snapshot directly to the returned Tempo instance .ai property', async () => { + await initAI({ remoteConfigUrl: false, providers: [{ id: 'groq', key: 'test-key' }] }); + const fetchSpy = vi.spyOn(globalThis, 'fetch'); + + fetchSpy.mockResolvedValueOnce(new Response(JSON.stringify({ + choices: [{ message: { content: '{"iso":"2026-11-26T00:00:00"}' } }] + }), { + status: 200, + headers: new Headers({ + 'x-ratelimit-remaining-requests': '499', + 'x-ratelimit-remaining-tokens': '99500' + }) + })); + + const result = await parseAI('Thanksgiving 2026', { force: true }); + expect(result.ai?.limits).toBeDefined(); + expect(result.ai?.limits?.remainingRequests).toBe(499); + expect(result.ai?.limits?.remainingTokens).toBe(99500); + }); + it('should ignore invalid or malformed duration strings without throwing or crashing', async () => { - initAI({ providers: [{ id: 'openai', key: 'test-key' }] }); + await initAI({ remoteConfigUrl: false, providers: [{ id: 'openai', key: 'test-key' }] }); const fetchSpy = vi.spyOn(globalThis, 'fetch'); fetchSpy.mockResolvedValueOnce(new Response(null, { @@ -524,5 +613,33 @@ describe('AI Parsing Plugin', () => { expect(getAiRateLimits()).toBeNull(); }); + + it('should exclude transport timeout option from serialized request body', async () => { + await initAI({ + remoteConfigUrl: false, + providers: [{ + id: 'openai', + key: 'test-key', + options: { + timeout: 4500, + user: 'user-tempo-test' + } + }] + }); + const fetchSpy = vi.spyOn(globalThis, 'fetch'); + + fetchSpy.mockResolvedValueOnce(new Response(JSON.stringify({ + choices: [{ message: { content: '{"iso":"2026-11-26T00:00:00"}' } }] + }), { status: 200 })); + + await parseAI('Thanksgiving 2026', { force: true }); + + expect(fetchSpy).toHaveBeenCalledTimes(1); + const requestInit = fetchSpy.mock.calls[0][1]; + const parsedBody = JSON.parse(requestInit?.body as string); + + expect(parsedBody.user).toBe('user-tempo-test'); + expect(parsedBody.timeout).toBeUndefined(); + }); }); }); diff --git a/packages/plugins/ai/test/recurrence.test.ts b/packages/plugins/ai/test/recurrence.test.ts new file mode 100644 index 00000000..26106063 --- /dev/null +++ b/packages/plugins/ai/test/recurrence.test.ts @@ -0,0 +1,266 @@ +import { Tempo } from '@magmacomputing/tempo'; +import { isRRuleString } from '@magmacomputing/tempo/library'; +import { recurrenceAI, initAI } from '../src/index.js'; + +describe('AI Recurrence Plugin (recurrenceAI)', () => { + beforeEach(async () => { + vi.spyOn(console, 'warn').mockImplementation(() => { }); + vi.spyOn(console, 'error').mockImplementation(() => { }); + vi.spyOn(console, 'log').mockImplementation(() => { }); + await initAI({ remoteConfigUrl: false, providers: [{ id: 'groq', key: 'mock-key-for-unit-testing' }] }); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('should detect raw RRULE strings and parse them natively without network calls', async () => { + const rruleInput = 'FREQ=WEEKLY;BYDAY=TU;BYHOUR=15'; + expect(isRRuleString(rruleInput)).toBe(true); + + const result = await recurrenceAI(rruleInput, { count: 3 }); + expect(result).toBeDefined(); + expect(result.rrule).toBe('FREQ=WEEKLY;BYDAY=TU;BYHOUR=15'); + expect(result.provider).toBe('rrule-parser'); + expect(result.confidence).toBe(1.0); + expect(result.isFinite).toBe(false); + expect(result.size).toBe(Number.POSITIVE_INFINITY); + expect(result.summary).toBe('Recurring schedule (FREQ=WEEKLY;BYDAY=TU;BYHOUR=15)'); + + const batch1 = result.take(3); + expect(batch1).toHaveLength(3); + expect(batch1[0]).toBeInstanceOf(Tempo); + }); + + it('should support stateful paged batching via .take(n)', async () => { + const anchor = new Tempo('2026-08-01T09:00:00Z'); + const result = await recurrenceAI('FREQ=WEEKLY;BYDAY=FR', { anchor }); + + const batch1 = result.take(2); + expect(batch1).toHaveLength(2); + expect(batch1[0].format('{yyyy}-{mm}-{dd}')).toBe('2026-08-07'); + expect(batch1[1].format('{yyyy}-{mm}-{dd}')).toBe('2026-08-14'); + + const batch2 = result.take(2); + expect(batch2).toHaveLength(2); + expect(batch2[0].format('{yyyy}-{mm}-{dd}')).toBe('2026-08-21'); + expect(batch2[1].format('{yyyy}-{mm}-{dd}')).toBe('2026-08-28'); + }); + + it('should parse COUNT in finite RRULE strings, compute size, and return [] when exhausted', async () => { + const finiteRRule = 'FREQ=MONTHLY;BYDAY=1MO;COUNT=2'; + const result = await recurrenceAI(finiteRRule); + + expect(result.isFinite).toBe(true); + expect(result.size).toBe(2); + + const batch1 = result.take(2); + expect(batch1).toHaveLength(2); + + const exhaustedBatch = result.take(2); + expect(exhaustedBatch).toHaveLength(0); + expect(exhaustedBatch).toEqual([]); + }); + + it('should compile natural language prompts into RRULE strings, localized summary, and take() batches', async () => { + const fetchSpy = vi.spyOn(globalThis, 'fetch'); + fetchSpy.mockResolvedValueOnce(new Response(JSON.stringify({ + choices: [{ + message: { + content: JSON.stringify({ + rrule: 'FREQ=MONTHLY;BYDAY=2TU;BYHOUR=15', + summary: 'Chaque 2ème mardi du mois à 15:00', + reasoning: 'Parsed 2nd Tuesday schedule in French locale.', + confidence: 0.95 + }) + } + }] + }), { status: 200, headers: { 'Content-Type': 'application/json' } })); + + const prompt = 'Chaque 2ème mardi du mois à 15h'; + expect(isRRuleString(prompt)).toBe(false); + + const result = await recurrenceAI(prompt, { locale: 'fr-FR', count: 5 }); + expect(result.rrule).toBe('FREQ=MONTHLY;BYDAY=2TU;BYHOUR=15'); + expect(result.summary).toBe('Chaque 2ème mardi du mois à 15:00'); + expect(result.confidence).toBe(0.95); + + const batch = result.take(5); + expect(batch).toHaveLength(5); + expect(batch[0]).toBeInstanceOf(Tempo); + + // Assert request body receives ONLY recurrence schema without date-parser iso schema + expect(fetchSpy).toHaveBeenCalledTimes(1); + const requestBody = JSON.parse(fetchSpy.mock.calls[0][1]?.body as string); + const systemPrompt = requestBody.messages[0].content; + expect(systemPrompt).toContain('You are a calendar recurrence compiler.'); + expect(systemPrompt).toContain('"rrule": "Standard RFC 5545 RRULE string'); + expect(systemPrompt).not.toContain('You are a high-performance date parser.'); + expect(systemPrompt).not.toContain('"iso":'); + + // Verify iterator yields 5 items + const iterated = Array.from(result); + expect(iterated).toHaveLength(5); + }); + + it('should support provider race execution mode returning fastest provider and aborting slower requests', async () => { + let slowWasAborted = false; + const fetchSpy = vi.spyOn(globalThis, 'fetch'); + fetchSpy.mockImplementation(async (_url, init) => { + const body = JSON.parse(init?.body as string); + const signal = init?.signal as AbortSignal | undefined; + if (body.model === 'fast-model') { + return new Response(JSON.stringify({ + choices: [{ + message: { + content: JSON.stringify({ rrule: 'FREQ=DAILY', summary: 'Fast Daily', confidence: 0.95 }) + } + }] + }), { status: 200, headers: { 'Content-Type': 'application/json' } }); + } + return new Promise((_resolve, reject) => { + if (signal?.aborted) { + slowWasAborted = true; + reject(new DOMException('Aborted', 'AbortError')); + return; + } + signal?.addEventListener('abort', () => { + slowWasAborted = true; + reject(new DOMException('Aborted', 'AbortError')); + }); + }); + }); + + const resRace = await recurrenceAI('Every day', { + mode: 'race', + providers: [ + { id: 'slow-provider', key: 'key-1', url: 'https://api.openai.com/v1/chat/completions', model: 'slow-model' }, + { id: 'fast-provider', key: 'key-2', url: 'https://api.groq.com/v1/chat/completions', model: 'fast-model' } + ] + }); + + expect(resRace.rrule).toBe('FREQ=DAILY'); + expect(resRace.provider).toBe('fast-provider'); + expect(slowWasAborted).toBe(true); + }); + + it('should support consensus mode selecting highest confidence result when providers disagree', async () => { + const fetchSpy = vi.spyOn(globalThis, 'fetch'); + fetchSpy + .mockResolvedValueOnce(new Response(JSON.stringify({ + choices: [{ + message: { + content: JSON.stringify({ rrule: 'FREQ=WEEKLY;BYDAY=FR', summary: 'Every Friday', confidence: 0.85 }) + } + }] + }), { status: 200, headers: { 'Content-Type': 'application/json' } })) + .mockResolvedValueOnce(new Response(JSON.stringify({ + choices: [{ + message: { + content: JSON.stringify({ rrule: 'FREQ=MONTHLY;BYDAY=-1FR', summary: 'Last Friday of month', confidence: 0.96 }) + } + }] + }), { status: 200, headers: { 'Content-Type': 'application/json' } })); + + const resConsensus = await recurrenceAI('Last Friday of the month', { + mode: 'consensus', + providers: [ + { id: 'p1', key: 'key-1', url: 'https://api.groq.com/v1/chat/completions', model: 'm1' }, + { id: 'p2', key: 'key-2', url: 'https://api.openai.com/v1/chat/completions', model: 'm2' } + ] + }); + + expect(resConsensus.rrule).toBe('FREQ=MONTHLY;BYDAY=-1FR'); + expect(resConsensus.confidence).toBe(0.96); + expect(resConsensus.provider).toBe('p2'); + }); + + it('should throw TempoAiError for invalid execution mode', async () => { + await expect(recurrenceAI('Every Friday', { mode: 'invalid_mode' as any })) + .rejects.toThrow(/invalid execution mode/i); + }); + + it('should throw TempoAiError if provider returns empty or missing rrule string', async () => { + const fetchSpy = vi.spyOn(globalThis, 'fetch'); + fetchSpy.mockResolvedValueOnce(new Response(JSON.stringify({ + choices: [{ message: { content: JSON.stringify({ summary: 'No rrule', confidence: 0.9 }) } }] + }), { status: 200, headers: { 'Content-Type': 'application/json' } })); + + await expect(recurrenceAI('Unparseable prompt')) + .rejects.toThrow(/missing or empty rrule string/i); + }); + + it('should honor minConfidence threshold and throw TempoAiError if confidence is low', async () => { + const fetchSpy = vi.spyOn(globalThis, 'fetch'); + fetchSpy.mockResolvedValueOnce(new Response(JSON.stringify({ + choices: [{ + message: { + content: JSON.stringify({ + rrule: 'FREQ=DAILY', + summary: 'Uncertain daily', + confidence: 0.4 + }) + } + }] + }), { status: 200, headers: { 'Content-Type': 'application/json' } })); + + await expect(recurrenceAI('Uncertain repeat prompt', { minConfidence: 0.8 })) + .rejects.toThrow(/confidence \(0.4\) is below the required threshold of 0.8/i); + }); + + it('should evaluate RFC 5545 RRULE occurrences applying after and before windows', async () => { + const rruleStr = 'FREQ=DAILY;COUNT=10'; + const anchor = new Tempo('2026-08-01T09:00:00'); + const result = await recurrenceAI(rruleStr, { + anchor, + after: '2026-08-03T00:00:00', + before: '2026-08-06T00:00:00' + }); + + expect(result.isFinite).toBe(true); + const items = result.take(10); + expect(items).toHaveLength(3); + expect(items[0].format('{yyyy}-{mm}-{dd} {hh}:{mi}:{ss}')).toBe('2026-08-03 09:00:00'); + expect(items[1].format('{yyyy}-{mm}-{dd} {hh}:{mi}:{ss}')).toBe('2026-08-04 09:00:00'); + expect(items[2].format('{yyyy}-{mm}-{dd} {hh}:{mi}:{ss}')).toBe('2026-08-05 09:00:00'); + }); + + it('should correctly evaluate negative BYDAY ordinals such as -1FR and date-only UNTIL', async () => { + const rruleStr = 'FREQ=MONTHLY;BYDAY=-1FR;UNTIL=20261231'; + const anchor = new Tempo('2026-08-01T09:00:00Z'); + const result = await recurrenceAI(rruleStr, { anchor }); + + expect(result.isFinite).toBe(true); + expect(result.size).toBe(5); + const items = result.take(5); + expect(items).toHaveLength(5); + // August 2026 last Friday is Aug 28th + expect(items[0].format('{yyyy}-{mm}-{dd}')).toBe('2026-08-28'); + // September 2026 last Friday is Sep 25th + expect(items[1].format('{yyyy}-{mm}-{dd}')).toBe('2026-09-25'); + // October 2026 last Friday is Oct 30th + expect(items[2].format('{yyyy}-{mm}-{dd}')).toBe('2026-10-30'); + // November 2026 last Friday is Nov 27th + expect(items[3].format('{yyyy}-{mm}-{dd}')).toBe('2026-11-27'); + // December 2026 last Friday is Dec 25th + expect(items[4].format('{yyyy}-{mm}-{dd}')).toBe('2026-12-25'); + }); + + it('should propagate resolved context options (tz, cal, loc, sph) to generated Tempo instances and expandOccurrences', async () => { + const rruleStr = 'FREQ=DAILY;COUNT=3'; + const result = await recurrenceAI(rruleStr, { + anchor: '2026-08-01T09:00:00', + timeZone: 'Australia/Sydney', + calendar: 'iso8601', + locale: 'en-AU', + sphere: 'south', + }); + + const items = result.take(3); + expect(items).toHaveLength(3); + expect(items[0].config.timeZone).toBe('Australia/Sydney'); + expect(items[0].config.calendar).toBe('iso8601'); + expect(items[0].config.locale).toBe('en-AU'); + expect(items[0].config.sphere).toBe('south'); + }); +}); diff --git a/packages/plugins/ai/test/schedule.test.ts b/packages/plugins/ai/test/schedule.test.ts new file mode 100644 index 00000000..1fea398e --- /dev/null +++ b/packages/plugins/ai/test/schedule.test.ts @@ -0,0 +1,488 @@ +import { Tempo, Interval } from '@magmacomputing/tempo'; +import { ParseModule } from '@magmacomputing/tempo/parse'; +import { scheduleAI, initAI } from '../src/index.js'; + +Tempo.extend(ParseModule); + +describe('AI Schedule Plugin (scheduleAI)', () => { + beforeEach(async () => { + vi.spyOn(console, 'warn').mockImplementation(() => { }); + vi.spyOn(console, 'log').mockImplementation(() => { }); + await initAI({ remoteConfigUrl: false, providers: [{ id: 'groq', key: 'mock-key-for-unit-testing' }] }); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('should resolve natural language scheduling prompt into an Interval instance with AI metadata', async () => { + const fetchSpy = vi.spyOn(globalThis, 'fetch'); + fetchSpy.mockResolvedValueOnce(new Response(JSON.stringify({ + choices: [{ + message: { + content: JSON.stringify({ + start: '2026-08-11T14:15:00-07:00', + end: '2026-08-11T15:00:00-07:00', + durationMinutes: 45, + summary: '45-minute meeting on Tuesday afternoon', + reasoning: 'Selected Tuesday Aug 11 14:15-15:00 PST outside lunch.', + confidence: 0.95, + alternatives: [ + { start: '2026-08-11T15:15:00-07:00', end: '2026-08-11T16:00:00-07:00' } + ] + }) + } + }] + }), { status: 200, headers: { 'Content-Type': 'application/json' } })); + + const prompt = 'Find 45 minutes next Tuesday afternoon after 2pm PST excluding lunch'; + const anchor = new Tempo('2026-08-07T10:00:00-07:00'); + + const slot = await scheduleAI(prompt, { + anchor, + timeZone: 'America/Los_Angeles', + workingHours: { start: '09:00', end: '17:00' } + }); + + expect(slot).toBeInstanceOf(Interval); + expect(slot.start).toBeInstanceOf(Tempo); + expect(slot.end).toBeInstanceOf(Tempo); + + expect(slot.start.format('{yyyy}-{mm}-{dd} {hh}:{mi}')).toBe('2026-08-11 14:15'); + expect(slot.end.format('{yyyy}-{mm}-{dd} {hh}:{mi}')).toBe('2026-08-11 15:00'); + expect(slot.durationMinutes).toBe(45); + expect(slot.confidence).toBe(0.95); + expect(slot.provider).toBe('groq'); + expect(slot.reasoning).toContain('Selected Tuesday Aug 11'); + + expect(slot.alternatives).toHaveLength(1); + expect(slot.alternatives![0]).toBeInstanceOf(Interval); + expect(slot.alternatives![0].start?.format('{yyyy}-{mm}-{dd} {hh}:{mi}')).toBe('2026-08-11 15:15'); + + // Assert system prompt includes reference anchor and working hours + expect(fetchSpy).toHaveBeenCalledTimes(1); + const requestBody = JSON.parse(fetchSpy.mock.calls[0][1]?.body as string); + const systemPrompt = requestBody.messages[0].content; + expect(systemPrompt).toContain('You are a high-precision calendar scheduling engine.'); + expect(systemPrompt).toContain('Working Hours: 09:00 to 17:00'); + }); + + it('should accept busy events as core Interval instances and deterministically bump slot on conflict', async () => { + const fetchSpy = vi.spyOn(globalThis, 'fetch'); + // LLM hallucination: proposes a slot 14:00 - 14:45 that conflicts with busy1 (13:30 - 14:30) + fetchSpy.mockResolvedValueOnce(new Response(JSON.stringify({ + choices: [{ + message: { + content: JSON.stringify({ + start: '2026-08-11T14:00:00-07:00', + end: '2026-08-11T14:45:00-07:00', + summary: 'Conflicting proposed slot', + confidence: 0.90 + }) + } + }] + }), { status: 200, headers: { 'Content-Type': 'application/json' } })); + + const busy1 = new Interval( + new Tempo('2026-08-11 13:30:00', { timeZone: 'America/Los_Angeles' }), + new Tempo('2026-08-11 14:30:00', { timeZone: 'America/Los_Angeles' }) + ); + + const slot = await scheduleAI('Find 45 minutes next Tuesday afternoon', { + intervals: [busy1], + timeZone: 'America/Los_Angeles' + }); + + expect(slot).toBeInstanceOf(Interval); + // Deterministic check should have bumped start from 14:00 to 14:30 (end of busy1) + expect(slot.start.format('{yyyy}-{mm}-{dd} {hh}:{mi}')).toBe('2026-08-11 14:30'); + expect(slot.end.format('{yyyy}-{mm}-{dd} {hh}:{mi}')).toBe('2026-08-11 15:15'); + expect(slot.ai?.conflictBumped).toBe(true); + expect(slot.ai?.originalSlot?.start?.format('{yyyy}-{mm}-{dd} {hh}:{mi}')).toBe('2026-08-11 14:00'); + expect(slot.reasoning).toContain('[Adjusted for conflict]'); + }); + + it('should iteratively bump through multiple consecutive busy events', async () => { + const fetchSpy = vi.spyOn(globalThis, 'fetch'); + fetchSpy.mockResolvedValueOnce(new Response(JSON.stringify({ + choices: [{ + message: { + content: JSON.stringify({ + start: '2026-08-11T14:00:00-07:00', + end: '2026-08-11T14:45:00-07:00', + summary: 'Initial slot candidate', + confidence: 0.90, + }), + }, + }], + }), { status: 200, headers: { 'Content-Type': 'application/json' } })); + + const busy1 = new Interval( + new Tempo('2026-08-11 13:30:00', { timeZone: 'America/Los_Angeles' }), + new Tempo('2026-08-11 14:30:00', { timeZone: 'America/Los_Angeles' }) + ); + const busy2 = new Interval( + new Tempo('2026-08-11 14:30:00', { timeZone: 'America/Los_Angeles' }), + new Tempo('2026-08-11 15:00:00', { timeZone: 'America/Los_Angeles' }) + ); + + const slot = await scheduleAI('Find 45 minutes next Tuesday afternoon', { + intervals: [busy1, busy2], + timeZone: 'America/Los_Angeles', + }); + + expect(slot).toBeInstanceOf(Interval); + // Should have bumped past busy1 (to 14:30) and then past busy2 (to 15:00) + expect(slot.start.format('{yyyy}-{mm}-{dd} {hh}:{mi}')).toBe('2026-08-11 15:00'); + expect(slot.end.format('{yyyy}-{mm}-{dd} {hh}:{mi}')).toBe('2026-08-11 15:45'); + expect(slot.ai?.conflictBumped).toBe(true); + expect(slot.ai?.originalSlot?.start?.format('{yyyy}-{mm}-{dd} {hh}:{mi}')).toBe('2026-08-11 14:00'); + }); + + it('should wrap slot to next active working day when conflict bump pushes slot past working hours end', async () => { + const fetchSpy = vi.spyOn(globalThis, 'fetch'); + // Slot proposed at 16:15 - 17:00 on Friday (2026-08-14) + fetchSpy.mockResolvedValueOnce(new Response(JSON.stringify({ + choices: [{ + message: { + content: JSON.stringify({ + start: '2026-08-14T16:15:00-07:00', + end: '2026-08-14T17:00:00-07:00', + summary: 'Late Friday slot', + confidence: 0.90, + }), + }, + }], + }), { status: 200, headers: { 'Content-Type': 'application/json' } })); + + // Busy event from 16:00 to 16:45 on Friday + const busyFriday = new Interval( + new Tempo('2026-08-14 16:00:00', { timeZone: 'America/Los_Angeles' }), + new Tempo('2026-08-14 16:45:00', { timeZone: 'America/Los_Angeles' }) + ); + + const slot = await scheduleAI('Find 45 minutes Friday afternoon', { + intervals: [busyFriday], + timeZone: 'America/Los_Angeles', + workingHours: { + start: '09:00', + end: '17:00', + days: [1, 2, 3, 4, 5], + }, + }); + + expect(slot).toBeInstanceOf(Interval); + // Bump past 16:45 pushes 45m slot to 17:30 (exceeding 17:00), wrapping to Monday Aug 17 09:00 + expect(slot.start.format('{yyyy}-{mm}-{dd} {hh}:{mi}')).toBe('2026-08-17 09:00'); + expect(slot.end.format('{yyyy}-{mm}-{dd} {hh}:{mi}')).toBe('2026-08-17 09:45'); + expect(slot.ai?.conflictBumped).toBe(true); + expect(slot.ai?.originalSlot?.start?.format('{yyyy}-{mm}-{dd} {hh}:{mi}')).toBe('2026-08-14 16:15'); + }); + + it('should support provider race execution mode', async () => { + let slowWasAborted = false; + const fetchSpy = vi.spyOn(globalThis, 'fetch'); + fetchSpy.mockImplementation(async (_url, init) => { + const body = JSON.parse(init?.body as string); + const signal = init?.signal as AbortSignal | undefined; + if (body.model === 'fast-model') { + return new Response(JSON.stringify({ + choices: [{ + message: { + content: JSON.stringify({ + start: '2026-08-11T09:00:00Z', + end: '2026-08-11T10:00:00Z', + confidence: 0.95 + }) + } + }] + }), { status: 200, headers: { 'Content-Type': 'application/json' } }); + } + return new Promise((_resolve, reject) => { + if (signal?.aborted) { + slowWasAborted = true; + reject(new DOMException('Aborted', 'AbortError')); + return; + } + signal?.addEventListener('abort', () => { + slowWasAborted = true; + reject(new DOMException('Aborted', 'AbortError')); + }); + }); + }); + + const resRace = await scheduleAI('Schedule 1 hour slot', { + timeZone: 'UTC', + mode: 'race', + providers: [ + { id: 'slow-provider', key: 'key-1', url: 'https://api.openai.com/v1/chat/completions', model: 'slow-model' }, + { id: 'fast-provider', key: 'key-2', url: 'https://api.groq.com/v1/chat/completions', model: 'fast-model' } + ] + }); + + expect(resRace.provider).toBe('fast-provider'); + expect(resRace.start.format('{yyyy}-{mm}-{dd} {hh}:{mi}')).toBe('2026-08-11 09:00'); + expect(slowWasAborted).toBe(true); + }); + + it('should support provider consensus execution mode', async () => { + const fetchSpy = vi.spyOn(globalThis, 'fetch'); + fetchSpy + .mockResolvedValueOnce(new Response(JSON.stringify({ + choices: [{ + message: { + content: JSON.stringify({ + start: '2026-08-11T10:00:00Z', + end: '2026-08-11T10:30:00Z', + confidence: 0.82 + }) + } + }] + }), { status: 200, headers: { 'Content-Type': 'application/json' } })) + .mockResolvedValueOnce(new Response(JSON.stringify({ + choices: [{ + message: { + content: JSON.stringify({ + start: '2026-08-11T11:00:00Z', + end: '2026-08-11T11:30:00Z', + confidence: 0.96 + }) + } + }] + }), { status: 200, headers: { 'Content-Type': 'application/json' } })); + + const resConsensus = await scheduleAI('Schedule 30 minute slot', { + timeZone: 'UTC', + mode: 'consensus', + providers: [ + { id: 'p1', key: 'key-1', url: 'https://api.groq.com/v1/chat/completions', model: 'm1' }, + { id: 'p2', key: 'key-2', url: 'https://api.openai.com/v1/chat/completions', model: 'm2' } + ] + }); + + expect(resConsensus.confidence).toBe(0.96); + expect(resConsensus.provider).toBe('p2'); + expect(resConsensus.start.format('{yyyy}-{mm}-{dd} {hh}:{mi}')).toBe('2026-08-11 11:00'); + }); + + it('should throw TempoAiError when confidence is below minConfidence threshold', async () => { + const fetchSpy = vi.spyOn(globalThis, 'fetch'); + fetchSpy.mockResolvedValueOnce(new Response(JSON.stringify({ + choices: [{ + message: { + content: JSON.stringify({ + start: '2026-08-11T10:00:00Z', + end: '2026-08-11T10:30:00Z', + confidence: 0.50 + }) + } + }] + }), { status: 200, headers: { 'Content-Type': 'application/json' } })); + + await expect(scheduleAI('Low confidence prompt', { minConfidence: 0.8 })) + .rejects.toThrow(/scheduleAI confidence \(0.5\) is below the required threshold of 0.8/i); + }); + + it('should format ISO weekday numbers (including Sunday=7) and string tokens correctly in workingHours prompt', async () => { + const fetchSpy = vi.spyOn(globalThis, 'fetch'); + fetchSpy.mockResolvedValueOnce(new Response(JSON.stringify({ + choices: [{ + message: { + content: JSON.stringify({ + start: '2026-08-09T10:00:00Z', + end: '2026-08-09T11:00:00Z', + confidence: 0.95, + }), + }, + }], + }), { status: 200, headers: { 'Content-Type': 'application/json' } })); + + await scheduleAI('Schedule Sunday weekend session', { + workingHours: { + days: [7, 'SA', 'MON'], + start: '10:00', + end: '16:00', + }, + }); + + expect(fetchSpy).toHaveBeenCalledTimes(1); + const requestBody = JSON.parse(fetchSpy.mock.calls[0][1]?.body as string); + const systemPrompt = requestBody.messages[0].content; + expect(systemPrompt).toContain('Working Hours: 10:00 to 16:00 (Sunday, Saturday, Monday)'); + }); + + it('should throw TempoAiError if prompt is empty or providers missing', async () => { + await expect(scheduleAI('')).rejects.toThrow(/invalid scheduling prompt/i); + + await initAI({ remoteConfigUrl: false, providers: [] }); + await expect(scheduleAI('Schedule meeting')).rejects.toThrow(/no AI providers configured/i); + }); + + it('should preserve Interval prototype behavior, own-property metadata, and toString conversion', async () => { + const fetchSpy = vi.spyOn(globalThis, 'fetch'); + fetchSpy.mockResolvedValueOnce(new Response(JSON.stringify({ + choices: [{ + message: { + content: JSON.stringify({ + start: '2026-08-11T14:00:00Z', + end: '2026-08-11T15:00:00Z', + summary: '1-hour review session', + confidence: 0.95, + }), + }, + }], + }), { status: 200, headers: { 'Content-Type': 'application/json' } })); + + const slot = await scheduleAI('Schedule 1 hour review session', { + timeZone: 'UTC', + }); + + expect(slot).toBeInstanceOf(Interval); + expect(slot.constructor).toBe(Interval); + expect(slot.constructor === Interval).toBe(true); + const containsRef = slot.contains; + const toStringRef = slot.toString; + expect(slot.contains).toBe(containsRef); + expect(slot.toString).toBe(toStringRef); + expect(Object.prototype.toString.call(slot)).toBe('[object Tempo.Interval]'); + expect(typeof slot.toString).toBe('function'); + expect(typeof slot.valueOf).toBe('function'); + + // Metadata keys exist as own properties / proxy traps + expect('durationMinutes' in slot).toBe(true); + expect('summary' in slot).toBe(true); + expect('confidence' in slot).toBe(true); + expect(slot.summary).toBe('1-hour review session'); + + // Interval prototype methods operate normally + const testPoint = new Tempo('2026-08-11T14:30:00Z'); + expect(slot.contains(testPoint)).toBe(true); + }); + + it('should throw TempoAiError with status 400 for invalid mode in scheduleAI', async () => { + await initAI({ remoteConfigUrl: false, providers: [{ id: 'openai', key: 'test-key' }] }); + await expect(scheduleAI('Schedule meeting', { mode: 'invalid-mode' as any })) + .rejects.toThrow(/Invalid execution mode: 'invalid-mode'/); + }); + + it('should resolve options.timeZone and apply to anchorTempo in buildContextPrompt', async () => { + const fetchSpy = vi.spyOn(globalThis, 'fetch'); + fetchSpy.mockResolvedValueOnce(new Response(JSON.stringify({ + choices: [{ + message: { + content: JSON.stringify({ + start: '2026-08-11T14:00:00-04:00', + end: '2026-08-11T15:00:00-04:00', + summary: '1-hour meeting in NY', + confidence: 0.95, + }), + }, + }], + }), { status: 200, headers: { 'Content-Type': 'application/json' } })); + + await scheduleAI('Schedule 1 hour meeting', { + anchor: '2026-08-11T10:00:00', + timeZone: 'America/New_York', + }); + + expect(fetchSpy).toHaveBeenCalledTimes(1); + const requestBody = JSON.parse(fetchSpy.mock.calls[0][1]?.body as string); + const systemPrompt = requestBody.messages[0].content; + expect(systemPrompt).toContain('Reference Anchor Time: 2026-08-11T10:00:00 (America/New_York)'); + expect(systemPrompt).toContain('Target TimeZone: America/New_York'); + }); + + it('should sanitize invalid workingHours.days and fallback to weekdays without infinite loop', async () => { + const fetchSpy = vi.spyOn(globalThis, 'fetch'); + fetchSpy.mockResolvedValueOnce(new Response(JSON.stringify({ + choices: [{ + message: { + content: JSON.stringify({ + start: '2026-08-15T10:00:00Z', // Saturday + end: '2026-08-15T11:00:00Z', + summary: 'Saturday slot needing weekday bump', + confidence: 0.95, + }), + }, + }], + }), { status: 200, headers: { 'Content-Type': 'application/json' } })); + + const slot = await scheduleAI('Schedule 1 hour slot', { + anchor: '2026-08-14T09:00:00Z', + timeZone: 'UTC', + workingHours: { + days: [0, 8, -3, 'invalid-day' as any, 99], + start: '09:00', + end: '17:00' + }, + events: [ + { start: '2026-08-15T09:00:00Z', end: '2026-08-15T12:00:00Z' } + ] + }); + + expect(slot).toBeDefined(); + // Saturday bumped past weekend to Monday Aug 17th + expect(slot.start.format('{yyyy}-{mm}-{dd}')).toBe('2026-08-17'); + expect(slot.start.format('{hh}:{mi}')).toBe('09:00'); + }); + + it('should discard partial numeric day strings like "1junk" and "1.5" while keeping valid strings', async () => { + const fetchSpy = vi.spyOn(globalThis, 'fetch'); + fetchSpy.mockResolvedValueOnce(new Response(JSON.stringify({ + choices: [{ + message: { + content: JSON.stringify({ + start: '2026-08-15T10:00:00Z', // Saturday + end: '2026-08-15T11:00:00Z', + confidence: 0.95 + }) + } + }] + }), { status: 200, headers: { 'Content-Type': 'application/json' } })); + + const slot = await scheduleAI('Schedule 1 hour slot', { + anchor: '2026-08-14T09:00:00Z', + timeZone: 'UTC', + workingHours: { + days: ['1junk' as any, '1.5' as any, '2', 'TU', '4'], + start: '09:00', + end: '17:00' + } + }); + + expect(slot).toBeDefined(); + // Active days are Tuesday (2) and Thursday (4). Next active day from Saturday Aug 15 is Tuesday Aug 18. + expect(slot.start.format('{yyyy}-{mm}-{dd}')).toBe('2026-08-18'); + expect(slot.start.format('{hh}:{mi}')).toBe('09:00'); + }); + + it('should adjust slots outside working hours even when no busy events exist', async () => { + const fetchSpy = vi.spyOn(globalThis, 'fetch'); + fetchSpy.mockResolvedValueOnce(new Response(JSON.stringify({ + choices: [{ + message: { + content: JSON.stringify({ + start: '2026-08-11T07:00:00Z', // Early Tuesday (before 09:00) + end: '2026-08-11T08:00:00Z', + confidence: 0.95 + }) + } + }] + }), { status: 200, headers: { 'Content-Type': 'application/json' } })); + + const slot = await scheduleAI('Schedule 1 hour slot', { + anchor: '2026-08-10T09:00:00Z', + timeZone: 'UTC', + workingHours: { + start: '09:00', + end: '17:00' + } + }); + + expect(slot.start.format('{hh}:{mi}')).toBe('09:00'); + expect(slot.end.format('{hh}:{mi}')).toBe('10:00'); + expect(slot.ai?.conflictBumped).toBe(true); + }); +}); + + diff --git a/packages/plugins/astro/package.json b/packages/plugins/astro/package.json index 3b2624e8..2e1c800a 100644 --- a/packages/plugins/astro/package.json +++ b/packages/plugins/astro/package.json @@ -18,7 +18,7 @@ }, "scripts": { "build": "tsup && tsc", - "test": "vitest run -c ../vitest.shared.ts", + "test": "cross-env TEMPO_LICENSE_KEY=\"\" vitest run -c ../vitest.shared.ts", "prepublishOnly": "if [ $(git rev-parse --abbrev-ref HEAD) != main ]; then echo 'ERROR: Must be on main branch to publish.'; exit 1; fi && npm run build" }, "peerDependencies": { diff --git a/packages/plugins/batch/package.json b/packages/plugins/batch/package.json index 8c0a4c0d..a27780fc 100644 --- a/packages/plugins/batch/package.json +++ b/packages/plugins/batch/package.json @@ -17,7 +17,7 @@ }, "scripts": { "build": "tsup && tsc", - "test": "vitest run -c ../vitest.shared.ts", + "test": "cross-env TEMPO_LICENSE_KEY=\"\" vitest run -c ../vitest.shared.ts", "prepublishOnly": "if [ $(git rev-parse --abbrev-ref HEAD) != main ]; then echo 'ERROR: Must be on main branch to publish.'; exit 1; fi && npm run build" }, "tempo": { diff --git a/packages/plugins/finance/package.json b/packages/plugins/finance/package.json index 342f80f3..0cc07b2b 100644 --- a/packages/plugins/finance/package.json +++ b/packages/plugins/finance/package.json @@ -9,7 +9,7 @@ "scripts": { "build": "tsup && tsc", "prepublishOnly": "if [ $(git rev-parse --abbrev-ref HEAD) != main ]; then echo 'ERROR: Must be on main branch to publish.'; exit 1; fi && npm run build", - "test": "vitest run -c ../vitest.shared.ts" + "test": "cross-env TEMPO_LICENSE_KEY=\"\" vitest run -c ../vitest.shared.ts" }, "keywords": [ "tempo", @@ -42,7 +42,7 @@ }, "devDependencies": { "@magmacomputing/tempo": "^3.9.0", - "vitest": "^1.0.0" + "vitest": "^4.1.10" }, "exports": { ".": { diff --git a/packages/plugins/snap/package.json b/packages/plugins/snap/package.json index 7a3a3dbd..c11d7636 100644 --- a/packages/plugins/snap/package.json +++ b/packages/plugins/snap/package.json @@ -17,7 +17,7 @@ }, "scripts": { "build": "tsup && tsc", - "test": "vitest run -c ../vitest.shared.ts", + "test": "cross-env TEMPO_LICENSE_KEY=\"\" vitest run -c ../vitest.shared.ts", "prepublishOnly": "if [ $(git rev-parse --abbrev-ref HEAD) != main ]; then echo 'ERROR: Must be on main branch to publish.'; exit 1; fi && npm run build" }, "peerDependencies": { diff --git a/packages/plugins/sync/package.json b/packages/plugins/sync/package.json index 2dc61d87..51022cfd 100644 --- a/packages/plugins/sync/package.json +++ b/packages/plugins/sync/package.json @@ -17,7 +17,7 @@ }, "scripts": { "build": "tsup && tsc", - "test": "vitest run -c ../vitest.shared.ts", + "test": "cross-env TEMPO_LICENSE_KEY=\"\" vitest run -c ../vitest.shared.ts", "prepublishOnly": "if [ $(git rev-parse --abbrev-ref HEAD) != main ]; then echo 'ERROR: Must be on main branch to publish.'; exit 1; fi && npm run build" }, "tempo": { diff --git a/packages/plugins/vitest.shared.ts b/packages/plugins/vitest.shared.ts index fcc0ad19..19c90850 100644 --- a/packages/plugins/vitest.shared.ts +++ b/packages/plugins/vitest.shared.ts @@ -40,6 +40,7 @@ export default defineConfig({ { find: /^#tempo\/(.*)$/, replacement: resolve(__dirname, '../tempo/src/$1.ts') }, { find: /^#tempo$/, replacement: resolve(__dirname, '../tempo/src/tempo.index.ts') }, { find: /^@magmacomputing\/tempo\/plugin-api$/, replacement: resolve(__dirname, '../tempo/src/plugin-api.index.ts') }, + { find: /^@magmacomputing\/tempo\/library$/, replacement: resolve(__dirname, '../tempo/src/library.index.ts') }, { find: /^@magmacomputing\/tempo$/, replacement: resolve(__dirname, '../tempo/src/tempo.index.ts') }, { find: /^@magmacomputing\/tempo\/(parse|format|mutate|duration)$/, replacement: resolve(__dirname, '../tempo/src/module/module.$1.ts') }, { find: /^@magmacomputing\/tempo\/core$/, replacement: resolve(__dirname, '../tempo/src/core.index.ts') }, diff --git a/packages/tempo/.vitepress/config.ts b/packages/tempo/.vitepress/config.ts index 24b4bc1f..afa4d5f6 100644 --- a/packages/tempo/.vitepress/config.ts +++ b/packages/tempo/.vitepress/config.ts @@ -1,4 +1,5 @@ import { defineConfig } from 'vitepress' +import { withMermaid } from 'vitepress-plugin-mermaid' import { fileURLToPath } from 'node:url' import { Temporal } from '@js-temporal/polyfill' @@ -12,7 +13,7 @@ if (typeof (globalThis as any).Temporal === 'undefined') { } -export default defineConfig({ +export default withMermaid(defineConfig({ base: '/magma/', title: "Tempo", description: "The Professional Date-Time Library for Temporal", @@ -30,7 +31,7 @@ export default defineConfig({ { text: 'API Reference', link: '/api/index.html', target: '_self' }, { text: 'Library API', link: '/api/library/index.html', target: '_self' }, { text: 'Releases', link: '/doc/8-project-and-support/releases/' }, - { text: 'Functions ↗', link: 'https://magmacomputing.github.io/magma/functions/' } + { text: 'Functions', link: 'https://magmacomputing.github.io/magma/functions/' } ], sidebar: [ { @@ -38,6 +39,7 @@ export default defineConfig({ items: [ { text: 'Introduction', link: '/README' }, { text: 'Installation', link: '/doc/1-getting-started/installation' }, + { text: 'AI & IDE Integration', link: '/doc/1-getting-started/ai-integration' }, { text: 'Cookbook', link: '/doc/1-getting-started/tempo.cookbook' } ] }, @@ -201,4 +203,4 @@ export default defineConfig({ noExternal: ['@magmacomputing/tempo', '@magmacomputing/library'] } } -}) +})) diff --git a/packages/tempo/.vitepress/env.d.ts b/packages/tempo/.vitepress/env.d.ts new file mode 100644 index 00000000..11f02fe2 --- /dev/null +++ b/packages/tempo/.vitepress/env.d.ts @@ -0,0 +1 @@ +/// diff --git a/packages/tempo/.vitepress/theme/custom.css b/packages/tempo/.vitepress/theme/custom.css new file mode 100644 index 00000000..4a75a146 --- /dev/null +++ b/packages/tempo/.vitepress/theme/custom.css @@ -0,0 +1,33 @@ +/* Custom global theme styles for Tempo Plugins catalog */ + +.btn { + background-color: var(--vp-c-brand); + color: white; + border: none; + border-radius: 4px; + padding: 0.4rem 0.8rem; + cursor: pointer; + font-weight: 600; + font-size: 0.85rem; + text-decoration: none; + text-align: center; +} + +.icon-btn { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0.4rem; + line-height: 1; +} + +.btn-secondary { + background-color: var(--vp-c-bg-soft); + color: var(--vp-c-text-1) !important; + border: 1px solid var(--vp-c-divider); +} + +.btn-secondary:hover { + background-color: var(--vp-c-bg-mute); + text-decoration: none !important; +} diff --git a/packages/tempo/.vitepress/theme/data/catalog.json b/packages/tempo/.vitepress/theme/data/catalog.json index 8814dbdb..f40e7958 100644 --- a/packages/tempo/.vitepress/theme/data/catalog.json +++ b/packages/tempo/.vitepress/theme/data/catalog.json @@ -51,7 +51,7 @@ "packageName": "@magmacomputing/tempo-plugin-ai", "plan": "community", "status": "experimental", - "version": "0.2.0" + "version": "0.3.0" }, { "id": "ticker", diff --git a/packages/tempo/.vitepress/theme/index.ts b/packages/tempo/.vitepress/theme/index.ts index ac54a791..b46de4a3 100644 --- a/packages/tempo/.vitepress/theme/index.ts +++ b/packages/tempo/.vitepress/theme/index.ts @@ -2,6 +2,7 @@ import DefaultTheme from 'vitepress/theme' import type { Theme } from 'vitepress' import CatalogList from './components/CatalogList.vue' import Layout from './Layout.vue' +import './custom.css' export default { extends: DefaultTheme, diff --git a/packages/tempo/CHANGELOG.md b/packages/tempo/CHANGELOG.md index a5c684d4..2b6d9375 100644 --- a/packages/tempo/CHANGELOG.md +++ b/packages/tempo/CHANGELOG.md @@ -6,6 +6,22 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [3.11.1] - 2026-08-10 + +### Added +- **Timezone Abbreviation & Humanized Offset Parsing**: Upgraded `Token.tzd` snippet compilation and Master Guard scanning to natively support 3–4 letter timezone abbreviations (e.g. `AEST`, `PST`, `EST`, `CET`, `JST`) alongside `GMT`/`UTC` offset prefixes (e.g. `'Aug 6, 16:16 GMT+10'`, `'August 6, 16:16 AEST'`). Dynamically compiles `Token.tzd` from `DEFAULTS.TIMEZONE` and introduces `Match.offset` for clean structural offset matching with downstream `Temporal.ZonedDateTime` validation. +- **Cache Serialization (`toJSON`)**: Added native `toJSON()` serialization support to `BoundedCache` and the `Tempo.cache` facade object. Calling `Tempo.cache.toJSON()` or `JSON.stringify(Tempo.cache)` now cleanly converts active, non-expired in-memory cache entries into a plain key-value JavaScript object. +- **AI Context & IDE Integration (`llms.txt`)**: Published official standardized `llms.txt` and `llms-full.txt` context bundles at `https://tempo.magmacomputing.com.au` to provide full project context and enhance code-generation accuracy for IDE tools (Cursor, VS Code / GitHub Copilot, Antigravity) and web AI interfaces (ChatGPT, Claude, Gemini). +- **Automated Doc Harvester**: Created `bin/generate-llms-txt.mjs` monorepo build script integrated into `npm run docs:build` to harvest all 56 markdown documentation files into a unified `llms-full.txt` corpus. +- **AI Documentation Guide**: Added a dedicated `AI & IDE Integration` guide (`doc/1-getting-started/ai-integration.md`) featured directly in the primary VitePress navigation sidebar under Getting Started. + +### Changed & Hardened +- **Timezone Offset Normalization (`engine.lexer.ts`)**: Upgraded `parseZone` to normalize signed hour-and-minute offsets (e.g. `+5:30`, `-8:30`, `+05:30`, `+530`) to canonical ISO-8601 `±HH:MM` format before calling `toZonedDateTime`, enabling seamless parsing for half-hour and quarter-hour timezones. +- **Safe Timezone Configuration Mutation**: Hardened `parseZone` so that `config.timeZone` is updated only when `toZonedDateTime` completes successfully without throwing, preventing state mutation on invalid timezone identifiers. +- **Remote Provider Manifest Defaults (`providers.v1.json`)**: Updated the Groq provider default model from the retiring `llama-3.3-70b-versatile` to `openai/gpt-oss-120b`. +- **Hardened String-to-Temporal Composer (`engine.composer.ts`)**: Upgraded raw string fallback resolution to utilize a lookahead boundary regex (`/^(\d{4}-\d{2}-\d{2})\s+(?=\d{2}:\d{2})/`) and whitespace stripper (`/\s+(?=[Zz]|[+-]\d{2}|\[)/`). This automatically normalizes SQL/space-delimited timestamps (`2026-08-08 10:30`) to ISO 8601 (`2026-08-08T10:30`), collapses multiple whitespace runs, and strips spaces before UTC markers (`Z`), offsets (`+10:00`), and timezone brackets (`[Australia/Sydney]`), while ensuring timezone names containing internal spaces (e.g., `[America/Port of Spain]`) remain uncorrupted. +- **Master Guard Fast-Path Safety Valve (`module.parse.ts`)**: Added a zero-cost bypass for standard ISO date strings (`YYYY-MM-DD`), preventing false-negative token scanner rejections before reaching the layout resolution engine. + ## [3.11.0] - 2026-07-31 ### Added diff --git a/packages/tempo/README.md b/packages/tempo/README.md index 000076f2..332e14d9 100644 --- a/packages/tempo/README.md +++ b/packages/tempo/README.md @@ -93,7 +93,7 @@ For standard usage natively in the browser, use the pre-optimized **Global ESM B --- -## ✨ Why Tempo? +## ⏳ Why Tempo? While the native Temporal API gives you perfect primitives (`ZonedDateTime`, `PlainDate`), it doesn't give you business logic. Tempo bridges that gap. @@ -106,16 +106,10 @@ While the native Temporal API gives you perfect primitives (`ZonedDateTime`, `Pl ### The Missing Domain Layer -* **🏗️ Future Standard**: Built natively on the TC39 `Temporal` proposal. Inherit the reliability of the future standard. -* **🧩 Premium Ecosystem**: Don't build temporal math from scratch. Drop in our plugins for Astronomical seasons (`astro`) and atomic state syncing (`sync`). -* **🌍 Zero-Bundle Localization**: Best-in-class multi-language parsing and formatting powered natively by the `Intl` API—no massive static locale dictionaries required. -* **🗣️ Natural Language**: Resolve complex terms like "two days ago" with zero configuration. -* **🧠 Functional Aliases**: Extend the parser with custom logic using a powerful resolution context for relative date math. -* **🔄 Cycle Persistence**: Shift by semantic terms (Quarters, Seasons) while preserving your relative day-of-period offset. -* **⚡ Zero-Cost Parsing**: Lazy evaluation and smart matching ensure instantiation overhead is near-zero. -* **🛡️ Monorepo Resilient**: Built for stability in complex environments with proxy-protected registries. -* **📦 Tree-Shakable**: Keep your bundle light. Only load what you need—from Fiscal calendars to high-performance Tickers. -* **🪶 Ultra Lightweight**: Tempo itself is incredibly lean. While the required `Temporal` polyfill adds weight today, it can be dropped entirely the moment JavaScript environments natively adopt the Stage 4 standard. +* **🗣️ Natural Language & Smart Parsing**: Parse natural language phrases (e.g. "next Friday 3pm", "two days ago") with zero-cost lazy evaluation and functional aliases. +* **🌍 Zero-Bundle Localization**: Multi-language date parsing and formatting powered natively by ECMAScript `Intl`—no heavy static locale dictionaries required. +* **🧩 Extensible & AI-Ready**: Modular plugin ecosystem for astronomical cycles (`astro`), financial quarters (`finance`), schedulers (`ticker`), and LLM-powered parsing (`ai`). +* **🏗️ Future-Proof & Ultra-Lightweight**: Built natively on the TC39 Stage 4 `Temporal` foundation with zero legacy runtime baggage and full tree-shaking support. --- @@ -135,8 +129,9 @@ Tempo is the core library, but the ecosystem extends further: | Package | Description | Resources | | :--- | :--- | :--- | | **[`@magmacomputing/tempo`](https://www.npmjs.com/package/@magmacomputing/tempo)** | Core library — parsing, formatting, natural-language engine | [![Docs](https://img.shields.io/badge/Docs-VitePress-brightgreen?logo=vitepress&style=flat-square)](https://magmacomputing.github.io/magma/) | -| **[`@magmacomputing/tempo-fns`](https://www.npmjs.com/package/@magmacomputing/tempo-fns)** | Pure functional utilities built on native Temporal & Tempo — tree-shakeable helpers | [![Docs](https://img.shields.io/badge/Docs-VitePress-brightgreen?logo=vitepress&style=flat-square)](https://magmacomputing.github.io/magma/functions/) | +| **[`@magmacomputing/tempo-plugin-ai`](https://www.npmjs.com/package/@magmacomputing/tempo-plugin-ai)** | LLM-powered natural language parsing, recurrence expansion & smart scheduling | [![Docs](https://img.shields.io/badge/Docs-AI%20Plugin-blueviolet?logo=vitepress&style=flat-square)](https://magmacomputing.github.io/magma/doc/9-plugins/ai/index.html) | | **[`@magmacomputing/tempo-plugin-*`](https://www.npmjs.com/search?q=%40magmacomputing%2Ftempo-plugin)** | Premium & community plugins — Ticker, Astro, Finance, Sync, Snap and more | [![Ecosystem](https://img.shields.io/badge/Browse-Plugin%20Ecosystem-blueviolet?logo=npm&style=flat-square)](https://magmacomputing.github.io/magma/doc/3-extending-tempo/ecosystem) | +| **[`@magmacomputing/tempo-fns`](https://www.npmjs.com/package/@magmacomputing/tempo-fns)** | Pure functional utilities built on native Temporal & Tempo — tree-shakeable helpers | [![Docs](https://img.shields.io/badge/Docs-VitePress-brightgreen?logo=vitepress&style=flat-square)](https://magmacomputing.github.io/magma/functions/) | --- diff --git a/packages/tempo/bin/expand-typedoc.mjs b/packages/tempo/bin/expand-typedoc.mjs new file mode 100644 index 00000000..e46d93a5 --- /dev/null +++ b/packages/tempo/bin/expand-typedoc.mjs @@ -0,0 +1,135 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import ts from 'typescript'; + +const __filename = fileURLToPath(import.meta.url); +const tempoDir = path.dirname(path.dirname(__filename)); +const libraryDir = path.resolve(tempoDir, '../library'); + +console.log('🔍 Running TypeDoc Compiler API Type Expansion Post-Processor...'); + +const targets = [ + { + name: '@magmacomputing/library', + dir: libraryDir, + entryPoints: [ + path.resolve(libraryDir, 'src/common.index.ts'), + path.resolve(libraryDir, 'src/browser.index.ts'), + path.resolve(libraryDir, 'src/server.index.ts') + ], + tsconfigPath: path.resolve(libraryDir, 'tsconfig.json'), + htmlOutputDir: path.resolve(tempoDir, 'public/api/library/types') + }, + { + name: '@magmacomputing/tempo', + dir: tempoDir, + entryPoints: [ + path.resolve(tempoDir, 'src/tempo.index.ts') + ], + tsconfigPath: path.resolve(tempoDir, 'tsconfig.build.json'), + htmlOutputDir: path.resolve(tempoDir, 'public/api/types') + } +]; + +for (const target of targets) { + if (!fs.existsSync(target.htmlOutputDir)) { + console.warn(`⚠️ Skipping ${target.name}: HTML output directory does not exist yet (${target.htmlOutputDir})`); + continue; + } + + const configFile = ts.readConfigFile(target.tsconfigPath, ts.sys.readFile); + const parsedCmd = ts.parseJsonConfigFileContent(configFile.config, ts.sys, target.dir); + + const program = ts.createProgram(target.entryPoints, parsedCmd.options); + const checker = program.getTypeChecker(); + const typeMap = new Map(); + + function visit(node, currentNamespace = '') { + if (ts.isTypeAliasDeclaration(node) && node.modifiers?.some(m => m.kind === ts.SyntaxKind.ExportKeyword)) { + const typeName = node.name.text; + const fullKey = currentNamespace ? `${currentNamespace}.${typeName}` : typeName; + const rhsNode = node.type; + const rhsType = checker.getTypeAtLocation(rhsNode); + + let expanded = checker.typeToString( + rhsType, + rhsNode, + ts.TypeFormatFlags.NoTruncation | + ts.TypeFormatFlags.InTypeAlias | + ts.TypeFormatFlags.AllowUniqueESSymbolType + ); + + if (expanded === typeName || expanded.startsWith(`${typeName}<`)) { + expanded = rhsNode.getText(); + } + + const typeParams = node.typeParameters?.map(tp => tp.name.text).join(', '); + const fullSignature = typeParams ? `${typeName}<${typeParams}> = ${expanded}` : `${typeName} = ${expanded}`; + + typeMap.set(typeName, fullSignature); + typeMap.set(fullKey, fullSignature); + } else if (ts.isModuleDeclaration(node) && node.body) { + const nsName = node.name.text; + const nextNs = currentNamespace ? `${currentNamespace}.${nsName}` : nsName; + ts.forEachChild(node.body, child => visit(child, nextNs)); + } + } + + for (const sourceFile of program.getSourceFiles()) { + if (sourceFile.isDeclarationFile) continue; + ts.forEachChild(sourceFile, node => visit(node)); + } + + console.log(`Found ${typeMap.size} type alias mappings for ${target.name}.`); + + const htmlFiles = fs.readdirSync(target.htmlOutputDir).filter(f => f.endsWith('.html')); + let processedCount = 0; + + for (const file of htmlFiles) { + const filePath = path.join(target.htmlOutputDir, file); + let html = fs.readFileSync(filePath, 'utf-8'); + + // Extract type identifier from filename e.g.: + // - common.index.CountOf.html -> CountOf + // - Tempo.DateTime.html -> Tempo.DateTime or DateTime + // - Tempo.WEEKDAY-1.html -> Tempo.WEEKDAY or WEEKDAY + const cleanName = file.replace(/\.html$/, '').replace(/-\d+$/, ''); + const parts = cleanName.split('.'); + const typeName = parts[parts.length - 1]; + + const expandedSig = typeMap.get(cleanName) || typeMap.get(typeName); + + if (expandedSig && !html.includes('expanded-type-details')) { + const injectionHtml = ` +
+ + 🔍 Expanded Type Evaluation (Compiler API) + +
+
type ${escapeHtml(expandedSig)}
+
+
+`; + + const signatureEndIdx = html.indexOf('', html.indexOf('class="tsd-signature"')); + if (signatureEndIdx !== -1) { + const insertPos = signatureEndIdx + 6; + html = html.slice(0, insertPos) + injectionHtml + html.slice(insertPos); + fs.writeFileSync(filePath, html, 'utf-8'); + processedCount++; + } + } + } + + console.log(`✅ Injected expanded type definitions into ${processedCount} HTML pages in ${path.relative(tempoDir, target.htmlOutputDir)}`); +} + +function escapeHtml(str) { + return str + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); +} diff --git a/packages/tempo/bin/generate-llms-txt.mjs b/packages/tempo/bin/generate-llms-txt.mjs new file mode 100644 index 00000000..930c7562 --- /dev/null +++ b/packages/tempo/bin/generate-llms-txt.mjs @@ -0,0 +1,42 @@ +import { readdir, readFile, writeFile } from 'node:fs/promises'; +import { join, relative } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __dirname = fileURLToPath(new URL('.', import.meta.url)); +const docDir = join(__dirname, '../doc'); +const outputFile = join(__dirname, '../public/llms-full.txt'); + +async function getMarkdownFiles(dir) { + const entries = await readdir(dir, { withFileTypes: true }); + let files = []; + for (const entry of entries) { + const res = join(dir, entry.name); + if (entry.isDirectory()) { + files = files.concat(await getMarkdownFiles(res)); + } else if (entry.isFile() && entry.name.endsWith('.md')) { + files.push(res); + } + } + return files.sort(); +} + +async function generateLlmsFull() { + try { + const files = await getMarkdownFiles(docDir); + let content = `# Tempo Full Documentation Context\n\n> This file contains the complete concatenated markdown documentation set for @magmacomputing/tempo. It is intended for automated LLM context ingestion and RAG indexing.\n\n---\n\n`; + + for (const file of files) { + const relPath = relative(docDir, file); + const fileContent = await readFile(file, 'utf-8'); + content += `\n\n\n# Document: ${relPath}\n\n${fileContent}\n\n\n\n---\n`; + } + + await writeFile(outputFile, content, 'utf-8'); + console.log(`✅ Successfully generated llms-full.txt (${files.length} markdown documents merged)`); + } catch (err) { + console.error('❌ Error generating llms-full.txt:', err); + process.exit(1); + } +} + +generateLlmsFull(); diff --git a/packages/tempo/bin/update-version.mjs b/packages/tempo/bin/update-version.mjs index bb0cff10..2e726a54 100644 --- a/packages/tempo/bin/update-version.mjs +++ b/packages/tempo/bin/update-version.mjs @@ -9,7 +9,7 @@ * Called automatically by `npm run prebuild`. */ import pkg from '../package.json' with { type: 'json' }; -import { writeFileSync } from 'node:fs'; +import { writeFileSync, readFileSync, existsSync } from 'node:fs'; import { resolve, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; @@ -28,5 +28,13 @@ export const TEMPO_VERSION = '${version}'; `; writeFileSync(versionFile, content, 'utf-8'); + +const esmHtmlFile = resolve(__dirname, '../public/esm_sh.index.html'); +if (existsSync(esmHtmlFile)) { + let html = readFileSync(esmHtmlFile, 'utf-8'); + html = html.replace(/https:\/\/esm\.sh\/@magmacomputing\/tempo@[^\"]+/, `https://esm.sh/@magmacomputing/tempo@${version}`); + writeFileSync(esmHtmlFile, html, 'utf-8'); +} + console.log(`✅ Tempo version stamped: ${version}`); diff --git a/packages/tempo/doc/1-getting-started/ai-integration.md b/packages/tempo/doc/1-getting-started/ai-integration.md new file mode 100644 index 00000000..51a2dd1e --- /dev/null +++ b/packages/tempo/doc/1-getting-started/ai-integration.md @@ -0,0 +1,76 @@ +# 🤖 AI & IDE Integration (`llms.txt`) + +To ensure modern AI coding assistants—such as **Cursor**, **VS Code (GitHub Copilot)**, **Antigravity**, **ChatGPT**, and **Claude**—generate accurate, idiomatically aligned Tempo code and minimize hallucinations, Tempo publishes an official, standardized [`llms.txt`](https://tempo.magmacomputing.com.au/llms.txt) rulebook. + +By providing these rules to your AI assistant, your IDE will respect Tempo's strict immutability, zero-cost getters, native `Temporal` runtime expectations, and layout token syntax out-of-the-box. + +--- + +## 🚀 Quick Setup by IDE / Tool + +### 1. Cursor IDE +Add Tempo to Cursor's native documentation index: +1. Open **Cursor Settings** (`Cmd + ,` or `Ctrl + ,`). +2. Navigate to **Features** ➔ **Docs**. +3. Click **+ Add new doc** and enter: + - **Name**: `Tempo` + - **URL**: `https://tempo.magmacomputing.com.au/llms.txt` + +> [!TIP] +> Once added, type `@Tempo` in any Cursor chat or prompt window to inject exact API syntax rules into your conversation. + +--- + +### 2. VS Code & GitHub Copilot +In VS Code, configure GitHub Copilot Chat by adding a `.github/copilot-instructions.md` file to the root of your workspace: + +```markdown +# Tempo AI Rules +- Always use `Tempo` from `@magmacomputing/tempo`. +- Never instantiate legacy JavaScript `Date`. Tempo expects native `Temporal` or polyfill. +- All mutating methods (`.add()`, `.subtract()`, `.with()`) return a brand-new, frozen `Tempo` instance. +- Refer to https://tempo.magmacomputing.com.au/llms.txt for full layout token grammar. +``` + +When prompting Copilot Chat in VS Code: +```text +"Using https://tempo.magmacomputing.com.au/llms.txt, write a custom layout parser..." +``` + +--- + +### 3. Antigravity AI Assistant +In Antigravity, you can reference the live endpoint directly in your chat prompt or store it as a localized Knowledge Item (KI): +- Reference `@https://tempo.magmacomputing.com.au/llms.txt` in your prompt for instant context ingestion. + +--- + +### 4. ChatGPT & Claude Projects +For web-based LLM interfaces, reference or copy-paste the full, un-truncated documentation context file: +👉 **[Full RAG Documentation Bundle (`llms-full.txt`)](https://tempo.magmacomputing.com.au/llms-full.txt)** + +--- + +## 🛠️ Prompting AI for Custom Layout Extensions + +When asking AI assistants to generate custom layout patterns for parsing unique date-time formats, instruct the model to use Tempo's configuration syntax (`Tempo.init({ registry: { layouts: { ... } } })`) and named capture tokens (`{yy}`, `{mm}`, `{dd}`, `{hh}`, `{mi}`, `{ss}`, `{tzd}`). + +### Sample Prompt: +> *"Using the rules from https://tempo.magmacomputing.com.au/llms.txt, register a custom Tempo layout for fiscal quarters (e.g., 'Q3 2026') using `Tempo.init({ registry: { layouts: { ... } } })` and instantiate a date with the layout option."* + +### Generated Code (Actual Tempo Syntax): +```typescript +import { Tempo } from '@magmacomputing/tempo'; + +// 1. Register custom layout pattern using snippet tokens +Tempo.init({ + registry: { + layouts: { + fiscal_quarter: 'Q{nbr} {yy}' + } + } +}); + +// 2. Parse date string using the registered layout +const date = new Tempo('Q3 2026', { layout: 'fiscal_quarter' }); +``` diff --git a/packages/tempo/doc/1-getting-started/installation.md b/packages/tempo/doc/1-getting-started/installation.md index 8bf64aac..c532d0a4 100644 --- a/packages/tempo/doc/1-getting-started/installation.md +++ b/packages/tempo/doc/1-getting-started/installation.md @@ -242,3 +242,14 @@ We recommend pinning your versions in production environments to ensure stabilit * **JSDelivr**: `https://cdn.jsdelivr.net/npm/@magmacomputing/tempo@3/...` (Locks to major version 3) * **Latest**: `https://cdn.jsdelivr.net/npm/@magmacomputing/tempo/...` (Omit the version string to always receive the latest release. Note that JSDelivr will resolve a missing version tag to the latest published release). + +--- + +## 🤖 AI & IDE Integration (`llms.txt`) + +> [!TIP] +> Using **Cursor**, **VS Code (GitHub Copilot)**, **Antigravity**, **ChatGPT**, or **Claude**? +> Tempo publishes an official [`llms.txt`](https://tempo.magmacomputing.com.au/llms.txt) index to give AI assistants documented project context and reduce hallucinations regarding Tempo's syntax, token grammar, and immutability rules. +> +> 👉 **[Read the dedicated AI & IDE Integration Guide](./ai-integration.md)** for step-by-step setup instructions for your IDE or tool. + diff --git a/packages/tempo/doc/2-core-concepts/tempo.parse.md b/packages/tempo/doc/2-core-concepts/tempo.parse.md index e4668e7b..d2afb4a6 100644 --- a/packages/tempo/doc/2-core-concepts/tempo.parse.md +++ b/packages/tempo/doc/2-core-concepts/tempo.parse.md @@ -126,6 +126,29 @@ Tempo.extend(ParseModule); Tempo uses your configuration to intelligently parse ambiguous dates and foreign languages. +### TimeZone & Offset Parsing +Tempo natively supports human-readable timezone abbreviations (e.g. `AEST`, `PST`, `EST`, `CET`, `JST`) as well as explicit `GMT` / `UTC` offset designators (e.g. `'Aug 6, 16:16 GMT+10'`, `'August 6, 16:16 AEST'`, `'UTC-5'`). + +> [!WARNING] +> **Default Layout Positioning & Collisions** +> By default, Tempo's built-in layouts expect timezone offsets and abbreviations to *follow* the date/time payload. This design prevents leading abbreviations from colliding with 3-letter month names (such as `MAR` for March vs. Marshall Islands Time). + +#### Registering a Custom Leading Timezone Layout +If your application processes custom log files or legacy data streams that position timezone designators at the *start* (e.g. `"PST 8 Aug 10:30"`), you can register a custom layout using `{tzd}` alongside `{dt}` and `{tm}`. The `{tzd}` token accepts both timezone offset designators (`+10:00`, `Z`, `GMT+10`) and registered timezone abbreviations (such as `AEST`, `PST`, `EST`): + +```typescript +Tempo.init({ + registry: { + layouts: { + leadingTz: '{tzd}{sep}+{dt}{sep}+{tm}' + } + } +}); + +const t = new Tempo('PST 8 Aug 10:30'); +console.log(t.tz); // 'America/Los_Angeles' +``` + ### US-Style Dates (`MM/DD/YYYY`) If you parse a numeric string like `04012026`, Tempo uses your `timeZone` to decide if it means **April 1st** (US) or **4th of January** (UK/AU). diff --git a/packages/tempo/doc/3-extending-tempo/tempo.layout.md b/packages/tempo/doc/3-extending-tempo/tempo.layout.md index 39896a86..98ae8d0a 100644 --- a/packages/tempo/doc/3-extending-tempo/tempo.layout.md +++ b/packages/tempo/doc/3-extending-tempo/tempo.layout.md @@ -101,6 +101,19 @@ console.log(regex.source); --- +## 🤖 AI & LLM Layout Prompting + +When prompting AI assistants (Cursor, GitHub Copilot, ChatGPT, Claude) to write custom `Tempo` regular expression snippets and layout extensions: + +1. **Ingest AI Rules**: Provide the assistant with our official `llms.txt` rules by referencing `@https://tempo.magmacomputing.com.au/llms.txt` in Cursor or pasting `llms.txt` context into ChatGPT. +2. **Explicit Token Request**: Ask the LLM to use Tempo's standard snippet tokens (`{yy}`, `{mm}`, `{dd}`, `{hh}`, `{mi}`, `{ss}`, `{tzd}`) rather than raw, un-anchored regular expressions. +3. **Example AI Prompt**: + ```text + "Using https://tempo.magmacomputing.com.au/llms.txt, register a custom layout for strings like 'Q3 2026' using Tempo.init({ registry: { layouts: { ... } } }) and snippet tokens." + ``` + +--- + ## Professional Services If your project involves specialized terminology, complex financial calendars, or legacy application log formats, the **Magma Computing Solutions** team offers professional services to design and test custom `Tempo` Layouts optimized for your business needs. diff --git a/packages/tempo/doc/3-extending-tempo/tempo.modularity.md b/packages/tempo/doc/3-extending-tempo/tempo.modularity.md index e064b12b..9c85e87e 100644 --- a/packages/tempo/doc/3-extending-tempo/tempo.modularity.md +++ b/packages/tempo/doc/3-extending-tempo/tempo.modularity.md @@ -117,11 +117,9 @@ declare module '@magmacomputing/tempo' { There is a subtle but important distinction between how features are activated in Core mode: * **`Tempo.extend(Module)`**: This is **Immediate and Explicit**. It applies the module to the class exactly when the line is executed. This is the recommended pattern for modular applications. -* **`Tempo.init()`**: This is **Discovery-Driven**. It scans the global environment for any plugins that were imported via side effects (e.g., `import '@magmacomputing/tempo/term'`) and hydrates the engine all at once. +* **`Tempo.init()`**: Establishes global baseline configuration at application startup and registers plugins specified in the `plugins` array during initial startup discovery. -::: danger -**The Initialization Lifecycle**: `Tempo.init()` performs a **full state refresh**. It resets configuration, Term registries, and formatting maps to defaults before re-applying all currently discovered plugins. To ensure your custom logic is managed correctly, always use `Tempo.extend()` or encapsulate changes within a formal plugin. +::: note +**Startup Initialization Lifecycle**: `Tempo.init()` establishes baseline configuration during application startup. Built-in plugins are registered automatically via static imports in full Tempo (`@magmacomputing/tempo`), while explicit plugin lists are registered during `Tempo.init({ plugins: [...] })`. Note that subsequent calls to `Tempo.init()` re-evaluate and merge configuration options rather than executing as an idempotent no-op. To register additional plugins dynamically after startup, use `Tempo.extend(...)`. ::: -**The Side-Effect Trap**: If you import a side-effect plugin *after* you have already called `Tempo.init()`, the feature will **not** automatically appear on the `Tempo` class. Because `Tempo.init()` short-circuits once state already exists, re-calling it will not load those late modules. Use `Tempo.extend()` explicitly to activate late-loaded modules instead of trying to re-run `Tempo.init()`. - diff --git a/packages/tempo/doc/3-extending-tempo/tempo.plugin.md b/packages/tempo/doc/3-extending-tempo/tempo.plugin.md index 89d7fcfa..737382e7 100644 --- a/packages/tempo/doc/3-extending-tempo/tempo.plugin.md +++ b/packages/tempo/doc/3-extending-tempo/tempo.plugin.md @@ -92,17 +92,20 @@ Modern Tempo plugins are designed to be "plug-and-play." By using the `definePlu ::: ```typescript -import '@magmacomputing/tempo-plugin-ticker'; // 1. Module self-registers via side-effect -import { Tempo } from '@magmacomputing/tempo/core'; // 2. Load the `lite` engine +import { Tempo } from '@magmacomputing/tempo/core'; // 1. Load the `lite` engine +import { TickerPlugin } from '@magmacomputing/tempo-plugin-ticker'; // 2. Import the plugin -Tempo.init({ license: 'YOUR_JWT_KEY' }); // 3. Discover, verify, and activate all imported plugins +Tempo.init({ + license: 'YOUR_JWT_KEY', + plugins: [TickerPlugin] // 3. Register and activate plugin during init +}); // Ticker is now available on the core Tempo class! const pulse = Tempo.ticker(1); ``` -> [!NOTE] Import Order -> While older versions of Tempo were sensitive to import order, current versions handle sequencing robustly. `Tempo.init()` is automatically called during bootstrap to ensure all discovered plugins are integrated. If you dynamically load plugins later, you can call `Tempo.init()` manually to refresh the registry. +> [!NOTE] Dynamic Extension vs Startup Registration +> `Tempo.init({ plugins: [...] })` establishes baseline configuration at startup and performs initial registration of plugins. In full Tempo (`@magmacomputing/tempo`), standard plugins are registered automatically upon import. To dynamically register custom plugins loaded later at runtime, use `Tempo.extend(Plugin)` directly rather than re-running `Tempo.init()`. --- @@ -141,21 +144,21 @@ if (errorCondition) { This pattern ensures that Tempo remains robust in production environments while providing strict validation during development. -## Alternative: Standalone Functions (`functions`) +## Alternative: Standalone Functions (`tempo-fns`) The JavaScript ecosystem is divided between two architectural preferences: **Chained Fluent APIs** (like Tempo Plugins) and **Pure Standalone Functions** (for aggressive tree-shaking). -To support teams that mandate strict 0kb bundle-impacts and functional programming paradigms, Magma Computing provides the **`@magmacomputing/functions`** library. +To support teams that mandate strict 0kb bundle-impacts and functional programming paradigms, Magma Computing provides the **`@magmacomputing/tempo-fns`** library. ```typescript // The Pure, Tree-shakeable approach: -import { isFirstDayOfMonth } from '@magmacomputing/functions'; +import { isFirstDayOfMonth } from '@magmacomputing/tempo-fns'; import { Tempo } from '@magmacomputing/tempo/core'; if (isFirstDayOfMonth(new Tempo('2024-01-01'))) { ... } ``` -When building complex logic, consider whether it belongs as a core Plugin extension, or as a standalone utility in `functions` (or a hybrid wrapper of both!). +When building complex logic, consider whether it belongs as a core Plugin extension, or as a standalone utility in `tempo-fns` (or a hybrid wrapper of both!). ## Distributing Your Plugin diff --git a/packages/tempo/doc/3-extending-tempo/tempo.registry.md b/packages/tempo/doc/3-extending-tempo/tempo.registry.md index 2c72dee9..46c24bf3 100644 --- a/packages/tempo/doc/3-extending-tempo/tempo.registry.md +++ b/packages/tempo/doc/3-extending-tempo/tempo.registry.md @@ -35,22 +35,21 @@ Tempo.extend({ ## 📅 TIMEZONE Registry -Tempo includes a built-in registry of common timezone abbreviations. **These aliases map to DST-aware regional IANA zones, not fixed UTC offsets.** For example, `gmt` maps to `Europe/London` and `est` maps to `America/New_York`, which means they will automatically adjust to British Summer Time (BST) or Eastern Daylight Time (EDT) seasonally. If your application strictly requires a literal fixed offset (e.g., standard EST year-round), you should use an appropriate fixed-offset zone instead of these regional aliases. +Tempo includes a built-in registry of common timezone abbreviations. **Most regional aliases map to DST-aware regional IANA zones, not fixed UTC offsets.** For example, both `est` and `edt` map to `America/New_York`, which means dates parsed during summer months will automatically adjust to Eastern Daylight Time (`-04:00`), whereas `gmt` and `utc` map to fixed `UTC` (`+00:00`). If your application requires a constant, non-shifting offset year-round (e.g. strict UTC-5 without Daylight Saving adjustments), use an explicit fixed offset like `"-05:00"` or `'Etc/GMT+5'` instead of a regional abbreviation. | Alias | IANA Identifier | | :--- | :--- | -| `utc` | `UTC` | -| `gmt` | `Europe/London` | -| `est` | `America/New_York` | -| `cst` | `America/Chicago` | -| `mst` | `America/Denver` | -| `pst` | `America/Los_Angeles` | -| `aest` | `Australia/Sydney` | -| `acst` | `Australia/Adelaide` | +| `utc`/ `gmt` | `UTC` | +| `est` / `edt` | `America/New_York` | +| `cst` / `cdt` | `America/Chicago` | +| `mst` / `mdt` | `America/Denver` | +| `pst` / `pdt` | `America/Los_Angeles` | +| `aest` / `aedt` | `Australia/Sydney` | +| `acst` / `acdt` | `Australia/Adelaide` | | `awst` | `Australia/Perth` | -| `nzt` | `Pacific/Auckland` | -| `cet` | `Europe/Paris` | -| `eet` | `Europe/Helsinki` | +| `nzt` / `nzst` / `nzdt` | `Pacific/Auckland` | +| `cet` / `cest` | `Europe/Paris` | +| `eet` / `eest` | `Europe/Helsinki` | | `ist` | `Asia/Kolkata` | | `npt` | `Asia/Kathmandu` | | `jst` | `Asia/Tokyo` | diff --git a/packages/tempo/doc/6-utility-library/tempo.library.md b/packages/tempo/doc/6-utility-library/tempo.library.md index 86bf57e3..481f2a67 100644 --- a/packages/tempo/doc/6-utility-library/tempo.library.md +++ b/packages/tempo/doc/6-utility-library/tempo.library.md @@ -1,3 +1,5 @@ +![Tempo Library](/library-logo.svg) + # Tempo Library Functionality While Tempo is primarily a Date-Time engine, it relies on several custom utilities under the hood to handle data structures, deep cloning, and serialization safely. diff --git a/packages/tempo/img/library-logo.svg b/packages/tempo/img/library-logo.svg new file mode 100644 index 00000000..ec7fd7c4 --- /dev/null +++ b/packages/tempo/img/library-logo.svg @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/tempo/package.json b/packages/tempo/package.json index 03168496..8b40e042 100644 --- a/packages/tempo/package.json +++ b/packages/tempo/package.json @@ -1,6 +1,6 @@ { "name": "@magmacomputing/tempo", - "version": "3.11.0", + "version": "3.11.1", "engines": { "node": ">=20.0.0" }, @@ -120,100 +120,124 @@ "exports": { ".": { "types": "./dist/tempo.index.d.ts", - "import": "./dist/tempo.index.js" + "import": "./dist/tempo.index.js", + "default": "./dist/tempo.index.js" }, "./enums": { "types": "./dist/support/support.enum.d.ts", - "import": "./dist/support/support.enum.js" + "import": "./dist/support/support.enum.js", + "default": "./dist/support/support.enum.js" }, "./extend/*": { "types": "./dist/plugin/extend/extend.*.d.ts", - "import": "./dist/plugin/extend/extend.*.js" + "import": "./dist/plugin/extend/extend.*.js", + "default": "./dist/plugin/extend/extend.*.js" }, "./module/*": { "types": "./dist/module/module.*.d.ts", - "import": "./dist/module/module.*.js" + "import": "./dist/module/module.*.js", + "default": "./dist/module/module.*.js" }, "./term/*": { "types": "./dist/plugin/term/term.*.d.ts", - "import": "./dist/plugin/term/term.*.js" + "import": "./dist/plugin/term/term.*.js", + "default": "./dist/plugin/term/term.*.js" }, "./term/standard": { "types": "./dist/term/index.d.ts", - "import": "./dist/term/index.js" + "import": "./dist/term/index.js", + "default": "./dist/term/index.js" }, "./term/quarter": { "types": "./dist/term/term.quarter.d.ts", - "import": "./dist/term/term.quarter.js" + "import": "./dist/term/term.quarter.js", + "default": "./dist/term/term.quarter.js" }, "./term/season": { "types": "./dist/term/term.season.d.ts", - "import": "./dist/term/term.season.js" + "import": "./dist/term/term.season.js", + "default": "./dist/term/term.season.js" }, "./term/zodiac": { "types": "./dist/term/term.zodiac.d.ts", - "import": "./dist/term/term.zodiac.js" + "import": "./dist/term/term.zodiac.js", + "default": "./dist/term/term.zodiac.js" }, "./term/timeline": { "types": "./dist/term/term.timeline.d.ts", - "import": "./dist/term/term.timeline.js" + "import": "./dist/term/term.timeline.js", + "default": "./dist/term/term.timeline.js" }, "./plugin": { "types": "./dist/plugin/plugin.index.d.ts", - "import": "./dist/plugin/plugin.index.js" + "import": "./dist/plugin/plugin.index.js", + "default": "./dist/plugin/plugin.index.js" }, "./plugin/*": { "types": "./dist/plugin/*.d.ts", - "import": "./dist/plugin/*.js" + "import": "./dist/plugin/*.js", + "default": "./dist/plugin/*.js" }, "./duration": { "types": "./dist/module/module.duration.d.ts", - "import": "./dist/module/module.duration.js" + "import": "./dist/module/module.duration.js", + "default": "./dist/module/module.duration.js" }, "./mutate": { "types": "./dist/module/module.mutate.d.ts", - "import": "./dist/module/module.mutate.js" + "import": "./dist/module/module.mutate.js", + "default": "./dist/module/module.mutate.js" }, "./format": { "types": "./dist/module/module.format.d.ts", - "import": "./dist/module/module.format.js" + "import": "./dist/module/module.format.js", + "default": "./dist/module/module.format.js" }, "./ticker": { "types": "./dist/plugin/extend/extend.ticker.d.ts", - "import": "./dist/plugin/extend/extend.ticker.js" + "import": "./dist/plugin/extend/extend.ticker.js", + "default": "./dist/plugin/extend/extend.ticker.js" }, "./parse": { "types": "./dist/module/module.parse.d.ts", - "import": "./dist/module/module.parse.js" + "import": "./dist/module/module.parse.js", + "default": "./dist/module/module.parse.js" }, "./library": { "types": "./dist/library.index.d.ts", - "import": "./dist/library.index.js" + "import": "./dist/library.index.js", + "default": "./dist/library.index.js" }, "./core": { "types": "./dist/core.index.d.ts", - "import": "./dist/core.index.js" + "import": "./dist/core.index.js", + "default": "./dist/core.index.js" }, "./term": { "types": "./dist/plugin/term/term.index.d.ts", - "import": "./dist/plugin/term/term.index.js" + "import": "./dist/plugin/term/term.index.js", + "default": "./dist/plugin/term/term.index.js" }, "./bundle": { "types": "./dist/tempo.index.d.ts", - "import": "./dist/tempo.bundle.esm.js" + "import": "./dist/tempo.bundle.esm.js", + "default": "./dist/tempo.bundle.esm.js" }, "./global": { "types": "./dist/tempo.index.d.ts", "import": "./dist/tempo.bundle.js", - "script": "./dist/tempo.bundle.js" + "script": "./dist/tempo.bundle.js", + "default": "./dist/tempo.bundle.js" }, "./plugin-api": { "types": "./dist/plugin-api.index.d.ts", - "import": "./dist/plugin-api.index.js" + "import": "./dist/plugin-api.index.js", + "default": "./dist/plugin-api.index.js" }, "./support": { "types": "./dist/support/support.index.d.ts", - "import": "./dist/support/support.index.js" + "import": "./dist/support/support.index.js", + "default": "./dist/support/support.index.js" } }, "scripts": { @@ -234,9 +258,9 @@ "prebuild": "npm run build:version", "clean": "magma-cli rm dist && (node ../../node_modules/typescript-7/bin/tsc -b --clean || true)", "prepublishOnly": "if [ $(git rev-parse --abbrev-ref HEAD) != main ]; then echo 'ERROR: Must be on main branch to publish.'; exit 1; fi && if [ -z \"$TEMPO_LICENSE_PATH\" ] || [ ! -f \"$TEMPO_LICENSE_PATH\" ]; then echo '🚨 ERROR: TEMPO_LICENSE_PATH is missing or invalid. Cannot publish Premium build.'; exit 1; fi && npm run build", - "docs:api": "typedoc && typedoc --options typedoc.library.json", - "docs:dev": "npm run build && npm run docs:api && node bin/build-catalog.mjs && node bin/harvest-plugins.mjs && vitepress dev", - "docs:build": "npm run build && npm run docs:api && node bin/build-catalog.mjs && node bin/harvest-plugins.mjs && vitepress build", + "docs:api": "typedoc && typedoc --options typedoc.library.json && node bin/expand-typedoc.mjs", + "docs:dev": "npm run build && npm run docs:api && node bin/build-catalog.mjs && node bin/harvest-plugins.mjs && node bin/generate-llms-txt.mjs && vitepress dev", + "docs:build": "npm run build && npm run docs:api && node bin/build-catalog.mjs && node bin/harvest-plugins.mjs && node bin/generate-llms-txt.mjs && vitepress build", "docs:preview": "vitepress preview", "docs:push": "bash ./bin/push-docs.sh" }, @@ -260,9 +284,11 @@ "@rollup/plugin-terser": "^1.0.0", "@rollup/plugin-typescript": "^12.3.0", "javascript-obfuscator": "^5.4.3", - "magic-string": "^0.30.21", + "magic-string": "^1.1.1", + "mermaid": "^11.16.1", "typedoc": "^0.28.19", - "vitepress": "^1.6.4" + "vitepress": "^1.6.4", + "vitepress-plugin-mermaid": "^2.0.17" }, "directories": { "doc": "doc", diff --git a/packages/tempo/public/bundle.index.html b/packages/tempo/public/bundle.index.html index 3e9fa82d..14a726aa 100644 --- a/packages/tempo/public/bundle.index.html +++ b/packages/tempo/public/bundle.index.html @@ -99,6 +99,7 @@ margin-bottom: 5px; background: linear-gradient(135deg, #fff, #a5b4fc); -webkit-background-clip: text; + background-clip: text; -webkit-text-fill-color: transparent; letter-spacing: -0.5px; } diff --git a/packages/tempo/public/esm_core.index.html b/packages/tempo/public/esm_core.index.html index 97e48f97..a1d060d6 100644 --- a/packages/tempo/public/esm_core.index.html +++ b/packages/tempo/public/esm_core.index.html @@ -99,6 +99,7 @@ margin-bottom: 5px; background: linear-gradient(135deg, #fff, #a5b4fc); -webkit-background-clip: text; + background-clip: text; -webkit-text-fill-color: transparent; letter-spacing: -0.5px; } diff --git a/packages/tempo/public/esm_full.index.html b/packages/tempo/public/esm_full.index.html index 26b6fc93..514bfe61 100644 --- a/packages/tempo/public/esm_full.index.html +++ b/packages/tempo/public/esm_full.index.html @@ -99,6 +99,7 @@ margin-bottom: 5px; background: linear-gradient(135deg, #fff, #a5b4fc); -webkit-background-clip: text; + background-clip: text; -webkit-text-fill-color: transparent; letter-spacing: -0.5px; } diff --git a/packages/tempo/public/esm_sh.index.html b/packages/tempo/public/esm_sh.index.html new file mode 100644 index 00000000..bdc2ce4c --- /dev/null +++ b/packages/tempo/public/esm_sh.index.html @@ -0,0 +1,301 @@ + + + + + + + Tempo esm.sh Test + + + + + +
+
+ +
+
+

Tempo

+
esm.sh Smart CDN Test
+ +
+ import '@js-temporal/polyfill'; +import { Tempo } from '@magmacomputing/tempo'; + +const t = new Tempo('next friday'); +t.format('{mon} {dd:raw}'); +
+ +
+ Result +
Initializing Temporal...
+
+ + +
+
+ + + + + + \ No newline at end of file diff --git a/packages/tempo/public/library-logo.svg b/packages/tempo/public/library-logo.svg new file mode 100644 index 00000000..ec7fd7c4 --- /dev/null +++ b/packages/tempo/public/library-logo.svg @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/tempo/public/llms.txt b/packages/tempo/public/llms.txt new file mode 100644 index 00000000..5c80e229 --- /dev/null +++ b/packages/tempo/public/llms.txt @@ -0,0 +1,75 @@ +# Tempo: Immutable Date-Time Engine & AI Syntax Rules + +> Tempo is a lightweight, immutable JavaScript/TypeScript date-time library built around the native ECMAScript Temporal API proposal. It provides type-safe parsing, formatting, relative time arithmetic, and extensible layout matching across Browser and Node.js environments. + +## Core Architectural Rules & Philosophy +- **Temporal Engine**: Tempo expects native `Temporal` in modern runtimes or uses `@js-temporal/polyfill` when necessary. Never instantiate legacy JavaScript `Date`. +- **Strict Immutability**: `Tempo` instances are completely frozen. All mutating operations (`add`, `subtract`, `with`, `startOf`, `endOf`) return a brand-new `Tempo` object. +- **Zero-Cost Getter Proxies**: Properties like `.year`, `.month`, `.day`, `.hour`, `.minute`, `.second`, `.millisecond`, `.microsecond`, `.nanosecond` are live getters proxying the underlying `Temporal` state. +- **Plugin Architecture**: Core functions can be extended via `Tempo.extend(Plugin)`. License validation occurs via `Tempo.init(...)`. + +## Formatting & Parsing Tokens +| Token | Description | Sample Output | +| :--- | :--- | :--- | +| `{yy}` | Year (2 or 4 digits) | `2026`, `26` | +| `{mon}` | Month name (Full or Abbreviated) | `August`, `Aug` | +| `{mm}` | Month number (01-12) | `08` | +| `{dd}` | Day of month (01-31) | `04` | +| `{hh}` | Hour (00-24) | `15` | +| `{mi}` | Minute (00-59) | `30` | +| `{ss}` | Second (00-59) | `00` | +| `{wkd}` | Weekday name | `Tuesday`, `Tue` | +| `{tzd}` | Time zone offset / identifier | `Z`, `+10:00`, `Australia/Sydney` | +| `{yw}` | ISO Week-Year number | `W32` | +| `{unt}` | Time unit keyword | `day`, `month`, `year` | + +## Key Module Links +- [Full Documentation Concatenation](/llms-full.txt): Complete raw markdown documentation for RAG ingestion. +- [AI Plugin Capabilities](https://github.com/magmacomputing/magma/tree/main/packages/plugins/ai): Natural language parsing (`parseAI`), slot scheduling (`scheduleAI`), and recurrence rule resolution (`recurrenceAI`). +- [Layout Patterns & Regex Snippets](https://github.com/magmacomputing/magma/tree/main/packages/tempo/doc/3-extending-tempo/tempo.layout.md): Guide to writing regex layouts and snippets. +- [Plugin Development](https://github.com/magmacomputing/magma/tree/main/packages/tempo/doc/3-extending-tempo/tempo.plugin.md): Rules for extending Tempo via plugins. +- [Utility Library](https://github.com/magmacomputing/magma/tree/main/packages/tempo/doc/6-utility-library/tempo.library.md): Type detection, serialization (`stringify`/`objectify`), and `Pledge`. + +## Common Code Snippets + +### Quick Setup & Basic Usage +```typescript +import { Tempo } from '@magmacomputing/tempo'; + +// Create from current instant or ISO string +const t = Tempo.from('2026-08-04T15:30:00Z'); + +// Format using layout tokens +t.format('{mon} {dd}, {yy}'); // "August 04, 2026" + +// Immutably manipulate date +const nextWeek = t.add({ days: 7 }); +``` + +### Smart Parsing & Shorthand Expressions +```typescript +import { Tempo } from '@magmacomputing/tempo'; + +// 1. Natural language & relative shorthand expressions +const t1 = Tempo.from('next friday'); +const t2 = Tempo.from('start of month'); +const t3 = Tempo.from('+3 days'); + +// 2. Flexible date parsing +const t4 = Tempo.parse('2026-08-04T15:30:00+10:00'); +``` + +### Custom Layout Registration & Parsing +```typescript +import { Tempo } from '@magmacomputing/tempo'; + +// Register custom snippet & layout pattern via config +Tempo.config({ + layouts: { + us_date: '{mm}/{dd}/{yy}' + } +}); + +// Parse string using registered layout +const parsed = Tempo.parse('08/04/2026', 'us_date'); +``` diff --git a/packages/tempo/public/providers.v1.json b/packages/tempo/public/providers.v1.json new file mode 100644 index 00000000..73570985 --- /dev/null +++ b/packages/tempo/public/providers.v1.json @@ -0,0 +1,26 @@ +{ + "version": "1.0", + "updatedAt": "2026-08-05T00:00:00Z", + "providers": { + "groq": { + "url": "https://api.groq.com/openai/v1/chat/completions", + "model": "openai/gpt-oss-120b", + "tokenParam": "max_tokens" + }, + "openai": { + "url": "https://api.openai.com/v1/chat/completions", + "model": "gpt-5.4-mini", + "tokenParam": "max_completion_tokens" + }, + "gemini": { + "url": "https://generativelanguage.googleapis.com/v1beta/openai/chat/completions", + "model": "gemini-3.6-flash", + "tokenParam": "max_tokens" + }, + "mistral": { + "url": "https://api.mistral.ai/v1/chat/completions", + "model": "mistral-small-latest", + "tokenParam": "max_tokens" + } + } +} diff --git a/packages/tempo/public/script.index.html b/packages/tempo/public/script.index.html index 57f8ddd4..4ca30a7b 100644 --- a/packages/tempo/public/script.index.html +++ b/packages/tempo/public/script.index.html @@ -99,6 +99,7 @@ margin-bottom: 5px; background: linear-gradient(135deg, #fff, #a5b4fc); -webkit-background-clip: text; + background-clip: text; -webkit-text-fill-color: transparent; letter-spacing: -0.5px; } diff --git a/packages/tempo/src/engine/engine.composer.ts b/packages/tempo/src/engine/engine.composer.ts index 873e16d7..8586905f 100644 --- a/packages/tempo/src/engine/engine.composer.ts +++ b/packages/tempo/src/engine/engine.composer.ts @@ -1,4 +1,4 @@ -import { getTemporalIds } from '#library/temporal.library.js'; +import { getTemporalIds, toZonedDateTime } from '#library/temporal.library.js'; import { isInstant, isZonedDateTime, isPlainDate, isPlainDateTime } from '#library/assertion.library.js'; import type { TemporalObject, TypeValue } from '#library/type.library.js'; @@ -15,7 +15,7 @@ const UNIT_LOOKUP: Record = { ms: { scale: 1_000_000n, matchName: 'Milliseconds' }, us: { scale: 1_000n, matchName: 'Microseconds' }, ns: { scale: 1n, matchName: 'Nanoseconds' }, -}; +} /** * Logic to compose various input types into a Temporal.ZonedDateTime. @@ -41,6 +41,13 @@ export function compose( let dateTime: Temporal.ZonedDateTime | undefined; switch (type) { + case 'Temporal.ZonedDateTime': + case 'Temporal.Instant': + case 'Temporal.PlainDateTime': + case 'Temporal.PlainDate': + temporal = value; + break; + case 'Void': case 'Empty': case 'Undefined': @@ -50,14 +57,7 @@ export function compose( case 'String': try { - let zdt: Temporal.ZonedDateTime; - if (value.includes('[')) { - zdt = Temporal.ZonedDateTime.from(value); - } else if (/Z$|[+-]\d{2}:?\d{2}/i.test(value)) { - zdt = Temporal.Instant.from(value).toZonedDateTimeISO(tz); - } else { - zdt = Temporal.PlainDateTime.from(value, { overflow: 'constrain' }).toZonedDateTime(tz); - } + const zdt = toZonedDateTime(value, tz); timeZone = getTemporalIds(zdt)[0]; temporal = zdt; onResult?.({ type, value, match: 'iso8601' }); @@ -144,8 +144,6 @@ export function compose( break; } - - default: break; } diff --git a/packages/tempo/src/engine/engine.lexer.ts b/packages/tempo/src/engine/engine.lexer.ts index 65c4b7cc..c2ea0fd8 100644 --- a/packages/tempo/src/engine/engine.lexer.ts +++ b/packages/tempo/src/engine/engine.lexer.ts @@ -301,6 +301,12 @@ export function parseZone(groups: t.Groups, dateTime: Temporal.ZonedDateTime, co const brk = groups["brk"]?.replace(Match.zed, 'UTC'); let zone: string | undefined = brk || tzd; + if (zone) { + const match = zone.match(/^([+-])(\d{1,2})(?::?(\d{2}))?$/); + if (match) + zone = `${match[1]}${match[2].padStart(2, '0')}:${match[3] ?? '00'}`; + } + let cal = groups["cal"]; if (zone?.startsWith('u-ca=')) { cal = zone; @@ -309,8 +315,13 @@ export function parseZone(groups: t.Groups, dateTime: Temporal.ZonedDateTime, co const zdt = dateTime as any; if (zone && zone !== zdt.timeZoneId) { - if (config) config.timeZone = zone; - dateTime = zdt.toPlainDateTime().toZonedDateTime(zone); + const resolvedZone = enums.TIMEZONE[zone.toLowerCase()] ?? zone; + try { + dateTime = zdt.toPlainDateTime().toZonedDateTime(resolvedZone); + if (config) config.timeZone = resolvedZone; + } catch { + logWarn(`Unrecognized or invalid timezone identifier: '${zone}'`, config); + } } if (cal && cal !== (dateTime as any).calendarId) { const calendar = cal.startsWith('u-ca=') ? cal.substring(5) : cal; diff --git a/packages/tempo/src/interval.class.ts b/packages/tempo/src/interval.class.ts index f2520713..5e1df0df 100644 --- a/packages/tempo/src/interval.class.ts +++ b/packages/tempo/src/interval.class.ts @@ -1,4 +1,5 @@ import type { Tempo } from './tempo.class.js'; +import { isInteger } from '#library/assertion.library.js'; import { Immutable } from '#library/class.library.js'; export type TemporalPoint = Tempo | { epochNanoseconds: bigint }; @@ -7,7 +8,7 @@ export type TemporalPoint = Tempo | { epochNanoseconds: bigint }; function getNs(point: TemporalPoint | unknown): bigint { const ns = (point as any)?.epoch?.ns ?? (point as any)?.epochNanoseconds; - if (typeof ns === 'bigint') return ns; + if (isInteger(ns)) return ns; throw new TypeError('Invalid TemporalPoint: missing epoch.ns or epochNanoseconds'); } diff --git a/packages/tempo/src/library.index.ts b/packages/tempo/src/library.index.ts index 58656121..cc18a5f8 100644 --- a/packages/tempo/src/library.index.ts +++ b/packages/tempo/src/library.index.ts @@ -8,11 +8,14 @@ export { Pledge } from '#library/pledge.class.js'; export * as cipher from '#library/cipher.library.js'; export * as webToken from '#library/webtoken.library.js'; export { enumify, type Enum } from '#library/enumerate.library.js'; -export { proxify } from '#library/proxy.library.js'; -export { stringify, objectify, cloneify } from '#library/serialize.library.js'; -export { isObject, isFunction, isDefined, isUndefined, isEmpty, isNumber, isNumeric, isFiniteNumber } from '#library/assertion.library.js'; export { fetchRequest, fetchHead, HttpError } from '#library/request.library.js'; export { asArray } from '#library/coercion.library.js'; -export { instant, normaliseFractionalDurations } from '#library/temporal.library.js'; +export { stringify, objectify, cloneify } from '#library/serialize.library.js'; + +export * from '#library/proxy.library.js'; +export * from '#library/assertion.library.js'; +export * from '#library/temporal.library.js'; +export * from '#library/calendar.library.js'; +export * from '#library/recurrence.library.js'; export type { OwnOf, KeyOf, ValueOf, EntryOf } from '#library/type.library.js'; diff --git a/packages/tempo/src/module/module.parse.ts b/packages/tempo/src/module/module.parse.ts index 57a9ddd6..e04ea7b4 100644 --- a/packages/tempo/src/module/module.parse.ts +++ b/packages/tempo/src/module/module.parse.ts @@ -17,7 +17,7 @@ import { getRange, getTermRange } from '../plugin/term/term.util.js'; import { defineInterpreterModule } from '../plugin/plugin.util.js'; import type { Range, ResolvedRange } from '../plugin/term/term.type.js'; -import { sym, isTempo, TermError, getRuntime, Match, TempoError, $setEvents, $setPeriods, markConfig, setPatterns, init, extendState, enums } from '#tempo/support'; +import { sym, isTempo, TermError, getRuntime, Match, TempoError, $setEvents, $setPeriods, markConfig, setPatterns, init, extendState, enums, Token, Snippet } from '#tempo/support'; import { setProperty, logError, logDebug } from '#tempo/support/support.util.js'; import * as t from '../tempo.type.js'; @@ -260,6 +260,17 @@ const _ParseEngine = { let guard = (TempoClass as any)?.[sym.$guard]?.test(trim) ?? true; + if (!guard) { + const snip = Object.assign({}, Snippet, state.parse?.snippet); + const sep = snip[Token.sep]?.source ?? Match.separator.source; + const mm = snip[Token.mm]?.source ?? Snippet[Token.mm].source; + const dd = (snip[Token.dd]?.source ?? Snippet[Token.dd].source) + .replace('{ord}', snip[Token.ord]?.source ?? Snippet[Token.ord].source); + const datePrefix = new RegExp(`^\\d{4}${sep}(?:${mm})${sep}(?:${dd})(?:\\b|[T\\s]|$)`, 'i'); + if (datePrefix.test(trim)) + guard = true; + } + // 🛡️ Bypass the strict global guard if the current instance is using localized parsing if (!guard && (!isEmpty(state.parse.monthMap) || !isEmpty(state.parse.weekdayMap))) guard = true; diff --git a/packages/tempo/src/plugin/extend/extend.recurrence.ts b/packages/tempo/src/plugin/extend/extend.recurrence.ts new file mode 100644 index 00000000..d06b4b84 --- /dev/null +++ b/packages/tempo/src/plugin/extend/extend.recurrence.ts @@ -0,0 +1,25 @@ +import { Tempo } from '../../tempo.class.js'; +import { getNextRRuleEpoch, isRRuleString, isString } from '#library'; + +declare module '../../tempo.class.js' { + interface Tempo { + /** + * Computes the next occurrence of a recurrence rule (RRULE string) after this instant. + * + * @param rrule - The RFC 5545 RRULE string or object with an rrule property + * @returns A new Tempo instance at the next occurrence, or null if no further occurrences exist + */ + nextOccurrence(rrule: string | { rrule: string }): Tempo | null; + } +} + +Tempo.prototype.nextOccurrence = function (this: Tempo, rrule: string | { rrule: string }): Tempo | null { + const rruleStr = isString(rrule) ? rrule : rrule.rrule; + const nextMs = getNextRRuleEpoch(rruleStr, this.epoch.ms); + if (nextMs === null) return null; + const epochMs = Temporal.Instant.fromEpochMilliseconds(nextMs); + + return new (this.constructor as typeof Tempo)(epochMs, this.config); +} + +export { isRRuleString, getNextRRuleEpoch }; diff --git a/packages/tempo/src/plugin/license/license.manager.ts b/packages/tempo/src/plugin/license/license.manager.ts index 6803717b..6c4c9188 100644 --- a/packages/tempo/src/plugin/license/license.manager.ts +++ b/packages/tempo/src/plugin/license/license.manager.ts @@ -194,8 +194,9 @@ export function warnIfExpiringSoon(license: Internal.LicenseState, config: any): } export function validateLicenseState(license: Internal.LicenseState, jws: Pledge) { + const runtime = getRuntime(); import('#tempo/license') - .then(m => new m.Validator(license.key!).verify()) + .then(m => new m.Validator(license.key!, runtime.state?.config).verify()) .then(res => jws.resolve(res)) .catch(err => { if (license.jws === jws) diff --git a/packages/tempo/src/plugin/license/license.validator.ts b/packages/tempo/src/plugin/license/license.validator.ts index 38f0510e..896da4bc 100644 --- a/packages/tempo/src/plugin/license/license.validator.ts +++ b/packages/tempo/src/plugin/license/license.validator.ts @@ -10,8 +10,8 @@ import { sym } from '../../support/support.symbol.js'; */ export class Validator { - constructor(public key: string) { - logWarn('Tempo Community Edition: License keys are ignored. Premium plugins cannot be validated without the cryptographic engine.'); + constructor(public key: string, public config?: any) { + logWarn('Tempo Community Edition: License keys are ignored. Premium plugins cannot be validated without the cryptographic engine.', config); } async verify() { diff --git a/packages/tempo/src/support/support.cache.ts b/packages/tempo/src/support/support.cache.ts index d863fa1c..dc9ed1c6 100644 --- a/packages/tempo/src/support/support.cache.ts +++ b/packages/tempo/src/support/support.cache.ts @@ -186,6 +186,20 @@ export class BoundedCache extends Map { return super[Symbol.iterator](); } + /** + * Returns a plain key-value object of all active non-expired cache entries. + * Filters out non-string keys to prevent lossy key conversions or collisions (e.g. numeric 1 vs string "1"). + */ + toJSON(): Record { + this.evictExpired(); + const stringEntries: [string, V][] = []; + for (const [k, v] of super.entries()) { + if (typeof k === 'string') + stringEntries.push([k, v]); + } + return Object.fromEntries(stringEntries) as Record; + } + static fromEntries(entries: Iterable, maxSize = 1000, ttl = 24 * 60 * 60 * 1000): BoundedCache { const cache = new BoundedCache(maxSize, ttl); for (const [k, v] of entries) { @@ -238,6 +252,10 @@ export function createCacheFacade(getState: () => t.Internal.State) { getState().cache.set(normalized, String(v)); } return this; + }, + toJSON() { + return getState().cache.toJSON(); } }); } + diff --git a/packages/tempo/src/support/support.default.ts b/packages/tempo/src/support/support.default.ts index 9a4669ec..bd5e8e9a 100644 --- a/packages/tempo/src/support/support.default.ts +++ b/packages/tempo/src/support/support.default.ts @@ -3,7 +3,7 @@ import { secure, proxify } from '#library/proxy.library.js'; import { getDateTimeFormat } from '#library/international.library.js'; import { LOG } from '#library/logger.class.js'; -import { NUMBER, MODE, MONTH_DAY } from './support.enum.js'; +import { NUMBER, TIMEZONE, MODE, MONTH_DAY } from './support.enum.js'; import { Token } from './support.symbol.js'; import type { Options, AliasContext, IntlOptions } from '../tempo.type.js'; @@ -34,6 +34,7 @@ export const Match = proxify({ /** slick shorthand-shifter (e.g. #qtr.>2q2) */ shorthand: /(?:(?:#[\w]+|[\w]+)\.(?:[\+\-\<\>]=?)?(?:[0-9]+)?(?:[\w]*))/, /** anchored version for shifter resolution */ slick: /^(?#[\w]+|[\w]+)\.(?[\+\-\<\>\=]=?)?(?-?[0-9]+)?(?[\w]*)$/, /** extracted value-only version of a slick shifter */ slickValue: /^(?[\+\-\<\>\=]=?)?(?-?[0-9]+)?(?[\w]*)$/, + /** numeric timezone offset (e.g. +10:00, +1000, -05:00, -0500, GMT+10, UTC-5) */ offset: /(?:[+-]\d{2}:\d{2}|[+-]\d{4}|(?<=\s|T|GMT|UTC)\s*[+-]\d{1,2}(?::?\d{2})?)/, /** escape special regex characters in a string */ escape: (str: string) => String(str).replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), /** escape only dangerous quantifiers and anchors to prevent backtracking/injection while allowing basic regex */ safeAlias: (str: string) => String(str).replace(/[*+{}!^$\\]/g, '\\$&'), @@ -67,7 +68,7 @@ export const Snippet = looseIndex()({ [Token.mer]: /(\s*(?am|pm))/, // meridiem suffix (am,pm) [Token.sfx]: /((?:{sep}+|T)({tm}){tzd}?)/, // time-pattern suffix 'T {tm} Z'; NOTE: {tm} resolves via Layout fallback in compileRegExp (cross-registry dependency: Snippet → Layout) [Token.wkd]: /(?Mon(?:day)?|Tue(?:sday)?|Wed(?:nesday)?|Thu(?:rsday)?|Fri(?:day)?|Sat(?:urday)?|Sun(?:day)?)/, // day-name (abbrev or full) - [Token.tzd]: /(?Z|(?:\+(?:(?:0[0-9]|1[0-3]):?[0-5][0-9]|14:?00)|-(?:(?:0[0-9]|1[0-1]):?[0-5][0-9]|12:?00)))/, // time-zone offset +14:00 to -12:00; colon optional throughout (including boundary values UTC+14 and UTC-12) + [Token.tzd]: new RegExp(`\\s*(?:(?:GMT|UTC)(?=\\s*[+-]))?\\s*(?Z|(?:${Object.keys(TIMEZONE).map(w => Match.escape(w.toUpperCase())).join('|')})|${Match.offset.source})`, 'i'), // time-zone offset or abbreviation with optional GMT/UTC prefix (e.g. GMT+10, UTC+10:00, +10:00, AEST, PST) [Token.nbr]: new RegExp(`(?[0-9]+|${Object.keys(NUMBER).map(w => Match.escape(w)).join('|')})`), // modifier count; number-word keys are regex-escaped at construction time (setPatterns() also re-escapes, but defence-in-depth) [Token.afx]: new RegExp(`((s)? (?${Match.modifier.source}))?{sep}?`), // affix optional plural 's' and (ago|hence) [Token.mod]: new RegExp(`((?${Match.modifier.source})? *)`), @@ -189,7 +190,7 @@ export type SLICK_KEYS = typeof SLICK_KEYS /** @internal Tempo Master Guard list */ export const Guard = [ - 'am', 'pm', 'ago', 'hence', 'this', 'next', 'prev', 'last', 'from', 'now', 'today', 'yesterday', 'tomorrow', 'start', 'mid', 'end', + 'am', 'pm', 'gmt', 'utc', 'ago', 'hence', 'this', 'next', 'prev', 'last', 'from', 'now', 'today', 'yesterday', 'tomorrow', 'start', 'mid', 'end', 'year', 'month', 'week', 'day', 'hour', 'minute', 'second', 'millisecond', 'microsecond', 'nanosecond', 'years', 'months', 'weeks', 'days', 'hours', 'minutes', 'seconds', 'milliseconds', 'microseconds', 'nanoseconds', 'st', 'nd', 'rd', 'th', diff --git a/packages/tempo/src/support/support.enum.ts b/packages/tempo/src/support/support.enum.ts index 5504ba85..9d234ecb 100644 --- a/packages/tempo/src/support/support.enum.ts +++ b/packages/tempo/src/support/support.enum.ts @@ -47,17 +47,27 @@ export const DEFAULTS = { }, TIMEZONE: { 'utc': 'UTC', - 'gmt': 'Europe/London', + 'gmt': 'UTC', 'est': 'America/New_York', + 'edt': 'America/New_York', 'cst': 'America/Chicago', + 'cdt': 'America/Chicago', 'mst': 'America/Denver', + 'mdt': 'America/Denver', 'pst': 'America/Los_Angeles', + 'pdt': 'America/Los_Angeles', 'aest': 'Australia/Sydney', + 'aedt': 'Australia/Sydney', 'acst': 'Australia/Adelaide', + 'acdt': 'Australia/Adelaide', 'awst': 'Australia/Perth', 'nzt': 'Pacific/Auckland', + 'nzst': 'Pacific/Auckland', + 'nzdt': 'Pacific/Auckland', 'cet': 'Europe/Paris', + 'cest': 'Europe/Paris', 'eet': 'Europe/Helsinki', + 'eest': 'Europe/Helsinki', 'ist': 'Asia/Kolkata', 'npt': 'Asia/Kathmandu', 'jst': 'Asia/Tokyo', diff --git a/packages/tempo/src/support/support.init.ts b/packages/tempo/src/support/support.init.ts index 34cddf55..569a13ec 100644 --- a/packages/tempo/src/support/support.init.ts +++ b/packages/tempo/src/support/support.init.ts @@ -140,6 +140,7 @@ export function init(options: t.Options = {}, isGlobal = true, baseState?: t.Int sphere: getHemisphere(timeZone), scope: 'global', catch: options.catch ?? false, + silent: options.silent ?? false, intl: {}, }); Object.defineProperty(state.config, 'get', { value: function (key: string) { return this[key] }, enumerable: false, writable: true, configurable: true }); @@ -151,6 +152,7 @@ export function init(options: t.Options = {}, isGlobal = true, baseState?: t.Int setProperties(state.config, { scope: 'local', catch: options.catch ?? (baseState.config as any).catch ?? false, + silent: options.silent ?? (baseState.config as any).silent ?? false, intl: Object.create((baseState.config as any).intl || {}), }); Object.defineProperty(state.config, 'get', { value: function (key: string) { return this[key] }, enumerable: false, writable: true, configurable: true }); @@ -171,11 +173,14 @@ export function init(options: t.Options = {}, isGlobal = true, baseState?: t.Int formats: enumify(STATE.FORMAT, false), sphere: getHemisphere(timeZone), scope: 'local', + silent: options.silent ?? false, intl: {}, }); Object.defineProperty(state.config, 'get', { value: function (key: string) { return this[key] }, enumerable: false, writable: true, configurable: true }); if (isDefined(options.catch)) setProperty(state.config, 'catch', options.catch); + if (isDefined(options.silent)) + setProperty(state.config, 'silent', options.silent); } // 3. Initialize registries that need objects @@ -343,6 +348,10 @@ export function extendState(state: t.Internal.State, options: t.Options): boolea setProperty(state.config, 'catch', Boolean(arg.value)); break; + case 'silent': + setProperty(state.config, 'silent', Boolean(arg.value)); + break; + case 'pivot': { const pivot = parseInt(String(arg.value)); state.parse.pivot = (Number.isFinite(pivot) && pivot >= 0 && pivot <= 99) ? pivot : Default.pivot!; diff --git a/packages/tempo/src/tempo.class.ts b/packages/tempo/src/tempo.class.ts index 9b2aa8ac..c939ae15 100644 --- a/packages/tempo/src/tempo.class.ts +++ b/packages/tempo/src/tempo.class.ts @@ -1732,6 +1732,9 @@ export class Tempo { logError(msg, this.#local.config); return undefined as any; } + if (isObject(res) && 'value' in res) { + return (res as any).value ?? (undefined as any); + } return res; } @@ -1836,7 +1839,6 @@ export namespace Tempo { export type Set = t.MutateSet; export type Add = t.MutateAdd; - export type Formats = t.Formats; export type Format = t.Format; export type FormatRegistry = t.FormatRegistry; diff --git a/packages/tempo/src/tempo.version.ts b/packages/tempo/src/tempo.version.ts index 89eb178b..04e3729c 100644 --- a/packages/tempo/src/tempo.version.ts +++ b/packages/tempo/src/tempo.version.ts @@ -5,4 +5,4 @@ * ⚠️ This file is auto-updated by `npm run build:version` (see `bin/update-version.mjs`). * Do NOT edit manually — your changes will be overwritten on the next build. */ -export const TEMPO_VERSION = '3.11.0'; +export const TEMPO_VERSION = '3.11.1'; diff --git a/packages/tempo/test/discrete/standalone_parse.test.ts b/packages/tempo/test/discrete/standalone_parse.test.ts index f42d05d1..e576ac11 100644 --- a/packages/tempo/test/discrete/standalone_parse.test.ts +++ b/packages/tempo/test/discrete/standalone_parse.test.ts @@ -59,3 +59,72 @@ test('standalone parse: timezone lookup', () => { const zdt = parse('2025-05-20 10:00', { timeZone: 'pst' }); expect(zdt.timeZoneId).toBe('America/Los_Angeles'); }); + +test('standalone parse: human date string with GMT/UTC timezone offset (e.g. Aug 6, 16:16 GMT+10)', () => { + const zdt = parse('Aug 6, 16:16 GMT+10'); + expect(zdt.month).toBe(8); + expect(zdt.day).toBe(6); + expect(zdt.hour).toBe(16); + expect(zdt.minute).toBe(16); + expect(zdt.offset).toBe('+10:00'); + + const zdt2 = parse('Aug 6, 16:16 UTC-5'); + expect(zdt2.month).toBe(8); + expect(zdt2.day).toBe(6); + expect(zdt2.hour).toBe(16); + expect(zdt2.minute).toBe(16); + expect(zdt2.offset).toBe('-05:00'); + + const t = new Tempo('Aug 6, 16:16 GMT+10'); + expect(t.mm).toBe(8); + expect(t.dd).toBe(6); + expect(t.hh).toBe(16); + expect(t.mi).toBe(16); + + const zdtAest = parse('August 6, 16:16 AEST'); + expect(zdtAest.month).toBe(8); + expect(zdtAest.day).toBe(6); + expect(zdtAest.hour).toBe(16); + expect(zdtAest.minute).toBe(16); + expect(zdtAest.timeZoneId).toBe('Australia/Sydney'); + expect(zdtAest.offset).toBe('+10:00'); + + const zdtPst = parse('Aug 6, 16:16 PST'); + expect(zdtPst.month).toBe(8); + expect(zdtPst.day).toBe(6); + expect(zdtPst.hour).toBe(16); + expect(zdtPst.minute).toBe(16); + expect(zdtPst.timeZoneId).toBe('America/Los_Angeles'); + expect(zdtPst.offset).toBe('-07:00'); + + const zdt3 = parse('Aug 6, 16:16 GMT+5:30'); + expect(zdt3.month).toBe(8); + expect(zdt3.day).toBe(6); + expect(zdt3.hour).toBe(16); + expect(zdt3.minute).toBe(16); + expect(zdt3.offset).toBe('+05:30'); + + const zdt4 = parse('Aug 6, 16:16 UTC-8:30'); + expect(zdt4.month).toBe(8); + expect(zdt4.day).toBe(6); + expect(zdt4.hour).toBe(16); + expect(zdt4.minute).toBe(16); + expect(zdt4.offset).toBe('-08:30'); +}); + +test('standalone parse: bare GMT resolves to fixed UTC with +00:00 offset regardless of season', () => { + const zdtSummer = parse('2026-07-15 12:00 GMT'); + expect(zdtSummer.month).toBe(7); + expect(zdtSummer.day).toBe(15); + expect(zdtSummer.hour).toBe(12); + expect(zdtSummer.offset).toBe('+00:00'); + expect(zdtSummer.timeZoneId).toBe('UTC'); + + const zdtLower = parse('2026-07-15 12:00 gmt'); + expect(zdtLower.offset).toBe('+00:00'); + expect(zdtLower.timeZoneId).toBe('UTC'); + + const t = new Tempo('2026-07-15 12:00 GMT'); + expect(t.toDateTime().offset).toBe('+00:00'); + expect(t.tz).toBe('UTC'); +}); diff --git a/packages/tempo/test/plugins/extend.recurrence.test.ts b/packages/tempo/test/plugins/extend.recurrence.test.ts new file mode 100644 index 00000000..80d6fa25 --- /dev/null +++ b/packages/tempo/test/plugins/extend.recurrence.test.ts @@ -0,0 +1,35 @@ +import { Tempo } from '#tempo'; +import '#tempo/plugin/extend/extend.recurrence.js'; + +describe('extend.recurrence', () => { + test('Tempo.prototype.nextOccurrence returns next date matching RRULE string', () => { + const start = new Tempo('2026-08-07T00:00:00.000Z', { timeZone: 'UTC' }); + const next = start.nextOccurrence('FREQ=DAILY;INTERVAL=1'); + expect(next).not.toBeNull(); + expect(next!.epoch.ms).toBe(Date.UTC(2026, 7, 8)); + expect(next!.format('{yyyy}-{mm}-{dd}')).toBe('2026-08-08'); + }); + + test('Tempo.prototype.nextOccurrence accepts object with rrule property', () => { + const start = new Tempo('2026-08-07T00:00:00.000Z', { timeZone: 'UTC' }); + const next = start.nextOccurrence({ rrule: 'FREQ=DAILY;INTERVAL=2' }); + expect(next).not.toBeNull(); + expect(next!.epoch.ms).toBe(Date.UTC(2026, 7, 9)); + expect(next!.format('{yyyy}-{mm}-{dd}')).toBe('2026-08-09'); + }); + + test('Tempo.prototype.nextOccurrence returns null when RRULE UNTIL is expired', () => { + const start = new Tempo('2026-08-07T00:00:00.000Z', { timeZone: 'UTC' }); + const next = start.nextOccurrence('FREQ=DAILY;UNTIL=20260801'); + expect(next).toBeNull(); + }); + + test('Tempo.prototype.nextOccurrence preserves sandbox subclass and respects non-ms timeStamp configurations', () => { + const Sandbox = Tempo.create({ timeZone: 'UTC', timeStamp: 'ss' }); + const start = new Sandbox('2026-08-07T00:00:00.000Z'); + const next = start.nextOccurrence('FREQ=DAILY;INTERVAL=1'); + expect(next).toBeInstanceOf(Sandbox); + expect(next!.epoch.ms).toBe(Date.UTC(2026, 7, 8)); + expect(next!.config.timeStamp).toBe('ss'); + }); +}); diff --git a/packages/tempo/test/support/cache.test.ts b/packages/tempo/test/support/cache.test.ts index 2f2c7f46..35ff232c 100644 --- a/packages/tempo/test/support/cache.test.ts +++ b/packages/tempo/test/support/cache.test.ts @@ -75,6 +75,23 @@ describe('Tempo Core Caching Architecture', () => { expect(cache.has('k2')).toBe(false); expect(cache.has('k3')).toBe(true); }); + it('should serialize to JSON object via toJSON()', () => { + const cache = new BoundedCache(10, 10000); + cache.set('k1', 'v1'); + cache.set('k2', 'v2'); + + expect(cache.toJSON()).toEqual({ k1: 'v1', k2: 'v2' }); + expect(JSON.stringify(cache)).toBe('{"k1":"v1","k2":"v2"}'); + }); + + it('should prevent lossy key conversion and silent key merging in toJSON() for distinct numeric and string keys', () => { + const cache = new BoundedCache(10, 10000); + cache.set(1, 'numeric_one'); + cache.set('1', 'string_one'); + + const json = cache.toJSON(); + expect(json).toEqual({ '1': 'string_one' }); + }); }); describe('Tempo.CACHE Enum & Facade', () => { @@ -84,15 +101,19 @@ describe('Tempo Core Caching Architecture', () => { expect(Tempo.CACHE.Refresh).toBe('refresh'); }); - it('should expose normalized Tempo.cache facade methods', () => { + it('should expose normalized Tempo.cache facade methods and serialize via toJSON()', () => { Tempo.cache.set(' MY_TERM ', '2026-05-10'); expect(Tempo.cache.has('my_term')).toBe(true); expect(Tempo.cache.get('my_term')).toBe('2026-05-10'); + expect(Tempo.cache.toJSON()).toEqual({ my_term: '2026-05-10' }); + expect(JSON.stringify(Tempo.cache)).toBe('{"my_term":"2026-05-10"}'); + Tempo.cache.delete('MY_TERM'); expect(Tempo.cache.has('my_term')).toBe(false); }); + it('should resolve static glossary terms instantly and record glossary source in parse result', () => { Tempo.cache.setStatic('eoy_party', '2026-12-31T18:00:00');