diff --git a/.github/workflows/_acceptance.yml b/.github/workflows/_acceptance.yml index d15c07982d..5c63637bc8 100644 --- a/.github/workflows/_acceptance.yml +++ b/.github/workflows/_acceptance.yml @@ -12,7 +12,7 @@ jobs: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - - uses: jdx/mise-action@fc42bcc7ddbf51124c30f95a877a0c5168a3bfde # v5.2.1 + - uses: jdx/mise-action@6be3c7ed4c21d4f76b208d351d95aac273ebb76d # v5.2.4 - uses: actions/setup-java@b843ebf9b913146b38da2eaa7aeddf76074ae1ad # v6.1.0 with: distribution: temurin @@ -33,7 +33,7 @@ jobs: with: persist-credentials: false - uses: ./.github/actions/setup-frontend - - uses: jdx/mise-action@fc42bcc7ddbf51124c30f95a877a0c5168a3bfde # v5.2.1 + - uses: jdx/mise-action@6be3c7ed4c21d4f76b208d351d95aac273ebb76d # v5.2.4 - uses: actions/setup-java@b843ebf9b913146b38da2eaa7aeddf76074ae1ad # v6.1.0 with: distribution: temurin diff --git a/.github/workflows/_agent.yml b/.github/workflows/_agent.yml index bec90c3e53..10407fbf91 100644 --- a/.github/workflows/_agent.yml +++ b/.github/workflows/_agent.yml @@ -17,7 +17,7 @@ jobs: with: persist-credentials: false - name: Setup Python 3.11 - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0 with: python-version: '3.11' - name: Install locked uv version diff --git a/.github/workflows/_backend.yml b/.github/workflows/_backend.yml index 7227560c76..b55e012843 100644 --- a/.github/workflows/_backend.yml +++ b/.github/workflows/_backend.yml @@ -244,7 +244,7 @@ jobs: persist-credentials: false - name: Setup mise for repository toolchain - uses: jdx/mise-action@fc42bcc7ddbf51124c30f95a877a0c5168a3bfde # v5.2.1 + uses: jdx/mise-action@6be3c7ed4c21d4f76b208d351d95aac273ebb76d # v5.2.4 - name: Setup JDK 17 for registry contracts uses: actions/setup-java@b843ebf9b913146b38da2eaa7aeddf76074ae1ad # v6.1.0 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2ae042b298..b28fa25111 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -56,7 +56,7 @@ jobs: with: persist-credentials: false - name: Setup Python - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0 with: python-version: '3.11' - name: Install pinned policy dependency diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index 1845e187bd..79479d93e7 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -65,7 +65,7 @@ jobs: fi echo "version=$VERSION" >> "$GITHUB_OUTPUT" - name: Log in to GHCR - uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4 + uses: docker/login-action@81c6f93d4bc991e510b342a00b419b3bb4efc5c6 # v4 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} @@ -211,7 +211,7 @@ jobs: with: cosign-release: v2.5.3 - name: Log in to GHCR - uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4 + uses: docker/login-action@81c6f93d4bc991e510b342a00b419b3bb4efc5c6 # v4 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index 9b97d281fe..d253ce62fb 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -166,6 +166,27 @@ HTTP 存活不代表 Dubbo 接口已被消费者发现。Admin readiness 额外 有界发现期限内检查 ProblemFactsPort 与 SubmissionUserReadPort,成功后才输出 launcher 使用的就绪标记;超时则启动失败。检查只读取发现状态,不调用业务 RPC 或重试写入。 launcher 记录本次启动前日志偏移,旧标记不会被当成本次成功。 +Submission 门禁另外按固定接口别名记录最终诊断,仅含固定状态/类型枚举、有界耗时和目录计数。 +诊断不调用 `isAvailable()` 或业务 RPC:Dubbo availability 检查可能触发失效/重连, +只保留原门禁的 availability 判据,诊断仅读取已知目录的元数据。目录计数范围为 0–10000, +缺失、超限和读取异常分别标为 UNKNOWN、OVERFLOW、ERROR;未知实现标为 UNSUPPORTED, +不读取其目录字段。计数不是原子快照,正数不等于 ready,也不能据其数量推导等式。 +诊断独立于成功判定,不改变发现期限或重试策略。安装报告保留日志尾部最多 16 条 +此类观测,以及 launcher 最后一次 readiness/PM2 快照,不上传原始日志或环境。 +这些历史观测没有按进程代次关联,也不能证明 App 与 Submission 启动的因果顺序; +自动重启后 ready 不能作为首次启动失败已修复的证据。 +真实安装验收在 launcher 返回后要求本次隔离 PM2 home 内的 Submission 已 ready、online、 +restart count 为 0;缺失、无效或其他运行的快照也会失败,不能靠自动重启通过。 +App readiness 另外读取 `SubmissionUserQueryPort` 与 `AccountQueryService` 的相同目录元数据, +仅在两种固定接口别名的观测变化时记录,不调用 availability、业务 RPC 或读取 URL,也不改变 +原有 HTTP readiness 判定。安装报告独立保留 App 日志尾部最多 16 条此类观测;它们不是跨进程 +调用链,不能把历史 Auth 查询异常自动归因为另一次 Submission 请求的失败。 +App 还单独读取现有 application child 的目录,以及已有 listener 的应用数、已收到实例事件的应用数和实例总数, +同样只输出固定枚举和有界数量,不读取或输出应用名称、实例内容、URL,不创建或刷新发现订阅。 +选中目录与 application child 分开记录,不能把空 DYNAMIC 目录直接认定为应用级实例缺失。 +验收退出时只在已经进入启动阶段后读取一次 Admin 原有 readiness HTTP 响应,报告仅保留固定组件的 UP/DOWN/UNKNOWN, +此观察晚于 launcher 快照,不改变原退出码、就绪条件或超时。 + 测试账号仅由 disposable dev bootstrap 与 Auth registration 创建。Manager 的直接权限 通过真实 Auth API 临时授予并过期,不改全局 role policy;Console participant 保持 USER。 @@ -189,6 +210,18 @@ ACCEPTANCE_DISPOSABLE=true bash scripts/ci/business-acceptance.sh `dev-lite` 是兼容默认:按 `auth → admin → app → notification → submission` 应用 Owner migrations,启动六个后端(含 Judge)并使用数据库 Search;不启动 Search worker,默认也不启动两个前端。`dev-lite` 与 `dev-full` 都要求 `APP_SUBMISSION_ROUTING_MODE=remote` 和 `SUBMISSION_CUTOVER_COMPLETE=true`;marker 未完成时 `up.sh` 会 fail closed。 +当所选 PM2 集合同时包含 App 与 Submission/Judge 时,`up.sh` 先启动其余所选进程并等待 App 的 `/api/v1/app/health/ready`,再启动 Submission/Judge;不含 App 的部分选择不额外启动或等待 App。App 超时且选择了 Submission 时,launcher 非零退出,不启动延后的 Submission/Judge,也不保存本次 PM2 集合;已经运行的进程不会因此被停止。仅延后 Judge 的选择保留原有自动重连回退。此顺序与生产 Compose 的 Submission → App healthy 依赖对齐,但 App HTTP readiness 不证明 Dubbo provider 已注册,Submission discovery gate 及其超时保持不变。Submission/Judge 的就绪日志偏移在各自启动前记录,忽略旧 banner 及等待 App 期间旧进程产生的 banner。 + +本次 launcher 已为管理员 bootstrap 启动的 Auth 会被复用,不在最终批次中再次重启。所选集合同时包含 Auth 与 App 时,先启动或复用 Auth,并重新确认 `/api/v1/auth/health/ready`,再启动 App;Auth 超时则非零退出,不启动 App 及其延后消费者。未选择 Auth 时不补起或等待 Auth,Auth-only 的 bootstrap 路径不发出空的 PM2 启动批次。HTTP readiness 不是 Dubbo 服务发现证明,最终仍须通过真实业务验收。 + +App、Judge 的 Submission 引用以及 Admin 的 App/Submission 引用通过 Dubbo `providedBy` 显式声明已知 Owner 应用, +避免消费者先启动时仅依赖首次接口到应用的映射通知。地址仍由 Nacos 实例发现提供;group、version、 +查询/写入重试与超时、身份校验、`APPLICATION_FIRST` 和所有 readiness 门禁保持不变。 +默认目标分别是 `backend-app` 与 `backend-submission`。独立部署若覆盖了 provider 的 `DUBBO_APPLICATION_NAME`, +需在相应消费者设置 `ulticode.rpc.providers.app` 或 `ulticode.rpc.providers.submission`;对应环境变量为 +`ULTICODE_RPC_PROVIDERS_APP`、`ULTICODE_RPC_PROVIDERS_SUBMISSION`,值必须匹配实际 provider 应用名, +不能使用消费者自己的 `DUBBO_APPLICATION_NAME`。标准 PM2/Compose 应用名无需额外设置。Judge 的执行栅栏与判题结果回写均使用同一 Submission 应用声明,写入仍为零自动重试。 + 按开发场景选择最小服务集合(`up.sh`/`stop.sh`/`doctor.sh` 共用同一 resolver,见 `scripts/dev/devstack-manifest.sh`): ```bash @@ -347,6 +380,8 @@ Management forms use patched Zod 4 through `src/lib/toTypedSchema.ts`, which pre vee-validate 4 form contract for parsed values, defaults, field errors and required metadata. The published Zod 3 adapter is not used; production dependency audits retain the high-severity gate. +Nacos 客户端使用官方 `pure` classifier,并显式引入同版本 `nacos-api`、`nacos-common`,由共享 RPC 模块提供给各 Owner/Worker;排除 Dubbo 引入的默认 shaded client,避免其内嵌依赖绕过 Maven 的安全版本管理。依赖升级须保留 protobuf 消息解析与 loopback gRPC 回归,并由镜像扫描及真实安装 CI 验证。Guava 升级不修复 Nacos 旧 protobuf 生成代码的资源放大风险;本地消息回归也不证明该风险已消除或线上可利用。 + 后续变更必须保留以下基线契约: - Console 与 Management 保持各自路由、布局和权限边界,跨端行为归属现有 focused package; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4562c6933b..0ae3e60dbf 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5,8 +5,10 @@ settings: excludeLinksFromLockfile: false overrides: + vue-i18n: 11.4.16 + stylus: 0.59.0 dompurify: 3.4.17 - follow-redirects: 1.16.1 + follow-redirects: 1.16.2 markdown-it: 15.0.3 brace-expansion@5: 5.0.12 browserslist: ^4.28.7 @@ -31,14 +33,14 @@ importers: version: 1.12.1 '@vitejs/plugin-vue': specifier: ^6.0.10 - version: 6.0.10(vite@8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.57.0(supports-color@7.2.0))(terser@5.48.0)(yaml@2.9.1))(vue@3.5.44(typescript@6.0.3)) + version: 6.0.10(vite@8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.59.0)(terser@5.48.0)(yaml@2.9.1))(vue@3.5.44(typescript@6.0.3)) dotenv: specifier: ^18.0.8 version: 18.0.8 devDependencies: '@vue/test-utils': specifier: ^2.5.1 - version: 2.5.1(@vue/compiler-dom@3.5.43)(@vue/server-renderer@3.5.43)(vue@3.5.44(typescript@6.0.3)) + version: 2.5.1(@vue/compiler-dom@3.5.44)(@vue/server-renderer@3.5.44)(vue@3.5.44(typescript@6.0.3)) jsdom: specifier: ^30.1.3 version: 30.1.3 @@ -47,7 +49,7 @@ importers: version: 1.64.1 vitest: specifier: ^5.0.5 - version: 5.0.5(@types/node@26.6.5)(@vitest/coverage-v8@5.0.5)(@vitest/ui@5.0.5)(happy-dom@21.0.3)(jsdom@30.1.3)(vite@8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.57.0(supports-color@7.2.0))(terser@5.48.0)(yaml@2.9.1)) + version: 5.0.5(@types/node@26.6.5)(@vitest/coverage-v8@5.0.5)(@vitest/ui@5.0.5)(happy-dom@21.0.3)(jsdom@30.1.3)(vite@8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.59.0)(terser@5.48.0)(yaml@2.9.1)) apps/console: dependencies: @@ -68,7 +70,7 @@ importers: version: 3.49.0(vue@3.5.44(typescript@6.0.3)) '@tailwindcss/vite': specifier: ^4.3.3 - version: 4.3.3(vite@8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.57.0(supports-color@7.2.0))(terser@5.48.0)(yaml@2.9.1)) + version: 4.3.3(vite@8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.59.0)(terser@5.48.0)(yaml@2.9.1)) '@tanstack/vue-virtual': specifier: ^3.13.47 version: 3.13.47(vue@3.5.44(typescript@6.0.3)) @@ -119,10 +121,10 @@ importers: version: link:../../packages/theme '@unovis/ts': specifier: ^1.7.1 - version: 1.7.1(supports-color@7.2.0) + version: 1.7.1 '@unovis/vue': specifier: ^1.7.1 - version: 1.7.1(@unovis/ts@1.7.1(supports-color@7.2.0))(vue@3.5.44(typescript@6.0.3)) + version: 1.7.1(@unovis/ts@1.7.1)(vue@3.5.44(typescript@6.0.3)) '@vue-dnd-kit/core': specifier: 1.7.0 version: 1.7.0(@vueuse/core@14.4.0(vue@3.5.44(typescript@6.0.3)))(vue@3.5.44(typescript@6.0.3)) @@ -131,7 +133,7 @@ importers: version: 14.4.0(vue@3.5.44(typescript@6.0.3)) axios: specifier: ^1.20.0 - version: 1.20.0(debug@4.4.3(supports-color@7.2.0))(supports-color@7.2.0) + version: 1.20.0 class-variance-authority: specifier: ^0.7.1 version: 0.7.1 @@ -164,7 +166,7 @@ importers: version: 2.11.0(vue@3.5.44(typescript@6.0.3)) sockjs-client: specifier: ^1.6.1 - version: 1.6.1(supports-color@7.2.0) + version: 1.6.1 tailwind-merge: specifier: ^3.7.0 version: 3.7.0 @@ -178,11 +180,11 @@ importers: specifier: ^3.5.44 version: 3.5.44(typescript@6.0.3) vue-i18n: - specifier: ^11.4.16 + specifier: 11.4.16 version: 11.4.16(vue@3.5.44(typescript@6.0.3)) vue-router: specifier: ^5.4.0 - version: 5.4.0(@vue/compiler-sfc@3.5.44)(pinia@4.1.0(@vue/devtools-api@8.2.1)(typescript@6.0.3)(vue@3.5.44(typescript@6.0.3)))(rolldown@1.2.14)(rollup@4.62.0)(vite@8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.57.0(supports-color@7.2.0))(terser@5.48.0)(yaml@2.9.1))(vue@3.5.44(typescript@6.0.3)) + version: 5.4.0(@vue/compiler-sfc@3.5.44)(pinia@4.1.0(@vue/devtools-api@8.2.1)(typescript@6.0.3)(vue@3.5.44(typescript@6.0.3)))(rolldown@1.2.14)(rollup@4.62.0)(vite@8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.59.0)(terser@5.48.0)(yaml@2.9.1))(vue@3.5.44(typescript@6.0.3)) vue-sonner: specifier: ^2.0.9 version: 2.0.9 @@ -216,31 +218,31 @@ importers: version: 1.5.4 '@vitejs/plugin-vue': specifier: ^6.0.10 - version: 6.0.10(vite@8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.57.0(supports-color@7.2.0))(terser@5.48.0)(yaml@2.9.1))(vue@3.5.44(typescript@6.0.3)) + version: 6.0.10(vite@8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.59.0)(terser@5.48.0)(yaml@2.9.1))(vue@3.5.44(typescript@6.0.3)) '@vitejs/plugin-vue-jsx': specifier: ^5.1.6 - version: 5.1.6(supports-color@7.2.0)(vite@8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.57.0(supports-color@7.2.0))(terser@5.48.0)(yaml@2.9.1))(vue@3.5.44(typescript@6.0.3)) + version: 5.1.6(vite@8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.59.0)(terser@5.48.0)(yaml@2.9.1))(vue@3.5.44(typescript@6.0.3)) '@vitest/coverage-v8': specifier: ^5.0.5 version: 5.0.5(vitest@5.0.5) '@vue/eslint-config-prettier': specifier: ^10.2.0 - version: 10.2.0(eslint@10.13.0(jiti@2.7.0)(supports-color@7.2.0))(prettier@3.9.10) + version: 10.2.0(eslint@10.13.0(jiti@2.7.0))(prettier@3.9.10) '@vue/eslint-config-typescript': specifier: ^14.9.0 - version: 14.9.0(eslint-plugin-vue@10.11.1(@typescript-eslint/parser@8.61.1(eslint@10.13.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint@10.13.0(jiti@2.7.0)(supports-color@7.2.0))(vue-eslint-parser@10.4.1(eslint@10.13.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)))(eslint@10.13.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) + version: 14.9.0(eslint-plugin-vue@10.11.1(@typescript-eslint/parser@8.61.1(eslint@10.13.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.13.0(jiti@2.7.0))(vue-eslint-parser@10.4.1(eslint@10.13.0(jiti@2.7.0))))(eslint@10.13.0(jiti@2.7.0))(typescript@6.0.3) '@vue/test-utils': specifier: ^2.5.1 - version: 2.5.1(@vue/compiler-dom@3.5.43)(@vue/server-renderer@3.5.43)(vue@3.5.44(typescript@6.0.3)) + version: 2.5.1(@vue/compiler-dom@3.5.44)(@vue/server-renderer@3.5.44)(vue@3.5.44(typescript@6.0.3)) '@vue/tsconfig': specifier: ^0.9.1 version: 0.9.1(typescript@6.0.3)(vue@3.5.44(typescript@6.0.3)) eslint: specifier: ^10.13.0 - version: 10.13.0(jiti@2.7.0)(supports-color@7.2.0) + version: 10.13.0(jiti@2.7.0) eslint-plugin-vue: specifier: ^10.11.1 - version: 10.11.1(@typescript-eslint/parser@8.61.1(eslint@10.13.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint@10.13.0(jiti@2.7.0)(supports-color@7.2.0))(vue-eslint-parser@10.4.1(eslint@10.13.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)) + version: 10.11.1(@typescript-eslint/parser@8.61.1(eslint@10.13.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.13.0(jiti@2.7.0))(vue-eslint-parser@10.4.1(eslint@10.13.0(jiti@2.7.0))) jiti: specifier: ^2.7.0 version: 2.7.0 @@ -261,19 +263,19 @@ importers: version: 6.0.3 unplugin-icons: specifier: ^24.0.0 - version: 24.0.0(@vue/compiler-sfc@3.5.43)(rolldown@1.2.14)(rollup@4.62.0)(vite@8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.57.0(supports-color@7.2.0))(terser@5.48.0)(yaml@2.9.1)) + version: 24.0.0(@vue/compiler-sfc@3.5.44)(rolldown@1.2.14)(rollup@4.62.0)(vite@8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.59.0)(terser@5.48.0)(yaml@2.9.1)) vite: specifier: ^8.0.16 - version: 8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.57.0(supports-color@7.2.0))(terser@5.48.0)(yaml@2.9.1) + version: 8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.59.0)(terser@5.48.0)(yaml@2.9.1) vite-plugin-pwa: specifier: ^2.0.0 - version: 2.0.0(supports-color@7.2.0)(vite@8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.57.0(supports-color@7.2.0))(terser@5.48.0)(yaml@2.9.1))(workbox-build@7.4.1(supports-color@7.2.0))(workbox-window@7.4.1) + version: 2.0.0(vite@8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.59.0)(terser@5.48.0)(yaml@2.9.1))(workbox-build@7.4.1)(workbox-window@7.4.1) vite-plugin-vue-devtools: specifier: ^8.2.1 - version: 8.2.1(supports-color@7.2.0)(vite@8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.57.0(supports-color@7.2.0))(terser@5.48.0)(yaml@2.9.1))(vue@3.5.44(typescript@6.0.3)) + version: 8.2.1(vite@8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.59.0)(terser@5.48.0)(yaml@2.9.1))(vue@3.5.44(typescript@6.0.3)) vitest: specifier: ^5.0.5 - version: 5.0.5(@types/node@26.6.5)(@vitest/coverage-v8@5.0.5)(@vitest/ui@5.0.5)(happy-dom@21.0.3)(jsdom@30.1.3)(vite@8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.57.0(supports-color@7.2.0))(terser@5.48.0)(yaml@2.9.1)) + version: 5.0.5(@types/node@26.6.5)(@vitest/coverage-v8@5.0.5)(@vitest/ui@5.0.5)(happy-dom@21.0.3)(jsdom@30.1.3)(vite@8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.59.0)(terser@5.48.0)(yaml@2.9.1)) vue-tsc: specifier: ^3.3.13 version: 3.3.13(typescript@6.0.3) @@ -284,7 +286,7 @@ importers: specifier: ^3.5.44 version: 3.5.44(typescript@6.0.3) vue-i18n: - specifier: ^11.4.16 + specifier: 11.4.16 version: 11.4.16(vue@3.5.44(typescript@6.0.3)) devDependencies: '@types/node': @@ -292,25 +294,25 @@ importers: version: 26.6.5 '@vitejs/plugin-vue': specifier: ^6.0.10 - version: 6.0.10(vite@8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.57.0(supports-color@7.2.0))(terser@5.48.0)(yaml@2.9.1))(vue@3.5.44(typescript@6.0.3)) + version: 6.0.10(vite@8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.59.0)(terser@5.48.0)(yaml@2.9.1))(vue@3.5.44(typescript@6.0.3)) '@vitest/coverage-v8': specifier: ^5.0.5 version: 5.0.5(vitest@5.0.5) '@vue/eslint-config-prettier': specifier: ^10.2.0 - version: 10.2.0(eslint@10.13.0(jiti@2.7.0)(supports-color@7.2.0))(prettier@3.9.10) + version: 10.2.0(eslint@10.13.0(jiti@2.7.0))(prettier@3.9.10) '@vue/eslint-config-typescript': specifier: ^14.9.0 - version: 14.9.0(eslint-plugin-vue@10.11.1(@typescript-eslint/parser@8.61.1(eslint@10.13.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint@10.13.0(jiti@2.7.0)(supports-color@7.2.0))(vue-eslint-parser@10.4.1(eslint@10.13.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)))(eslint@10.13.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) + version: 14.9.0(eslint-plugin-vue@10.11.1(@typescript-eslint/parser@8.61.1(eslint@10.13.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.13.0(jiti@2.7.0))(vue-eslint-parser@10.4.1(eslint@10.13.0(jiti@2.7.0))))(eslint@10.13.0(jiti@2.7.0))(typescript@6.0.3) '@vue/test-utils': specifier: ^2.5.1 - version: 2.5.1(@vue/compiler-dom@3.5.43)(@vue/server-renderer@3.5.43)(vue@3.5.44(typescript@6.0.3)) + version: 2.5.1(@vue/compiler-dom@3.5.44)(@vue/server-renderer@3.5.44)(vue@3.5.44(typescript@6.0.3)) eslint: specifier: ^10.13.0 - version: 10.13.0(jiti@2.7.0)(supports-color@7.2.0) + version: 10.13.0(jiti@2.7.0) eslint-plugin-vue: specifier: ^10.11.1 - version: 10.11.1(@typescript-eslint/parser@8.61.1(eslint@10.13.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint@10.13.0(jiti@2.7.0)(supports-color@7.2.0))(vue-eslint-parser@10.4.1(eslint@10.13.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)) + version: 10.11.1(@typescript-eslint/parser@8.61.1(eslint@10.13.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.13.0(jiti@2.7.0))(vue-eslint-parser@10.4.1(eslint@10.13.0(jiti@2.7.0))) jsdom: specifier: ^30.1.3 version: 30.1.3 @@ -319,10 +321,10 @@ importers: version: 6.0.3 vite: specifier: ^8.0.16 - version: 8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.57.0(supports-color@7.2.0))(terser@5.48.0)(yaml@2.9.1) + version: 8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.59.0)(terser@5.48.0)(yaml@2.9.1) vitest: specifier: ^5.0.5 - version: 5.0.5(@types/node@26.6.5)(@vitest/coverage-v8@5.0.5)(@vitest/ui@5.0.5)(happy-dom@21.0.3)(jsdom@30.1.3)(vite@8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.57.0(supports-color@7.2.0))(terser@5.48.0)(yaml@2.9.1)) + version: 5.0.5(@types/node@26.6.5)(@vitest/coverage-v8@5.0.5)(@vitest/ui@5.0.5)(happy-dom@21.0.3)(jsdom@30.1.3)(vite@8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.59.0)(terser@5.48.0)(yaml@2.9.1)) vue-tsc: specifier: ^3.3.13 version: 3.3.13(typescript@6.0.3) @@ -349,7 +351,7 @@ importers: version: 3.49.0(vue@3.5.44(typescript@6.0.3)) '@tailwindcss/vite': specifier: ^4.3.3 - version: 4.3.3(vite@8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.57.0(supports-color@7.2.0))(terser@5.48.0)(yaml@2.9.1)) + version: 4.3.3(vite@8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.59.0)(terser@5.48.0)(yaml@2.9.1)) '@tanstack/vue-table': specifier: ^8.21.3 version: 8.21.3(vue@3.5.44(typescript@6.0.3)) @@ -400,16 +402,16 @@ importers: version: link:../../packages/theme '@unovis/ts': specifier: ^1.7.1 - version: 1.7.1(supports-color@7.2.0) + version: 1.7.1 '@unovis/vue': specifier: ^1.7.1 - version: 1.7.1(@unovis/ts@1.7.1(supports-color@7.2.0))(vue@3.5.44(typescript@6.0.3)) + version: 1.7.1(@unovis/ts@1.7.1)(vue@3.5.44(typescript@6.0.3)) '@vueuse/core': specifier: ^14.4.0 version: 14.4.0(vue@3.5.44(typescript@6.0.3)) axios: specifier: ^1.20.0 - version: 1.20.0(debug@4.4.3(supports-color@7.2.0))(supports-color@7.2.0) + version: 1.20.0 class-variance-authority: specifier: ^0.7.1 version: 0.7.1 @@ -462,14 +464,14 @@ importers: specifier: ^8.3.2 version: 8.3.2(echarts@6.1.0)(vue@3.5.44(typescript@6.0.3)) vue-i18n: - specifier: ^11.4.16 + specifier: 11.4.16 version: 11.4.16(vue@3.5.44(typescript@6.0.3)) vue-input-otp: specifier: ^0.4.0 version: 0.4.0(typescript@6.0.3)(vue@3.5.44(typescript@6.0.3)) vue-router: specifier: ^5.4.0 - version: 5.4.0(@vue/compiler-sfc@3.5.44)(pinia@4.1.0(@vue/devtools-api@8.2.1)(typescript@6.0.3)(vue@3.5.44(typescript@6.0.3)))(rolldown@1.2.14)(rollup@4.62.0)(vite@8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.57.0(supports-color@7.2.0))(terser@5.48.0)(yaml@2.9.1))(vue@3.5.44(typescript@6.0.3)) + version: 5.4.0(@vue/compiler-sfc@3.5.44)(pinia@4.1.0(@vue/devtools-api@8.2.1)(typescript@6.0.3)(vue@3.5.44(typescript@6.0.3)))(rolldown@1.2.14)(rollup@4.62.0)(vite@8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.59.0)(terser@5.48.0)(yaml@2.9.1))(vue@3.5.44(typescript@6.0.3)) vue-sonner: specifier: ^2.0.9 version: 2.0.9 @@ -488,7 +490,7 @@ importers: version: 26.6.5 '@vitejs/plugin-vue': specifier: ^6.0.10 - version: 6.0.10(vite@8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.57.0(supports-color@7.2.0))(terser@5.48.0)(yaml@2.9.1))(vue@3.5.44(typescript@6.0.3)) + version: 6.0.10(vite@8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.59.0)(terser@5.48.0)(yaml@2.9.1))(vue@3.5.44(typescript@6.0.3)) '@vitest/coverage-v8': specifier: ^5.0.5 version: 5.0.5(vitest@5.0.5) @@ -497,22 +499,22 @@ importers: version: 5.0.5(vitest@5.0.5) '@vue/eslint-config-prettier': specifier: ^10.2.0 - version: 10.2.0(eslint@10.13.0(jiti@2.7.0)(supports-color@7.2.0))(prettier@3.9.10) + version: 10.2.0(eslint@10.13.0(jiti@2.7.0))(prettier@3.9.10) '@vue/eslint-config-typescript': specifier: ^14.9.0 - version: 14.9.0(eslint-plugin-vue@10.11.1(@typescript-eslint/parser@8.61.1(eslint@10.13.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint@10.13.0(jiti@2.7.0)(supports-color@7.2.0))(vue-eslint-parser@10.4.1(eslint@10.13.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)))(eslint@10.13.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) + version: 14.9.0(eslint-plugin-vue@10.11.1(@typescript-eslint/parser@8.61.1(eslint@10.13.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.13.0(jiti@2.7.0))(vue-eslint-parser@10.4.1(eslint@10.13.0(jiti@2.7.0))))(eslint@10.13.0(jiti@2.7.0))(typescript@6.0.3) '@vue/test-utils': specifier: ^2.5.1 - version: 2.5.1(@vue/compiler-dom@3.5.43)(@vue/server-renderer@3.5.43)(vue@3.5.44(typescript@6.0.3)) + version: 2.5.1(@vue/compiler-dom@3.5.44)(@vue/server-renderer@3.5.44)(vue@3.5.44(typescript@6.0.3)) '@vue/tsconfig': specifier: ^0.9.1 version: 0.9.1(typescript@6.0.3)(vue@3.5.44(typescript@6.0.3)) eslint: specifier: ^10.13.0 - version: 10.13.0(jiti@2.7.0)(supports-color@7.2.0) + version: 10.13.0(jiti@2.7.0) eslint-plugin-vue: specifier: ~10.11.1 - version: 10.11.1(@typescript-eslint/parser@8.61.1(eslint@10.13.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint@10.13.0(jiti@2.7.0)(supports-color@7.2.0))(vue-eslint-parser@10.4.1(eslint@10.13.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)) + version: 10.11.1(@typescript-eslint/parser@8.61.1(eslint@10.13.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.13.0(jiti@2.7.0))(vue-eslint-parser@10.4.1(eslint@10.13.0(jiti@2.7.0))) jiti: specifier: ^2.7.0 version: 2.7.0 @@ -536,13 +538,13 @@ importers: version: 6.0.3 vite: specifier: ^8.0.16 - version: 8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.57.0(supports-color@7.2.0))(terser@5.48.0)(yaml@2.9.1) + version: 8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.59.0)(terser@5.48.0)(yaml@2.9.1) vite-plugin-vue-devtools: specifier: ^8.2.1 - version: 8.2.1(supports-color@7.2.0)(vite@8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.57.0(supports-color@7.2.0))(terser@5.48.0)(yaml@2.9.1))(vue@3.5.44(typescript@6.0.3)) + version: 8.2.1(vite@8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.59.0)(terser@5.48.0)(yaml@2.9.1))(vue@3.5.44(typescript@6.0.3)) vitest: specifier: ^5.0.5 - version: 5.0.5(@types/node@26.6.5)(@vitest/coverage-v8@5.0.5)(@vitest/ui@5.0.5)(happy-dom@21.0.3)(jsdom@30.1.3)(vite@8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.57.0(supports-color@7.2.0))(terser@5.48.0)(yaml@2.9.1)) + version: 5.0.5(@types/node@26.6.5)(@vitest/coverage-v8@5.0.5)(@vitest/ui@5.0.5)(happy-dom@21.0.3)(jsdom@30.1.3)(vite@8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.59.0)(terser@5.48.0)(yaml@2.9.1)) vue-tsc: specifier: ^3.3.13 version: 3.3.13(typescript@6.0.3) @@ -554,7 +556,7 @@ importers: version: 5.9.3 vitest: specifier: ^5.0.5 - version: 5.0.5(@types/node@26.6.5)(@vitest/coverage-v8@5.0.5)(@vitest/ui@5.0.5)(happy-dom@21.0.3)(jsdom@30.1.3)(vite@8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.57.0(supports-color@7.2.0))(terser@5.48.0)(yaml@2.9.1)) + version: 5.0.5(@types/node@26.6.5)(@vitest/coverage-v8@5.0.5)(@vitest/ui@5.0.5)(happy-dom@21.0.3)(jsdom@30.1.3)(vite@8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.59.0)(terser@5.48.0)(yaml@2.9.1)) vue: specifier: ^3.5.44 version: 3.5.44(typescript@5.9.3) @@ -563,7 +565,7 @@ importers: dependencies: axios: specifier: ^1.16.1 - version: 1.20.0(debug@4.4.3(supports-color@7.2.0))(supports-color@7.2.0) + version: 1.20.0 clsx: specifier: ^2.1.1 version: 2.1.1 @@ -579,7 +581,7 @@ importers: version: 5.9.3 vitest: specifier: ^5.0.5 - version: 5.0.5(@types/node@26.6.5)(@vitest/coverage-v8@5.0.5)(@vitest/ui@5.0.5)(happy-dom@21.0.3)(jsdom@30.1.3)(vite@8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.57.0(supports-color@7.2.0))(terser@5.48.0)(yaml@2.9.1)) + version: 5.0.5(@types/node@26.6.5)(@vitest/coverage-v8@5.0.5)(@vitest/ui@5.0.5)(happy-dom@21.0.3)(jsdom@30.1.3)(vite@8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.59.0)(terser@5.48.0)(yaml@2.9.1)) vue: specifier: ^3.5.44 version: 3.5.44(typescript@5.9.3) @@ -608,15 +610,15 @@ importers: specifier: ^3.7.0 version: 3.7.0 vue-i18n: - specifier: ^11.0.0 - version: 11.4.13(vue@3.5.44(typescript@5.9.3)) + specifier: 11.4.16 + version: 11.4.16(vue@3.5.44(typescript@5.9.3)) devDependencies: '@vitejs/plugin-vue': specifier: ^6.0.10 - version: 6.0.10(vite@8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.57.0(supports-color@7.2.0))(terser@5.48.0)(yaml@2.9.1))(vue@3.5.44(typescript@5.9.3)) + version: 6.0.10(vite@8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.59.0)(terser@5.48.0)(yaml@2.9.1))(vue@3.5.44(typescript@5.9.3)) '@vue/test-utils': specifier: ^2.5.1 - version: 2.5.1(@vue/compiler-dom@3.5.43)(@vue/server-renderer@3.5.43)(vue@3.5.44(typescript@5.9.3)) + version: 2.5.1(@vue/compiler-dom@3.5.44)(@vue/server-renderer@3.5.44)(vue@3.5.44(typescript@5.9.3)) '@vue/tsconfig': specifier: ^0.9.1 version: 0.9.1(typescript@5.9.3)(vue@3.5.44(typescript@5.9.3)) @@ -628,16 +630,16 @@ importers: version: 5.9.3 vite: specifier: ^8.0.16 - version: 8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.57.0(supports-color@7.2.0))(terser@5.48.0)(yaml@2.9.1) + version: 8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.59.0)(terser@5.48.0)(yaml@2.9.1) vitest: specifier: ^5.0.5 - version: 5.0.5(@types/node@26.6.5)(@vitest/coverage-v8@5.0.5)(@vitest/ui@5.0.5)(happy-dom@21.0.3)(jsdom@30.1.3)(vite@8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.57.0(supports-color@7.2.0))(terser@5.48.0)(yaml@2.9.1)) + version: 5.0.5(@types/node@26.6.5)(@vitest/coverage-v8@5.0.5)(@vitest/ui@5.0.5)(happy-dom@21.0.3)(jsdom@30.1.3)(vite@8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.59.0)(terser@5.48.0)(yaml@2.9.1)) vue: specifier: ^3.5.44 version: 3.5.44(typescript@5.9.3) vue-router: specifier: ^5.4.0 - version: 5.4.0(@vue/compiler-sfc@3.5.44)(pinia@4.1.0(@vue/devtools-api@8.2.1)(typescript@5.9.3)(vue@3.5.44(typescript@5.9.3)))(rolldown@1.2.14)(rollup@4.62.0)(vite@8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.57.0(supports-color@7.2.0))(terser@5.48.0)(yaml@2.9.1))(vue@3.5.44(typescript@5.9.3)) + version: 5.4.0(@vue/compiler-sfc@3.5.44)(pinia@4.1.0(@vue/devtools-api@8.2.1)(typescript@5.9.3)(vue@3.5.44(typescript@5.9.3)))(rolldown@1.2.14)(rollup@4.62.0)(vite@8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.59.0)(terser@5.48.0)(yaml@2.9.1))(vue@3.5.44(typescript@5.9.3)) vue-tsc: specifier: ^3.3.13 version: 3.3.13(typescript@5.9.3) @@ -664,7 +666,7 @@ importers: version: 5.9.3 vitest: specifier: ^5.0.5 - version: 5.0.5(@types/node@26.6.5)(@vitest/coverage-v8@5.0.5)(@vitest/ui@5.0.5)(happy-dom@21.0.3)(jsdom@30.1.3)(vite@8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.57.0(supports-color@7.2.0))(terser@5.48.0)(yaml@2.9.1)) + version: 5.0.5(@types/node@26.6.5)(@vitest/coverage-v8@5.0.5)(@vitest/ui@5.0.5)(happy-dom@21.0.3)(jsdom@30.1.3)(vite@8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.59.0)(terser@5.48.0)(yaml@2.9.1)) packages/design-system: dependencies: @@ -679,10 +681,10 @@ importers: version: link:../theme '@unovis/ts': specifier: ^1.7.1 - version: 1.7.1(supports-color@7.2.0) + version: 1.7.1 '@unovis/vue': specifier: ^1.7.1 - version: 1.7.1(@unovis/ts@1.7.1(supports-color@7.2.0))(vue@3.5.44(typescript@5.9.3)) + version: 1.7.1(@unovis/ts@1.7.1)(vue@3.5.44(typescript@5.9.3)) '@vueuse/core': specifier: ^14.4.0 version: 14.4.0(vue@3.5.44(typescript@5.9.3)) @@ -716,19 +718,19 @@ importers: devDependencies: '@vitejs/plugin-vue': specifier: ^6.0.10 - version: 6.0.10(vite@8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.57.0(supports-color@7.2.0))(terser@5.48.0)(yaml@2.9.1))(vue@3.5.44(typescript@5.9.3)) + version: 6.0.10(vite@8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.59.0)(terser@5.48.0)(yaml@2.9.1))(vue@3.5.44(typescript@5.9.3)) '@vue/tsconfig': specifier: ^0.9.1 version: 0.9.1(typescript@5.9.3)(vue@3.5.44(typescript@5.9.3)) shadcn-vue: specifier: 2.8.2 - version: 2.8.2(babel-plugin-macros@3.1.0)(eslint@10.13.0(jiti@2.7.0)(supports-color@7.2.0))(magicast@0.5.5)(supports-color@7.2.0)(vue@3.5.44(typescript@5.9.3)) + version: 2.8.2(babel-plugin-macros@3.1.0)(eslint@10.13.0(jiti@2.7.0))(magicast@0.5.5)(vue@3.5.44(typescript@5.9.3)) typescript: specifier: ~5.9.3 version: 5.9.3 vite: specifier: ^8.0.16 - version: 8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.57.0(supports-color@7.2.0))(terser@5.48.0)(yaml@2.9.1) + version: 8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.59.0)(terser@5.48.0)(yaml@2.9.1) vue: specifier: ^3.5.44 version: 3.5.44(typescript@5.9.3) @@ -743,7 +745,7 @@ importers: version: 5.9.3 vitest: specifier: ^5.0.5 - version: 5.0.5(@types/node@26.6.5)(@vitest/coverage-v8@5.0.5)(@vitest/ui@5.0.5)(happy-dom@21.0.3)(jsdom@30.1.3)(vite@8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.57.0(supports-color@7.2.0))(terser@5.48.0)(yaml@2.9.1)) + version: 5.0.5(@types/node@26.6.5)(@vitest/coverage-v8@5.0.5)(@vitest/ui@5.0.5)(happy-dom@21.0.3)(jsdom@30.1.3)(vite@8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.59.0)(terser@5.48.0)(yaml@2.9.1)) packages/http-client: dependencies: @@ -752,7 +754,7 @@ importers: version: link:../auth-core axios: specifier: ^1.13.2 - version: 1.20.0(debug@4.4.3(supports-color@7.2.0))(supports-color@7.2.0) + version: 1.20.0 devDependencies: '@types/node': specifier: ^26.6.5 @@ -762,7 +764,7 @@ importers: version: 5.9.3 vitest: specifier: ^5.0.5 - version: 5.0.5(@types/node@26.6.5)(@vitest/coverage-v8@5.0.5)(@vitest/ui@5.0.5)(happy-dom@21.0.3)(jsdom@30.1.3)(vite@8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.57.0(supports-color@7.2.0))(terser@5.48.0)(yaml@2.9.1)) + version: 5.0.5(@types/node@26.6.5)(@vitest/coverage-v8@5.0.5)(@vitest/ui@5.0.5)(happy-dom@21.0.3)(jsdom@30.1.3)(vite@8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.59.0)(terser@5.48.0)(yaml@2.9.1)) packages/i18n-completeness: devDependencies: @@ -774,7 +776,7 @@ importers: version: 5.9.3 vitest: specifier: ^5.0.5 - version: 5.0.5(@types/node@26.6.5)(@vitest/coverage-v8@5.0.5)(@vitest/ui@5.0.5)(happy-dom@21.0.3)(jsdom@30.1.3)(vite@8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.57.0(supports-color@7.2.0))(terser@5.48.0)(yaml@2.9.1)) + version: 5.0.5(@types/node@26.6.5)(@vitest/coverage-v8@5.0.5)(@vitest/ui@5.0.5)(happy-dom@21.0.3)(jsdom@30.1.3)(vite@8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.59.0)(terser@5.48.0)(yaml@2.9.1)) packages/locale-preference: devDependencies: @@ -786,7 +788,7 @@ importers: version: 5.9.3 vitest: specifier: ^5.0.5 - version: 5.0.5(@types/node@26.6.5)(@vitest/coverage-v8@5.0.5)(@vitest/ui@5.0.5)(happy-dom@21.0.3)(jsdom@30.1.3)(vite@8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.57.0(supports-color@7.2.0))(terser@5.48.0)(yaml@2.9.1)) + version: 5.0.5(@types/node@26.6.5)(@vitest/coverage-v8@5.0.5)(@vitest/ui@5.0.5)(happy-dom@21.0.3)(jsdom@30.1.3)(vite@8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.59.0)(terser@5.48.0)(yaml@2.9.1)) vue: specifier: ^3.5.44 version: 3.5.44(typescript@5.9.3) @@ -820,7 +822,7 @@ importers: version: 5.9.3 vitest: specifier: ^5.0.5 - version: 5.0.5(@types/node@26.6.5)(@vitest/coverage-v8@5.0.5)(@vitest/ui@5.0.5)(happy-dom@21.0.3)(jsdom@30.1.3)(vite@8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.57.0(supports-color@7.2.0))(terser@5.48.0)(yaml@2.9.1)) + version: 5.0.5(@types/node@26.6.5)(@vitest/coverage-v8@5.0.5)(@vitest/ui@5.0.5)(happy-dom@21.0.3)(jsdom@30.1.3)(vite@8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.59.0)(terser@5.48.0)(yaml@2.9.1)) packages/request-state: devDependencies: @@ -832,7 +834,7 @@ importers: version: 5.9.3 vitest: specifier: ^5.0.5 - version: 5.0.5(@types/node@26.6.5)(@vitest/coverage-v8@5.0.5)(@vitest/ui@5.0.5)(happy-dom@21.0.3)(jsdom@30.1.3)(vite@8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.57.0(supports-color@7.2.0))(terser@5.48.0)(yaml@2.9.1)) + version: 5.0.5(@types/node@26.6.5)(@vitest/coverage-v8@5.0.5)(@vitest/ui@5.0.5)(happy-dom@21.0.3)(jsdom@30.1.3)(vite@8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.59.0)(terser@5.48.0)(yaml@2.9.1)) vue: specifier: ^3.5.44 version: 3.5.44(typescript@5.9.3) @@ -844,7 +846,7 @@ importers: version: 5.9.3 vitest: specifier: ^5.0.5 - version: 5.0.5(@types/node@26.6.5)(@vitest/coverage-v8@5.0.5)(@vitest/ui@5.0.5)(happy-dom@21.0.3)(jsdom@30.1.3)(vite@8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.57.0(supports-color@7.2.0))(terser@5.48.0)(yaml@2.9.1)) + version: 5.0.5(@types/node@26.6.5)(@vitest/coverage-v8@5.0.5)(@vitest/ui@5.0.5)(happy-dom@21.0.3)(jsdom@30.1.3)(vite@8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.59.0)(terser@5.48.0)(yaml@2.9.1)) packages/sidebar-menu: dependencies: @@ -865,14 +867,14 @@ importers: version: 3.7.0 vue-router: specifier: ^5.0.0 - version: 5.3.1(@vue/compiler-sfc@3.5.43)(pinia@4.1.0(@vue/devtools-api@8.2.1)(typescript@5.9.3)(vue@3.5.44(typescript@5.9.3)))(rolldown@1.2.14)(rollup@4.62.0)(vite@8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.57.0(supports-color@7.2.0))(terser@5.48.0)(yaml@2.9.1))(vue@3.5.44(typescript@5.9.3)) + version: 5.3.1(@vue/compiler-sfc@3.5.44)(pinia@4.1.0(@vue/devtools-api@8.2.1)(typescript@5.9.3)(vue@3.5.44(typescript@5.9.3)))(rolldown@1.2.14)(rollup@4.62.0)(vite@8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.59.0)(terser@5.48.0)(yaml@2.9.1))(vue@3.5.44(typescript@5.9.3)) devDependencies: '@vitejs/plugin-vue': specifier: ^6.0.10 - version: 6.0.10(vite@8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.57.0(supports-color@7.2.0))(terser@5.48.0)(yaml@2.9.1))(vue@3.5.44(typescript@5.9.3)) + version: 6.0.10(vite@8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.59.0)(terser@5.48.0)(yaml@2.9.1))(vue@3.5.44(typescript@5.9.3)) '@vue/test-utils': specifier: ^2.5.1 - version: 2.5.1(@vue/compiler-dom@3.5.43)(@vue/server-renderer@3.5.43)(vue@3.5.44(typescript@5.9.3)) + version: 2.5.1(@vue/compiler-dom@3.5.44)(@vue/server-renderer@3.5.44)(vue@3.5.44(typescript@5.9.3)) '@vue/tsconfig': specifier: ^0.9.1 version: 0.9.1(typescript@5.9.3)(vue@3.5.44(typescript@5.9.3)) @@ -884,10 +886,10 @@ importers: version: 5.9.3 vite: specifier: ^8.0.16 - version: 8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.57.0(supports-color@7.2.0))(terser@5.48.0)(yaml@2.9.1) + version: 8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.59.0)(terser@5.48.0)(yaml@2.9.1) vitest: specifier: ^5.0.5 - version: 5.0.5(@types/node@26.6.5)(@vitest/coverage-v8@5.0.5)(@vitest/ui@5.0.5)(happy-dom@21.0.3)(jsdom@30.1.3)(vite@8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.57.0(supports-color@7.2.0))(terser@5.48.0)(yaml@2.9.1)) + version: 5.0.5(@types/node@26.6.5)(@vitest/coverage-v8@5.0.5)(@vitest/ui@5.0.5)(happy-dom@21.0.3)(jsdom@30.1.3)(vite@8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.59.0)(terser@5.48.0)(yaml@2.9.1)) vue: specifier: ^3.5.44 version: 3.5.44(typescript@5.9.3) @@ -909,7 +911,7 @@ importers: version: 5.9.3 vitest: specifier: ^5.0.5 - version: 5.0.5(@types/node@26.6.5)(@vitest/coverage-v8@5.0.5)(@vitest/ui@5.0.5)(happy-dom@21.0.3)(jsdom@30.1.3)(vite@8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.57.0(supports-color@7.2.0))(terser@5.48.0)(yaml@2.9.1)) + version: 5.0.5(@types/node@26.6.5)(@vitest/coverage-v8@5.0.5)(@vitest/ui@5.0.5)(happy-dom@21.0.3)(jsdom@30.1.3)(vite@8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.59.0)(terser@5.48.0)(yaml@2.9.1)) packages/theme: devDependencies: @@ -918,7 +920,7 @@ importers: version: 26.6.5 '@vue/test-utils': specifier: ^2.5.1 - version: 2.5.1(@vue/compiler-dom@3.5.43)(@vue/server-renderer@3.5.43)(vue@3.5.44(typescript@5.9.3)) + version: 2.5.1(@vue/compiler-dom@3.5.44)(@vue/server-renderer@3.5.44)(vue@3.5.44(typescript@5.9.3)) happy-dom: specifier: ^21.0.3 version: 21.0.3 @@ -927,13 +929,16 @@ importers: version: 5.9.3 vitest: specifier: ^5.0.5 - version: 5.0.5(@types/node@26.6.5)(@vitest/coverage-v8@5.0.5)(@vitest/ui@5.0.5)(happy-dom@21.0.3)(jsdom@30.1.3)(vite@8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.57.0(supports-color@7.2.0))(terser@5.48.0)(yaml@2.9.1)) + version: 5.0.5(@types/node@26.6.5)(@vitest/coverage-v8@5.0.5)(@vitest/ui@5.0.5)(happy-dom@21.0.3)(jsdom@30.1.3)(vite@8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.59.0)(terser@5.48.0)(yaml@2.9.1)) vue: specifier: ^3.5.44 version: 3.5.44(typescript@5.9.3) packages: + '@adobe/css-tools@4.5.0': + resolution: {integrity: sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q==} + '@antfu/install-pkg@1.1.0': resolution: {integrity: sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==} @@ -1759,34 +1764,18 @@ packages: '@internationalized/number@3.6.7': resolution: {integrity: sha512-3ji1fcrT+FPAK86UqEhB/psHixYo6niWPJtt7+qRaYFynt/BaJG8GhAPimtWUpEiVSTq8ZM8L5psMxGquiB/Vg==} - '@intlify/core-base@11.4.13': - resolution: {integrity: sha512-3qGv1/j0fl6ZSeehGaznE78A4i3hCaOeOyDh7jq7SV6ch/XGoy96xIrYuALTV3xhIqj2rktsIKcwlLZ3CbOhkA==} - engines: {node: '>= 22'} - '@intlify/core-base@11.4.16': resolution: {integrity: sha512-xB8nMdMRLNx7b0zPvrHy0+N9uq9Vfi1aUxHqWgn2e2/TbjCKy8HfoFpnUXXxVP8I0p1ccd6LFSC3XsFjZ3vweA==} engines: {node: '>= 22'} - '@intlify/devtools-types@11.4.13': - resolution: {integrity: sha512-f38d2yvjNc1/c+BnTLsxTB0729clTtyy7kTxHu4FFlvI6FpmBOp7b9sLYtsKJpXSZ70kIEexvNwDSzLkIlYnlQ==} - engines: {node: '>= 22'} - '@intlify/devtools-types@11.4.16': resolution: {integrity: sha512-Bl7arICsKgwcSEQuZJun3swenN4jwSpg35lCpijFznYbW9GvkfH4wLxcI5+gz7d/EHqu66UT9CZDIdnQdgMAGg==} engines: {node: '>= 22'} - '@intlify/message-compiler@11.4.13': - resolution: {integrity: sha512-cjKsDeWUD/i3mj0QtC8hydLCFP5mgZtTU1xZZaN9/O3FG94EXc0rTbecYpifglp26rpoJoIjSlW137VejLKsZw==} - engines: {node: '>= 22'} - '@intlify/message-compiler@11.4.16': resolution: {integrity: sha512-p4lDWmBAsi3ja+PuEfPRavYyvqkYn/Upxz7UpnyI9LgIIy8XawcGIDzN+k99Z+99etO3HmDXGeTxnJIJGgSZ+g==} engines: {node: '>= 22'} - '@intlify/shared@11.4.13': - resolution: {integrity: sha512-ZjcNgEED1FtTCFb+ohWBdR21k5kvleKtM9peobQtLB83mNIfEhJK86dEbhfk+OcUrybrEbc4eDeE9UbFUVHsjQ==} - engines: {node: '>= 22'} - '@intlify/shared@11.4.16': resolution: {integrity: sha512-xehmkbEgbbNh/V+d8r3hFup0ViaN29YTJSAfRr28C54rxjyZEO0RNQKqW7VVajj16gLdglotNjiq+sIVNtHGzw==} engines: {node: '>= 22'} @@ -2931,11 +2920,6 @@ packages: '@volar/typescript@2.4.28': resolution: {integrity: sha512-Ja6yvWrbis2QtN4ClAKreeUZPVYMARDYZl9LMEv1iQ1QdepB6wn0jTRxA9MftYmYa4DQ4k/DaSZpFPUfxl8giw==} - peerDependencies: - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true '@vue-dnd-kit/core@1.7.0': resolution: {integrity: sha512-6Otpo/9Fp/cX5EiUoN6XmIPtm/mOreAXi8clr9HgzVvDHD0sfkgVOFxCUiVbH46yAzr6KBU1Q1tjSWLBqua+5g==} @@ -3272,11 +3256,6 @@ packages: resolution: {integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==} engines: {node: '>= 4.0.0'} - atob@2.1.2: - resolution: {integrity: sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==} - engines: {node: '>= 4.5.0'} - hasBin: true - available-typed-arrays@1.0.7: resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} engines: {node: '>= 0.4'} @@ -3545,9 +3524,6 @@ packages: resolution: {integrity: sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==} engines: {node: '>= 6'} - css@3.0.0: - resolution: {integrity: sha512-DG9pFfwOrzc+hawpmqX/dHYHJG+Bsdb0klhyi1sDneOgGOXy9wQIC8hzyVp1e4NRYDBdxcylvywPkkXCHAzTyQ==} - cssesc@3.0.0: resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} engines: {node: '>=4'} @@ -3745,10 +3721,6 @@ packages: decimal.js@10.6.0: resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} - decode-uri-component@0.2.2: - resolution: {integrity: sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==} - engines: {node: '>=0.10'} - dedent@1.7.2: resolution: {integrity: sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==} peerDependencies: @@ -4143,8 +4115,8 @@ packages: flatted@3.4.4: resolution: {integrity: sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==} - follow-redirects@1.16.1: - resolution: {integrity: sha512-FNvFGzoMLWmE6Yj9spb/zjd7yiNCHiAW9/Tg9CXrQ8wuu32HtlJOwWO11OJafl5FfY3DxTdQ0vj42zU1kvv5jg==} + follow-redirects@1.16.2: + resolution: {integrity: sha512-/kVP2U8z596Ia0gwFrwyIeo74ta5aVmSeU1FfAUMAmcEP5EP7zydmag3SLiN64NaRKef3lxsq7i/g7ltJQDGow==} engines: {node: '>=4.0'} peerDependencies: debug: '*' @@ -4301,10 +4273,6 @@ packages: resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==} engines: {node: '>= 0.4'} - has-flag@4.0.0: - resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} - engines: {node: '>=8'} - has-property-descriptors@1.0.2: resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} @@ -5651,10 +5619,6 @@ packages: resolution: {integrity: sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==} engines: {node: '>=0.10.0'} - source-map-resolve@0.6.0: - resolution: {integrity: sha512-KXBr9d/fO/bWo97NXsPIAW1bFSBOuCnjbNTBMO7N59hsv5i9yzRDfcYwwt0l04+VqnKC+EwzvJZIP/qkuMgR/w==} - deprecated: See https://github.com/lydell/source-map-resolve#deprecated - source-map-support@0.5.21: resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} @@ -5751,8 +5715,8 @@ packages: stylis@4.2.0: resolution: {integrity: sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==} - stylus@0.57.0: - resolution: {integrity: sha512-yOI6G8WYfr0q8v8rRvE91wbxFU+rJPo760Va4MF6K0I6BZjO4r+xSynkvyPBP9tV1CIEUeRsiidjIs2rzb1CnQ==} + stylus@0.59.0: + resolution: {integrity: sha512-lQ9w/XIOH5ZHVNuNbWW8D822r+/wBSO/d6XvtyHLF7LW4KaCIDeVbvn5DF8fGCJAUCwVhVi/h6J0NUcnylUEjg==} hasBin: true super-regex@1.1.0: @@ -5766,10 +5730,6 @@ packages: resolution: {integrity: sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==} engines: {node: '>=16'} - supports-color@7.2.0: - resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} - engines: {node: '>=8'} - supports-preserve-symlinks-flag@1.0.0: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} @@ -6168,7 +6128,6 @@ packages: engines: {node: '>=v14.21.3'} peerDependencies: vite: ^8.0.16 - vue: '*' vite-plugin-vue-inspector@6.0.0: resolution: {integrity: sha512-OpyITJLgZNibxlrik1EmRtvXHDjLRxNPsWkGFTERZs2LgMEdG4W0WoFt5GIgp3a3jRou+eJR8U1zOBk/XQgEbw==} @@ -6187,7 +6146,7 @@ packages: less: ^4.0.0 sass: ^1.70.0 sass-embedded: ^1.70.0 - stylus: '>=0.54.8' + stylus: 0.59.0 sugarss: ^5.0.0 terser: ^5.16.0 tsx: ^4.8.1 @@ -6288,12 +6247,6 @@ packages: peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 - vue-i18n@11.4.13: - resolution: {integrity: sha512-2G7LC9XcdZ0IljFib7lKGHNiBBOTv6HO+2bnBaOiQgDYZ+uUnqSpuED5DbBo1t7su50o/WRGn+ZfDQTGd26xEw==} - engines: {node: '>= 22'} - peerDependencies: - vue: ^3.0.0 - vue-i18n@11.4.16: resolution: {integrity: sha512-Kv9tp6iTmaAeOwop7hkAo28N1IBsbSVpnxyE26b6gqc4Q2E9Ke3ZHHA3oqqwS6/cYLT1henZU4etTaLQxrK98Q==} engines: {node: '>= 22'} @@ -6579,6 +6532,8 @@ packages: snapshots: + '@adobe/css-tools@4.5.0': {} + '@antfu/install-pkg@1.1.0': dependencies: package-manager-detector: 1.8.0 @@ -6618,20 +6573,20 @@ snapshots: '@babel/compat-data@7.29.7': {} - '@babel/core@7.29.7(supports-color@7.2.0)': + '@babel/core@7.29.7': dependencies: '@babel/code-frame': 7.29.7 '@babel/generator': 7.29.7 '@babel/helper-compilation-targets': 7.29.7 - '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0) + '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7) '@babel/helpers': 7.29.7 '@babel/parser': 7.29.8 '@babel/template': 7.29.7 - '@babel/traverse': 7.29.7(supports-color@7.2.0) + '@babel/traverse': 7.29.7 '@babel/types': 7.29.8 '@jridgewell/remapping': 2.3.5 convert-source-map: 2.0.0 - debug: 4.4.3(supports-color@7.2.0) + debug: 4.4.3 gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -6658,32 +6613,32 @@ snapshots: lru-cache: 5.1.1 semver: 6.3.1 - '@babel/helper-create-class-features-plugin@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0)': + '@babel/helper-create-class-features-plugin@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.7(supports-color@7.2.0) + '@babel/core': 7.29.7 '@babel/helper-annotate-as-pure': 7.29.7 - '@babel/helper-member-expression-to-functions': 7.29.7(supports-color@7.2.0) + '@babel/helper-member-expression-to-functions': 7.29.7 '@babel/helper-optimise-call-expression': 7.29.7 - '@babel/helper-replace-supers': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0) - '@babel/helper-skip-transparent-expression-wrappers': 7.29.7(supports-color@7.2.0) - '@babel/traverse': 7.29.7(supports-color@7.2.0) + '@babel/helper-replace-supers': 7.29.7(@babel/core@7.29.7) + '@babel/helper-skip-transparent-expression-wrappers': 7.29.7 + '@babel/traverse': 7.29.7 semver: 6.3.1 transitivePeerDependencies: - supports-color - '@babel/helper-create-regexp-features-plugin@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))': + '@babel/helper-create-regexp-features-plugin@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.7(supports-color@7.2.0) + '@babel/core': 7.29.7 '@babel/helper-annotate-as-pure': 7.29.7 regexpu-core: 6.4.0 semver: 6.3.1 - '@babel/helper-define-polyfill-provider@0.6.8(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0)': + '@babel/helper-define-polyfill-provider@0.6.8(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.7(supports-color@7.2.0) + '@babel/core': 7.29.7 '@babel/helper-compilation-targets': 7.29.7 '@babel/helper-plugin-utils': 7.29.7 - debug: 4.4.3(supports-color@7.2.0) + debug: 4.4.3 lodash.debounce: 4.0.8 resolve: 1.22.12 transitivePeerDependencies: @@ -6691,26 +6646,26 @@ snapshots: '@babel/helper-globals@7.29.7': {} - '@babel/helper-member-expression-to-functions@7.29.7(supports-color@7.2.0)': + '@babel/helper-member-expression-to-functions@7.29.7': dependencies: - '@babel/traverse': 7.29.7(supports-color@7.2.0) + '@babel/traverse': 7.29.7 '@babel/types': 7.29.8 transitivePeerDependencies: - supports-color - '@babel/helper-module-imports@7.29.7(supports-color@7.2.0)': + '@babel/helper-module-imports@7.29.7': dependencies: - '@babel/traverse': 7.29.7(supports-color@7.2.0) + '@babel/traverse': 7.29.7 '@babel/types': 7.29.8 transitivePeerDependencies: - supports-color - '@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0)': + '@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.7(supports-color@7.2.0) - '@babel/helper-module-imports': 7.29.7(supports-color@7.2.0) + '@babel/core': 7.29.7 + '@babel/helper-module-imports': 7.29.7 '@babel/helper-validator-identifier': 7.29.7 - '@babel/traverse': 7.29.7(supports-color@7.2.0) + '@babel/traverse': 7.29.7 transitivePeerDependencies: - supports-color @@ -6720,27 +6675,27 @@ snapshots: '@babel/helper-plugin-utils@7.29.7': {} - '@babel/helper-remap-async-to-generator@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0)': + '@babel/helper-remap-async-to-generator@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.7(supports-color@7.2.0) + '@babel/core': 7.29.7 '@babel/helper-annotate-as-pure': 7.29.7 - '@babel/helper-wrap-function': 7.29.7(supports-color@7.2.0) - '@babel/traverse': 7.29.7(supports-color@7.2.0) + '@babel/helper-wrap-function': 7.29.7 + '@babel/traverse': 7.29.7 transitivePeerDependencies: - supports-color - '@babel/helper-replace-supers@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0)': + '@babel/helper-replace-supers@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.7(supports-color@7.2.0) - '@babel/helper-member-expression-to-functions': 7.29.7(supports-color@7.2.0) + '@babel/core': 7.29.7 + '@babel/helper-member-expression-to-functions': 7.29.7 '@babel/helper-optimise-call-expression': 7.29.7 - '@babel/traverse': 7.29.7(supports-color@7.2.0) + '@babel/traverse': 7.29.7 transitivePeerDependencies: - supports-color - '@babel/helper-skip-transparent-expression-wrappers@7.29.7(supports-color@7.2.0)': + '@babel/helper-skip-transparent-expression-wrappers@7.29.7': dependencies: - '@babel/traverse': 7.29.7(supports-color@7.2.0) + '@babel/traverse': 7.29.7 '@babel/types': 7.29.8 transitivePeerDependencies: - supports-color @@ -6755,10 +6710,10 @@ snapshots: '@babel/helper-validator-option@7.29.7': {} - '@babel/helper-wrap-function@7.29.7(supports-color@7.2.0)': + '@babel/helper-wrap-function@7.29.7': dependencies: '@babel/template': 7.29.7 - '@babel/traverse': 7.29.7(supports-color@7.2.0) + '@babel/traverse': 7.29.7 '@babel/types': 7.29.8 transitivePeerDependencies: - supports-color @@ -6782,534 +6737,534 @@ snapshots: dependencies: '@babel/types': 8.0.6 - '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0)': + '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.7(supports-color@7.2.0) + '@babel/core': 7.29.7 '@babel/helper-plugin-utils': 7.29.7 - '@babel/traverse': 7.29.7(supports-color@7.2.0) + '@babel/traverse': 7.29.7 transitivePeerDependencies: - supports-color - '@babel/plugin-bugfix-safari-class-field-initializer-scope@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))': + '@babel/plugin-bugfix-safari-class-field-initializer-scope@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.7(supports-color@7.2.0) + '@babel/core': 7.29.7 '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))': + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.7(supports-color@7.2.0) + '@babel/core': 7.29.7 '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-bugfix-safari-rest-destructuring-rhs-array@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0)': + '@babel/plugin-bugfix-safari-rest-destructuring-rhs-array@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.7(supports-color@7.2.0) + '@babel/core': 7.29.7 '@babel/helper-plugin-utils': 7.29.7 - '@babel/helper-skip-transparent-expression-wrappers': 7.29.7(supports-color@7.2.0) + '@babel/helper-skip-transparent-expression-wrappers': 7.29.7 transitivePeerDependencies: - supports-color - '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0)': + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.7(supports-color@7.2.0) + '@babel/core': 7.29.7 '@babel/helper-plugin-utils': 7.29.7 - '@babel/helper-skip-transparent-expression-wrappers': 7.29.7(supports-color@7.2.0) - '@babel/plugin-transform-optional-chaining': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0) + '@babel/helper-skip-transparent-expression-wrappers': 7.29.7 + '@babel/plugin-transform-optional-chaining': 7.29.7(@babel/core@7.29.7) transitivePeerDependencies: - supports-color - '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0)': + '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.7(supports-color@7.2.0) + '@babel/core': 7.29.7 '@babel/helper-plugin-utils': 7.29.7 - '@babel/traverse': 7.29.7(supports-color@7.2.0) + '@babel/traverse': 7.29.7 transitivePeerDependencies: - supports-color - '@babel/plugin-proposal-decorators@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0)': + '@babel/plugin-proposal-decorators@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.7(supports-color@7.2.0) - '@babel/helper-create-class-features-plugin': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0) + '@babel/core': 7.29.7 + '@babel/helper-create-class-features-plugin': 7.29.7(@babel/core@7.29.7) '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-syntax-decorators': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0)) + '@babel/plugin-syntax-decorators': 7.29.7(@babel/core@7.29.7) transitivePeerDependencies: - supports-color - '@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2(@babel/core@7.29.7(supports-color@7.2.0))': + '@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.7(supports-color@7.2.0) + '@babel/core': 7.29.7 - '@babel/plugin-syntax-decorators@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))': + '@babel/plugin-syntax-decorators@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.7(supports-color@7.2.0) + '@babel/core': 7.29.7 '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-syntax-import-assertions@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))': + '@babel/plugin-syntax-import-assertions@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.7(supports-color@7.2.0) + '@babel/core': 7.29.7 '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-syntax-import-attributes@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))': + '@babel/plugin-syntax-import-attributes@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.7(supports-color@7.2.0) + '@babel/core': 7.29.7 '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.29.7(supports-color@7.2.0))': + '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.7(supports-color@7.2.0) + '@babel/core': 7.29.7 '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))': + '@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.7(supports-color@7.2.0) + '@babel/core': 7.29.7 '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))': + '@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.7(supports-color@7.2.0) + '@babel/core': 7.29.7 '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-syntax-unicode-sets-regex@7.18.6(@babel/core@7.29.7(supports-color@7.2.0))': + '@babel/plugin-syntax-unicode-sets-regex@7.18.6(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.7(supports-color@7.2.0) - '@babel/helper-create-regexp-features-plugin': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0)) + '@babel/core': 7.29.7 + '@babel/helper-create-regexp-features-plugin': 7.29.7(@babel/core@7.29.7) '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-arrow-functions@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))': + '@babel/plugin-transform-arrow-functions@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.7(supports-color@7.2.0) + '@babel/core': 7.29.7 '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-async-generator-functions@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0)': + '@babel/plugin-transform-async-generator-functions@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.7(supports-color@7.2.0) + '@babel/core': 7.29.7 '@babel/helper-plugin-utils': 7.29.7 - '@babel/helper-remap-async-to-generator': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0) - '@babel/traverse': 7.29.7(supports-color@7.2.0) + '@babel/helper-remap-async-to-generator': 7.29.7(@babel/core@7.29.7) + '@babel/traverse': 7.29.7 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-async-to-generator@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0)': + '@babel/plugin-transform-async-to-generator@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.7(supports-color@7.2.0) - '@babel/helper-module-imports': 7.29.7(supports-color@7.2.0) + '@babel/core': 7.29.7 + '@babel/helper-module-imports': 7.29.7 '@babel/helper-plugin-utils': 7.29.7 - '@babel/helper-remap-async-to-generator': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0) + '@babel/helper-remap-async-to-generator': 7.29.7(@babel/core@7.29.7) transitivePeerDependencies: - supports-color - '@babel/plugin-transform-block-scoped-functions@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))': + '@babel/plugin-transform-block-scoped-functions@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.7(supports-color@7.2.0) + '@babel/core': 7.29.7 '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-block-scoping@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))': + '@babel/plugin-transform-block-scoping@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.7(supports-color@7.2.0) + '@babel/core': 7.29.7 '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-class-properties@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0)': + '@babel/plugin-transform-class-properties@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.7(supports-color@7.2.0) - '@babel/helper-create-class-features-plugin': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0) + '@babel/core': 7.29.7 + '@babel/helper-create-class-features-plugin': 7.29.7(@babel/core@7.29.7) '@babel/helper-plugin-utils': 7.29.7 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-class-static-block@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0)': + '@babel/plugin-transform-class-static-block@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.7(supports-color@7.2.0) - '@babel/helper-create-class-features-plugin': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0) + '@babel/core': 7.29.7 + '@babel/helper-create-class-features-plugin': 7.29.7(@babel/core@7.29.7) '@babel/helper-plugin-utils': 7.29.7 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-classes@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0)': + '@babel/plugin-transform-classes@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.7(supports-color@7.2.0) + '@babel/core': 7.29.7 '@babel/helper-annotate-as-pure': 7.29.7 '@babel/helper-compilation-targets': 7.29.7 '@babel/helper-globals': 7.29.7 '@babel/helper-plugin-utils': 7.29.7 - '@babel/helper-replace-supers': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0) - '@babel/traverse': 7.29.7(supports-color@7.2.0) + '@babel/helper-replace-supers': 7.29.7(@babel/core@7.29.7) + '@babel/traverse': 7.29.7 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-computed-properties@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))': + '@babel/plugin-transform-computed-properties@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.7(supports-color@7.2.0) + '@babel/core': 7.29.7 '@babel/helper-plugin-utils': 7.29.7 '@babel/template': 7.29.7 - '@babel/plugin-transform-destructuring@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0)': + '@babel/plugin-transform-destructuring@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.7(supports-color@7.2.0) + '@babel/core': 7.29.7 '@babel/helper-plugin-utils': 7.29.7 - '@babel/traverse': 7.29.7(supports-color@7.2.0) + '@babel/traverse': 7.29.7 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-dotall-regex@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))': + '@babel/plugin-transform-dotall-regex@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.7(supports-color@7.2.0) - '@babel/helper-create-regexp-features-plugin': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0)) + '@babel/core': 7.29.7 + '@babel/helper-create-regexp-features-plugin': 7.29.7(@babel/core@7.29.7) '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-duplicate-keys@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))': + '@babel/plugin-transform-duplicate-keys@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.7(supports-color@7.2.0) + '@babel/core': 7.29.7 '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))': + '@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.7(supports-color@7.2.0) - '@babel/helper-create-regexp-features-plugin': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0)) + '@babel/core': 7.29.7 + '@babel/helper-create-regexp-features-plugin': 7.29.7(@babel/core@7.29.7) '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-dynamic-import@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))': + '@babel/plugin-transform-dynamic-import@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.7(supports-color@7.2.0) + '@babel/core': 7.29.7 '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-explicit-resource-management@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0)': + '@babel/plugin-transform-explicit-resource-management@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.7(supports-color@7.2.0) + '@babel/core': 7.29.7 '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-destructuring': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0) + '@babel/plugin-transform-destructuring': 7.29.7(@babel/core@7.29.7) transitivePeerDependencies: - supports-color - '@babel/plugin-transform-exponentiation-operator@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))': + '@babel/plugin-transform-exponentiation-operator@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.7(supports-color@7.2.0) + '@babel/core': 7.29.7 '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-export-namespace-from@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))': + '@babel/plugin-transform-export-namespace-from@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.7(supports-color@7.2.0) + '@babel/core': 7.29.7 '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-for-of@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0)': + '@babel/plugin-transform-for-of@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.7(supports-color@7.2.0) + '@babel/core': 7.29.7 '@babel/helper-plugin-utils': 7.29.7 - '@babel/helper-skip-transparent-expression-wrappers': 7.29.7(supports-color@7.2.0) + '@babel/helper-skip-transparent-expression-wrappers': 7.29.7 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-function-name@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0)': + '@babel/plugin-transform-function-name@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.7(supports-color@7.2.0) + '@babel/core': 7.29.7 '@babel/helper-compilation-targets': 7.29.7 '@babel/helper-plugin-utils': 7.29.7 - '@babel/traverse': 7.29.7(supports-color@7.2.0) + '@babel/traverse': 7.29.7 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-json-strings@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))': + '@babel/plugin-transform-json-strings@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.7(supports-color@7.2.0) + '@babel/core': 7.29.7 '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-literals@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))': + '@babel/plugin-transform-literals@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.7(supports-color@7.2.0) + '@babel/core': 7.29.7 '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-logical-assignment-operators@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))': + '@babel/plugin-transform-logical-assignment-operators@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.7(supports-color@7.2.0) + '@babel/core': 7.29.7 '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-member-expression-literals@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))': + '@babel/plugin-transform-member-expression-literals@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.7(supports-color@7.2.0) + '@babel/core': 7.29.7 '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-modules-amd@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0)': + '@babel/plugin-transform-modules-amd@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.7(supports-color@7.2.0) - '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0) + '@babel/core': 7.29.7 + '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7) '@babel/helper-plugin-utils': 7.29.7 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-modules-commonjs@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0)': + '@babel/plugin-transform-modules-commonjs@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.7(supports-color@7.2.0) - '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0) + '@babel/core': 7.29.7 + '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7) '@babel/helper-plugin-utils': 7.29.7 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-modules-systemjs@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0)': + '@babel/plugin-transform-modules-systemjs@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.7(supports-color@7.2.0) - '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0) + '@babel/core': 7.29.7 + '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7) '@babel/helper-plugin-utils': 7.29.7 '@babel/helper-validator-identifier': 7.29.7 - '@babel/traverse': 7.29.7(supports-color@7.2.0) + '@babel/traverse': 7.29.7 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-modules-umd@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0)': + '@babel/plugin-transform-modules-umd@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.7(supports-color@7.2.0) - '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0) + '@babel/core': 7.29.7 + '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7) '@babel/helper-plugin-utils': 7.29.7 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-named-capturing-groups-regex@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))': + '@babel/plugin-transform-named-capturing-groups-regex@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.7(supports-color@7.2.0) - '@babel/helper-create-regexp-features-plugin': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0)) + '@babel/core': 7.29.7 + '@babel/helper-create-regexp-features-plugin': 7.29.7(@babel/core@7.29.7) '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-new-target@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))': + '@babel/plugin-transform-new-target@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.7(supports-color@7.2.0) + '@babel/core': 7.29.7 '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-nullish-coalescing-operator@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))': + '@babel/plugin-transform-nullish-coalescing-operator@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.7(supports-color@7.2.0) + '@babel/core': 7.29.7 '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-numeric-separator@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))': + '@babel/plugin-transform-numeric-separator@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.7(supports-color@7.2.0) + '@babel/core': 7.29.7 '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-object-rest-spread@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0)': + '@babel/plugin-transform-object-rest-spread@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.7(supports-color@7.2.0) + '@babel/core': 7.29.7 '@babel/helper-compilation-targets': 7.29.7 '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-destructuring': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0) - '@babel/plugin-transform-parameters': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0)) - '@babel/traverse': 7.29.7(supports-color@7.2.0) + '@babel/plugin-transform-destructuring': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-parameters': 7.29.7(@babel/core@7.29.7) + '@babel/traverse': 7.29.7 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-object-super@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0)': + '@babel/plugin-transform-object-super@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.7(supports-color@7.2.0) + '@babel/core': 7.29.7 '@babel/helper-plugin-utils': 7.29.7 - '@babel/helper-replace-supers': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0) + '@babel/helper-replace-supers': 7.29.7(@babel/core@7.29.7) transitivePeerDependencies: - supports-color - '@babel/plugin-transform-optional-catch-binding@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))': + '@babel/plugin-transform-optional-catch-binding@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.7(supports-color@7.2.0) + '@babel/core': 7.29.7 '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-optional-chaining@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0)': + '@babel/plugin-transform-optional-chaining@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.7(supports-color@7.2.0) + '@babel/core': 7.29.7 '@babel/helper-plugin-utils': 7.29.7 - '@babel/helper-skip-transparent-expression-wrappers': 7.29.7(supports-color@7.2.0) + '@babel/helper-skip-transparent-expression-wrappers': 7.29.7 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-parameters@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))': + '@babel/plugin-transform-parameters@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.7(supports-color@7.2.0) + '@babel/core': 7.29.7 '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-private-methods@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0)': + '@babel/plugin-transform-private-methods@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.7(supports-color@7.2.0) - '@babel/helper-create-class-features-plugin': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0) + '@babel/core': 7.29.7 + '@babel/helper-create-class-features-plugin': 7.29.7(@babel/core@7.29.7) '@babel/helper-plugin-utils': 7.29.7 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-private-property-in-object@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0)': + '@babel/plugin-transform-private-property-in-object@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.7(supports-color@7.2.0) + '@babel/core': 7.29.7 '@babel/helper-annotate-as-pure': 7.29.7 - '@babel/helper-create-class-features-plugin': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0) + '@babel/helper-create-class-features-plugin': 7.29.7(@babel/core@7.29.7) '@babel/helper-plugin-utils': 7.29.7 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-property-literals@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))': + '@babel/plugin-transform-property-literals@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.7(supports-color@7.2.0) + '@babel/core': 7.29.7 '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-regenerator@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))': + '@babel/plugin-transform-regenerator@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.7(supports-color@7.2.0) + '@babel/core': 7.29.7 '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-regexp-modifiers@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))': + '@babel/plugin-transform-regexp-modifiers@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.7(supports-color@7.2.0) - '@babel/helper-create-regexp-features-plugin': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0)) + '@babel/core': 7.29.7 + '@babel/helper-create-regexp-features-plugin': 7.29.7(@babel/core@7.29.7) '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-reserved-words@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))': + '@babel/plugin-transform-reserved-words@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.7(supports-color@7.2.0) + '@babel/core': 7.29.7 '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-shorthand-properties@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))': + '@babel/plugin-transform-shorthand-properties@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.7(supports-color@7.2.0) + '@babel/core': 7.29.7 '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-spread@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0)': + '@babel/plugin-transform-spread@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.7(supports-color@7.2.0) + '@babel/core': 7.29.7 '@babel/helper-plugin-utils': 7.29.7 - '@babel/helper-skip-transparent-expression-wrappers': 7.29.7(supports-color@7.2.0) + '@babel/helper-skip-transparent-expression-wrappers': 7.29.7 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-sticky-regex@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))': + '@babel/plugin-transform-sticky-regex@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.7(supports-color@7.2.0) + '@babel/core': 7.29.7 '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-template-literals@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))': + '@babel/plugin-transform-template-literals@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.7(supports-color@7.2.0) + '@babel/core': 7.29.7 '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-typeof-symbol@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))': + '@babel/plugin-transform-typeof-symbol@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.7(supports-color@7.2.0) + '@babel/core': 7.29.7 '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-typescript@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0)': + '@babel/plugin-transform-typescript@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.7(supports-color@7.2.0) + '@babel/core': 7.29.7 '@babel/helper-annotate-as-pure': 7.29.7 - '@babel/helper-create-class-features-plugin': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0) + '@babel/helper-create-class-features-plugin': 7.29.7(@babel/core@7.29.7) '@babel/helper-plugin-utils': 7.29.7 - '@babel/helper-skip-transparent-expression-wrappers': 7.29.7(supports-color@7.2.0) - '@babel/plugin-syntax-typescript': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0)) + '@babel/helper-skip-transparent-expression-wrappers': 7.29.7 + '@babel/plugin-syntax-typescript': 7.29.7(@babel/core@7.29.7) transitivePeerDependencies: - supports-color - '@babel/plugin-transform-unicode-escapes@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))': + '@babel/plugin-transform-unicode-escapes@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.7(supports-color@7.2.0) + '@babel/core': 7.29.7 '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-unicode-property-regex@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))': + '@babel/plugin-transform-unicode-property-regex@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.7(supports-color@7.2.0) - '@babel/helper-create-regexp-features-plugin': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0)) + '@babel/core': 7.29.7 + '@babel/helper-create-regexp-features-plugin': 7.29.7(@babel/core@7.29.7) '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-unicode-regex@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))': + '@babel/plugin-transform-unicode-regex@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.7(supports-color@7.2.0) - '@babel/helper-create-regexp-features-plugin': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0)) + '@babel/core': 7.29.7 + '@babel/helper-create-regexp-features-plugin': 7.29.7(@babel/core@7.29.7) '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-unicode-sets-regex@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))': + '@babel/plugin-transform-unicode-sets-regex@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.7(supports-color@7.2.0) - '@babel/helper-create-regexp-features-plugin': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0)) + '@babel/core': 7.29.7 + '@babel/helper-create-regexp-features-plugin': 7.29.7(@babel/core@7.29.7) '@babel/helper-plugin-utils': 7.29.7 - '@babel/preset-env@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0)': + '@babel/preset-env@7.29.7(@babel/core@7.29.7)': dependencies: '@babel/compat-data': 7.29.7 - '@babel/core': 7.29.7(supports-color@7.2.0) + '@babel/core': 7.29.7 '@babel/helper-compilation-targets': 7.29.7 '@babel/helper-plugin-utils': 7.29.7 '@babel/helper-validator-option': 7.29.7 - '@babel/plugin-bugfix-firefox-class-in-computed-class-key': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0) - '@babel/plugin-bugfix-safari-class-field-initializer-scope': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0)) - '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0)) - '@babel/plugin-bugfix-safari-rest-destructuring-rhs-array': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0) - '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0) - '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0) - '@babel/plugin-proposal-private-property-in-object': 7.21.0-placeholder-for-preset-env.2(@babel/core@7.29.7(supports-color@7.2.0)) - '@babel/plugin-syntax-import-assertions': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0)) - '@babel/plugin-syntax-import-attributes': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0)) - '@babel/plugin-syntax-unicode-sets-regex': 7.18.6(@babel/core@7.29.7(supports-color@7.2.0)) - '@babel/plugin-transform-arrow-functions': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0)) - '@babel/plugin-transform-async-generator-functions': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0) - '@babel/plugin-transform-async-to-generator': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0) - '@babel/plugin-transform-block-scoped-functions': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0)) - '@babel/plugin-transform-block-scoping': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0)) - '@babel/plugin-transform-class-properties': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0) - '@babel/plugin-transform-class-static-block': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0) - '@babel/plugin-transform-classes': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0) - '@babel/plugin-transform-computed-properties': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0)) - '@babel/plugin-transform-destructuring': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0) - '@babel/plugin-transform-dotall-regex': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0)) - '@babel/plugin-transform-duplicate-keys': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0)) - '@babel/plugin-transform-duplicate-named-capturing-groups-regex': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0)) - '@babel/plugin-transform-dynamic-import': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0)) - '@babel/plugin-transform-explicit-resource-management': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0) - '@babel/plugin-transform-exponentiation-operator': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0)) - '@babel/plugin-transform-export-namespace-from': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0)) - '@babel/plugin-transform-for-of': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0) - '@babel/plugin-transform-function-name': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0) - '@babel/plugin-transform-json-strings': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0)) - '@babel/plugin-transform-literals': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0)) - '@babel/plugin-transform-logical-assignment-operators': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0)) - '@babel/plugin-transform-member-expression-literals': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0)) - '@babel/plugin-transform-modules-amd': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0) - '@babel/plugin-transform-modules-commonjs': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0) - '@babel/plugin-transform-modules-systemjs': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0) - '@babel/plugin-transform-modules-umd': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0) - '@babel/plugin-transform-named-capturing-groups-regex': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0)) - '@babel/plugin-transform-new-target': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0)) - '@babel/plugin-transform-nullish-coalescing-operator': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0)) - '@babel/plugin-transform-numeric-separator': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0)) - '@babel/plugin-transform-object-rest-spread': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0) - '@babel/plugin-transform-object-super': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0) - '@babel/plugin-transform-optional-catch-binding': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0)) - '@babel/plugin-transform-optional-chaining': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0) - '@babel/plugin-transform-parameters': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0)) - '@babel/plugin-transform-private-methods': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0) - '@babel/plugin-transform-private-property-in-object': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0) - '@babel/plugin-transform-property-literals': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0)) - '@babel/plugin-transform-regenerator': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0)) - '@babel/plugin-transform-regexp-modifiers': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0)) - '@babel/plugin-transform-reserved-words': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0)) - '@babel/plugin-transform-shorthand-properties': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0)) - '@babel/plugin-transform-spread': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0) - '@babel/plugin-transform-sticky-regex': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0)) - '@babel/plugin-transform-template-literals': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0)) - '@babel/plugin-transform-typeof-symbol': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0)) - '@babel/plugin-transform-unicode-escapes': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0)) - '@babel/plugin-transform-unicode-property-regex': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0)) - '@babel/plugin-transform-unicode-regex': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0)) - '@babel/plugin-transform-unicode-sets-regex': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0)) - '@babel/preset-modules': 0.1.6-no-external-plugins(@babel/core@7.29.7(supports-color@7.2.0)) - babel-plugin-polyfill-corejs2: 0.4.17(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0) - babel-plugin-polyfill-corejs3: 0.14.2(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0) - babel-plugin-polyfill-regenerator: 0.6.8(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0) + '@babel/plugin-bugfix-firefox-class-in-computed-class-key': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-bugfix-safari-class-field-initializer-scope': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-bugfix-safari-rest-destructuring-rhs-array': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-proposal-private-property-in-object': 7.21.0-placeholder-for-preset-env.2(@babel/core@7.29.7) + '@babel/plugin-syntax-import-assertions': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-syntax-import-attributes': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-syntax-unicode-sets-regex': 7.18.6(@babel/core@7.29.7) + '@babel/plugin-transform-arrow-functions': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-async-generator-functions': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-async-to-generator': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-block-scoped-functions': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-block-scoping': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-class-properties': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-class-static-block': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-classes': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-computed-properties': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-destructuring': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-dotall-regex': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-duplicate-keys': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-duplicate-named-capturing-groups-regex': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-dynamic-import': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-explicit-resource-management': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-exponentiation-operator': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-export-namespace-from': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-for-of': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-function-name': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-json-strings': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-literals': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-logical-assignment-operators': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-member-expression-literals': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-modules-amd': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-modules-commonjs': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-modules-systemjs': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-modules-umd': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-named-capturing-groups-regex': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-new-target': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-nullish-coalescing-operator': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-numeric-separator': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-object-rest-spread': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-object-super': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-optional-catch-binding': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-optional-chaining': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-parameters': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-private-methods': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-private-property-in-object': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-property-literals': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-regenerator': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-regexp-modifiers': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-reserved-words': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-shorthand-properties': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-spread': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-sticky-regex': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-template-literals': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-typeof-symbol': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-unicode-escapes': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-unicode-property-regex': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-unicode-regex': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-unicode-sets-regex': 7.29.7(@babel/core@7.29.7) + '@babel/preset-modules': 0.1.6-no-external-plugins(@babel/core@7.29.7) + babel-plugin-polyfill-corejs2: 0.4.17(@babel/core@7.29.7) + babel-plugin-polyfill-corejs3: 0.14.2(@babel/core@7.29.7) + babel-plugin-polyfill-regenerator: 0.6.8(@babel/core@7.29.7) core-js-compat: 3.49.0 semver: 6.3.1 transitivePeerDependencies: - supports-color - '@babel/preset-modules@0.1.6-no-external-plugins(@babel/core@7.29.7(supports-color@7.2.0))': + '@babel/preset-modules@0.1.6-no-external-plugins(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.7(supports-color@7.2.0) + '@babel/core': 7.29.7 '@babel/helper-plugin-utils': 7.29.7 '@babel/types': 7.29.8 esutils: 2.0.3 - '@babel/preset-typescript@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0)': + '@babel/preset-typescript@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.7(supports-color@7.2.0) + '@babel/core': 7.29.7 '@babel/helper-plugin-utils': 7.29.7 '@babel/helper-validator-option': 7.29.7 - '@babel/plugin-syntax-jsx': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0)) - '@babel/plugin-transform-modules-commonjs': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0) - '@babel/plugin-transform-typescript': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0) + '@babel/plugin-syntax-jsx': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-modules-commonjs': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-typescript': 7.29.7(@babel/core@7.29.7) transitivePeerDependencies: - supports-color @@ -7321,7 +7276,7 @@ snapshots: '@babel/parser': 7.29.8 '@babel/types': 7.29.8 - '@babel/traverse@7.29.7(supports-color@7.2.0)': + '@babel/traverse@7.29.7': dependencies: '@babel/code-frame': 7.29.7 '@babel/generator': 7.29.7 @@ -7329,7 +7284,7 @@ snapshots: '@babel/parser': 7.29.8 '@babel/template': 7.29.7 '@babel/types': 7.29.8 - debug: 4.4.3(supports-color@7.2.0) + debug: 4.4.3 transitivePeerDependencies: - supports-color @@ -7474,9 +7429,9 @@ snapshots: tslib: 2.8.1 optional: true - '@emotion/babel-plugin@11.13.5(supports-color@7.2.0)': + '@emotion/babel-plugin@11.13.5': dependencies: - '@babel/helper-module-imports': 7.29.7(supports-color@7.2.0) + '@babel/helper-module-imports': 7.29.7 '@babel/runtime': 7.29.7 '@emotion/hash': 0.9.2 '@emotion/memoize': 0.9.0 @@ -7498,9 +7453,9 @@ snapshots: '@emotion/weak-memoize': 0.4.0 stylis: 4.2.0 - '@emotion/css@11.13.5(supports-color@7.2.0)': + '@emotion/css@11.13.5': dependencies: - '@emotion/babel-plugin': 11.13.5(supports-color@7.2.0) + '@emotion/babel-plugin': 11.13.5 '@emotion/cache': 11.14.0 '@emotion/serialize': 1.3.3 '@emotion/sheet': 1.4.0 @@ -7528,17 +7483,17 @@ snapshots: '@emotion/weak-memoize@0.4.0': {} - '@eslint-community/eslint-utils@4.9.1(eslint@10.13.0(jiti@2.7.0)(supports-color@7.2.0))': + '@eslint-community/eslint-utils@4.9.1(eslint@10.13.0(jiti@2.7.0))': dependencies: - eslint: 10.13.0(jiti@2.7.0)(supports-color@7.2.0) + eslint: 10.13.0(jiti@2.7.0) eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.2': {} - '@eslint/config-array@0.23.5(supports-color@7.2.0)': + '@eslint/config-array@0.23.5': dependencies: '@eslint/object-schema': 3.0.5 - debug: 4.4.3(supports-color@7.2.0) + debug: 4.4.3 minimatch: 10.2.5 transitivePeerDependencies: - supports-color @@ -7627,40 +7582,22 @@ snapshots: dependencies: '@swc/helpers': 0.5.23 - '@intlify/core-base@11.4.13': - dependencies: - '@intlify/devtools-types': 11.4.13 - '@intlify/message-compiler': 11.4.13 - '@intlify/shared': 11.4.13 - '@intlify/core-base@11.4.16': dependencies: '@intlify/devtools-types': 11.4.16 '@intlify/message-compiler': 11.4.16 '@intlify/shared': 11.4.16 - '@intlify/devtools-types@11.4.13': - dependencies: - '@intlify/core-base': 11.4.13 - '@intlify/shared': 11.4.13 - '@intlify/devtools-types@11.4.16': dependencies: '@intlify/core-base': 11.4.16 '@intlify/shared': 11.4.16 - '@intlify/message-compiler@11.4.13': - dependencies: - '@intlify/shared': 11.4.13 - source-map-js: 1.2.2 - '@intlify/message-compiler@11.4.16': dependencies: '@intlify/shared': 11.4.16 source-map-js: 1.2.2 - '@intlify/shared@11.4.13': {} - '@intlify/shared@11.4.16': {} '@isaacs/cliui@9.0.0': {} @@ -7768,7 +7705,7 @@ snapshots: optionalDependencies: markdown-it: 15.0.3 - '@modelcontextprotocol/sdk@1.32.1(supports-color@7.2.0)(zod@3.25.76)': + '@modelcontextprotocol/sdk@1.32.1(zod@3.25.76)': dependencies: '@hono/node-server': 2.1.5(hono@4.13.13) ajv: 8.20.0 @@ -7778,8 +7715,8 @@ snapshots: cross-spawn: 7.0.6 eventsource: 3.0.7 eventsource-parser: 3.1.1 - express: 5.3.0(supports-color@7.2.0) - express-rate-limit: 8.7.1(express@5.3.0(supports-color@7.2.0))(supports-color@7.2.0) + express: 5.3.0 + express-rate-limit: 8.7.1(express@5.3.0) hono: 4.13.13 jose: 6.2.12 json-schema-typed: 8.0.2 @@ -7992,10 +7929,10 @@ snapshots: '@rolldown/pluginutils@1.0.1': {} - '@rollup/plugin-babel@6.1.0(@babel/core@7.29.7(supports-color@7.2.0))(rollup@4.62.0)(supports-color@7.2.0)': + '@rollup/plugin-babel@6.1.0(@babel/core@7.29.7)(rollup@4.62.0)': dependencies: - '@babel/core': 7.29.7(supports-color@7.2.0) - '@babel/helper-module-imports': 7.29.7(supports-color@7.2.0) + '@babel/core': 7.29.7 + '@babel/helper-module-imports': 7.29.7 '@rollup/pluginutils': 5.4.0(rollup@4.62.0) optionalDependencies: rollup: 4.62.0 @@ -8194,12 +8131,12 @@ snapshots: postcss-selector-parser: 6.0.10 tailwindcss: 4.3.3 - '@tailwindcss/vite@4.3.3(vite@8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.57.0(supports-color@7.2.0))(terser@5.48.0)(yaml@2.9.1))': + '@tailwindcss/vite@4.3.3(vite@8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.59.0)(terser@5.48.0)(yaml@2.9.1))': dependencies: '@tailwindcss/node': 4.3.3 '@tailwindcss/oxide': 4.3.3 tailwindcss: 4.3.3 - vite: 8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.57.0(supports-color@7.2.0))(terser@5.48.0)(yaml@2.9.1) + vite: 8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.59.0)(terser@5.48.0)(yaml@2.9.1) '@tanstack/table-core@8.21.3': {} @@ -8455,15 +8392,15 @@ snapshots: dependencies: '@types/node': 26.6.5 - '@typescript-eslint/eslint-plugin@8.61.1(@typescript-eslint/parser@8.61.1(eslint@10.13.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint@10.13.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)': + '@typescript-eslint/eslint-plugin@8.61.1(@typescript-eslint/parser@8.61.1(eslint@10.13.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.13.0(jiti@2.7.0))(typescript@6.0.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.61.1(eslint@10.13.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) + '@typescript-eslint/parser': 8.61.1(eslint@10.13.0(jiti@2.7.0))(typescript@6.0.3) '@typescript-eslint/scope-manager': 8.61.1 - '@typescript-eslint/type-utils': 8.61.1(eslint@10.13.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) - '@typescript-eslint/utils': 8.61.1(eslint@10.13.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) + '@typescript-eslint/type-utils': 8.61.1(eslint@10.13.0(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/utils': 8.61.1(eslint@10.13.0(jiti@2.7.0))(typescript@6.0.3) '@typescript-eslint/visitor-keys': 8.61.1 - eslint: 10.13.0(jiti@2.7.0)(supports-color@7.2.0) + eslint: 10.13.0(jiti@2.7.0) ignore: 7.0.5 natural-compare: 1.4.0 ts-api-utils: 2.5.0(typescript@6.0.3) @@ -8471,23 +8408,23 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.61.1(eslint@10.13.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)': + '@typescript-eslint/parser@8.61.1(eslint@10.13.0(jiti@2.7.0))(typescript@6.0.3)': dependencies: '@typescript-eslint/scope-manager': 8.61.1 '@typescript-eslint/types': 8.61.1 - '@typescript-eslint/typescript-estree': 8.61.1(supports-color@7.2.0)(typescript@6.0.3) + '@typescript-eslint/typescript-estree': 8.61.1(typescript@6.0.3) '@typescript-eslint/visitor-keys': 8.61.1 - debug: 4.4.3(supports-color@7.2.0) - eslint: 10.13.0(jiti@2.7.0)(supports-color@7.2.0) + debug: 4.4.3 + eslint: 10.13.0(jiti@2.7.0) typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.61.1(supports-color@7.2.0)(typescript@6.0.3)': + '@typescript-eslint/project-service@8.61.1(typescript@6.0.3)': dependencies: '@typescript-eslint/tsconfig-utils': 8.61.1(typescript@6.0.3) '@typescript-eslint/types': 8.61.1 - debug: 4.4.3(supports-color@7.2.0) + debug: 4.4.3 typescript: 6.0.3 transitivePeerDependencies: - supports-color @@ -8501,13 +8438,13 @@ snapshots: dependencies: typescript: 6.0.3 - '@typescript-eslint/type-utils@8.61.1(eslint@10.13.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)': + '@typescript-eslint/type-utils@8.61.1(eslint@10.13.0(jiti@2.7.0))(typescript@6.0.3)': dependencies: '@typescript-eslint/types': 8.61.1 - '@typescript-eslint/typescript-estree': 8.61.1(supports-color@7.2.0)(typescript@6.0.3) - '@typescript-eslint/utils': 8.61.1(eslint@10.13.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) - debug: 4.4.3(supports-color@7.2.0) - eslint: 10.13.0(jiti@2.7.0)(supports-color@7.2.0) + '@typescript-eslint/typescript-estree': 8.61.1(typescript@6.0.3) + '@typescript-eslint/utils': 8.61.1(eslint@10.13.0(jiti@2.7.0))(typescript@6.0.3) + debug: 4.4.3 + eslint: 10.13.0(jiti@2.7.0) ts-api-utils: 2.5.0(typescript@6.0.3) typescript: 6.0.3 transitivePeerDependencies: @@ -8515,13 +8452,13 @@ snapshots: '@typescript-eslint/types@8.61.1': {} - '@typescript-eslint/typescript-estree@8.61.1(supports-color@7.2.0)(typescript@6.0.3)': + '@typescript-eslint/typescript-estree@8.61.1(typescript@6.0.3)': dependencies: - '@typescript-eslint/project-service': 8.61.1(supports-color@7.2.0)(typescript@6.0.3) + '@typescript-eslint/project-service': 8.61.1(typescript@6.0.3) '@typescript-eslint/tsconfig-utils': 8.61.1(typescript@6.0.3) '@typescript-eslint/types': 8.61.1 '@typescript-eslint/visitor-keys': 8.61.1 - debug: 4.4.3(supports-color@7.2.0) + debug: 4.4.3 minimatch: 10.2.5 semver: 7.8.5 tinyglobby: 0.2.17 @@ -8530,13 +8467,13 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.61.1(eslint@10.13.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)': + '@typescript-eslint/utils@8.61.1(eslint@10.13.0(jiti@2.7.0))(typescript@6.0.3)': dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.13.0(jiti@2.7.0)(supports-color@7.2.0)) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.13.0(jiti@2.7.0)) '@typescript-eslint/scope-manager': 8.61.1 '@typescript-eslint/types': 8.61.1 - '@typescript-eslint/typescript-estree': 8.61.1(supports-color@7.2.0)(typescript@6.0.3) - eslint: 10.13.0(jiti@2.7.0)(supports-color@7.2.0) + '@typescript-eslint/typescript-estree': 8.61.1(typescript@6.0.3) + eslint: 10.13.0(jiti@2.7.0) typescript: 6.0.3 transitivePeerDependencies: - supports-color @@ -8555,9 +8492,9 @@ snapshots: dependencies: lodash-es: 4.18.1 - '@unovis/ts@1.7.1(supports-color@7.2.0)': + '@unovis/ts@1.7.1': dependencies: - '@emotion/css': 11.13.5(supports-color@7.2.0) + '@emotion/css': 11.13.5 '@juggle/resize-observer': 3.4.0 '@maplibre/maplibre-gl-style-spec': 26.4.1 '@types/d3': 7.4.3 @@ -8629,20 +8566,20 @@ snapshots: transitivePeerDependencies: - supports-color - '@unovis/vue@1.7.1(@unovis/ts@1.7.1(supports-color@7.2.0))(vue@3.5.44(typescript@5.9.3))': + '@unovis/vue@1.7.1(@unovis/ts@1.7.1)(vue@3.5.44(typescript@5.9.3))': dependencies: - '@unovis/ts': 1.7.1(supports-color@7.2.0) + '@unovis/ts': 1.7.1 vue: 3.5.44(typescript@5.9.3) - '@unovis/vue@1.7.1(@unovis/ts@1.7.1(supports-color@7.2.0))(vue@3.5.44(typescript@6.0.3))': + '@unovis/vue@1.7.1(@unovis/ts@1.7.1)(vue@3.5.44(typescript@6.0.3))': dependencies: - '@unovis/ts': 1.7.1(supports-color@7.2.0) + '@unovis/ts': 1.7.1 vue: 3.5.44(typescript@6.0.3) - '@unovue/detypes@0.8.5(supports-color@7.2.0)': + '@unovue/detypes@0.8.5': dependencies: - '@babel/core': 7.29.7(supports-color@7.2.0) - '@babel/preset-typescript': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0) + '@babel/core': 7.29.7 + '@babel/preset-typescript': 7.29.7(@babel/core@7.29.7) '@vue/compiler-dom': 3.5.43 '@vue/compiler-sfc': 3.5.43 '@vuedx/template-ast-types': 0.7.1 @@ -8652,28 +8589,28 @@ snapshots: transitivePeerDependencies: - supports-color - '@vitejs/plugin-vue-jsx@5.1.6(supports-color@7.2.0)(vite@8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.57.0(supports-color@7.2.0))(terser@5.48.0)(yaml@2.9.1))(vue@3.5.44(typescript@6.0.3))': + '@vitejs/plugin-vue-jsx@5.1.6(vite@8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.59.0)(terser@5.48.0)(yaml@2.9.1))(vue@3.5.44(typescript@6.0.3))': dependencies: - '@babel/core': 7.29.7(supports-color@7.2.0) - '@babel/plugin-syntax-typescript': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0)) - '@babel/plugin-transform-typescript': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0) + '@babel/core': 7.29.7 + '@babel/plugin-syntax-typescript': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-typescript': 7.29.7(@babel/core@7.29.7) '@rolldown/pluginutils': 1.0.1 - '@vue/babel-plugin-jsx': 2.0.1(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0) - vite: 8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.57.0(supports-color@7.2.0))(terser@5.48.0)(yaml@2.9.1) + '@vue/babel-plugin-jsx': 2.0.1(@babel/core@7.29.7) + vite: 8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.59.0)(terser@5.48.0)(yaml@2.9.1) vue: 3.5.44(typescript@6.0.3) transitivePeerDependencies: - supports-color - '@vitejs/plugin-vue@6.0.10(vite@8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.57.0(supports-color@7.2.0))(terser@5.48.0)(yaml@2.9.1))(vue@3.5.44(typescript@5.9.3))': + '@vitejs/plugin-vue@6.0.10(vite@8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.59.0)(terser@5.48.0)(yaml@2.9.1))(vue@3.5.44(typescript@5.9.3))': dependencies: '@rolldown/pluginutils': 1.0.1 - vite: 8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.57.0(supports-color@7.2.0))(terser@5.48.0)(yaml@2.9.1) + vite: 8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.59.0)(terser@5.48.0)(yaml@2.9.1) vue: 3.5.44(typescript@5.9.3) - '@vitejs/plugin-vue@6.0.10(vite@8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.57.0(supports-color@7.2.0))(terser@5.48.0)(yaml@2.9.1))(vue@3.5.44(typescript@6.0.3))': + '@vitejs/plugin-vue@6.0.10(vite@8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.59.0)(terser@5.48.0)(yaml@2.9.1))(vue@3.5.44(typescript@6.0.3))': dependencies: '@rolldown/pluginutils': 1.0.1 - vite: 8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.57.0(supports-color@7.2.0))(terser@5.48.0)(yaml@2.9.1) + vite: 8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.59.0)(terser@5.48.0)(yaml@2.9.1) vue: 3.5.44(typescript@6.0.3) '@vitest/coverage-v8@5.0.5(vitest@5.0.5)': @@ -8686,7 +8623,7 @@ snapshots: obug: 3.0.0 std-env: 4.3.0 tinyrainbow: 3.2.0 - vitest: 5.0.5(@types/node@26.6.5)(@vitest/coverage-v8@5.0.5)(@vitest/ui@5.0.5)(happy-dom@21.0.3)(jsdom@30.1.3)(vite@8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.57.0(supports-color@7.2.0))(terser@5.48.0)(yaml@2.9.1)) + vitest: 5.0.5(@types/node@26.6.5)(@vitest/coverage-v8@5.0.5)(@vitest/ui@5.0.5)(happy-dom@21.0.3)(jsdom@30.1.3)(vite@8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.59.0)(terser@5.48.0)(yaml@2.9.1)) '@vitest/istanbul-lib-coverage@1.0.2': {} @@ -8694,14 +8631,14 @@ snapshots: dependencies: '@vitest/istanbul-lib-coverage': 1.0.2 - '@vitest/mocker@5.0.5(vite@8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.57.0(supports-color@7.2.0))(terser@5.48.0)(yaml@2.9.1))': + '@vitest/mocker@5.0.5(vite@8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.59.0)(terser@5.48.0)(yaml@2.9.1))': dependencies: '@jridgewell/trace-mapping': 0.3.31 '@vitest/spy': 5.0.5 estree-walker: 3.0.3 magic-string: 1.4.3 optionalDependencies: - vite: 8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.57.0(supports-color@7.2.0))(terser@5.48.0)(yaml@2.9.1) + vite: 8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.59.0)(terser@5.48.0)(yaml@2.9.1) '@vitest/pretty-format@5.0.5': dependencies: @@ -8717,7 +8654,7 @@ snapshots: pathe: 2.0.3 sirv: 3.0.2 tinyrainbow: 3.2.0 - vitest: 5.0.5(@types/node@26.6.5)(@vitest/coverage-v8@5.0.5)(@vitest/ui@5.0.5)(happy-dom@21.0.3)(jsdom@30.1.3)(vite@8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.57.0(supports-color@7.2.0))(terser@5.48.0)(yaml@2.9.1)) + vitest: 5.0.5(@types/node@26.6.5)(@vitest/coverage-v8@5.0.5)(@vitest/ui@5.0.5)(happy-dom@21.0.3)(jsdom@30.1.3)(vite@8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.59.0)(terser@5.48.0)(yaml@2.9.1)) '@vitest/utils@5.0.5': dependencies: @@ -8731,21 +8668,11 @@ snapshots: '@volar/source-map@2.4.28': {} - '@volar/typescript@2.4.28(typescript@5.9.3)': - dependencies: - '@volar/language-core': 2.4.28 - path-browserify: 1.0.1 - vscode-uri: 3.1.0 - optionalDependencies: - typescript: 5.9.3 - - '@volar/typescript@2.4.28(typescript@6.0.3)': + '@volar/typescript@2.4.28': dependencies: '@volar/language-core': 2.4.28 path-browserify: 1.0.1 vscode-uri: 3.1.0 - optionalDependencies: - typescript: 6.0.3 '@vue-dnd-kit/core@1.7.0(@vueuse/core@14.4.0(vue@3.5.44(typescript@6.0.3)))(vue@3.5.44(typescript@6.0.3))': dependencies: @@ -8776,54 +8703,54 @@ snapshots: '@vue/babel-helper-vue-transform-on@2.0.1': {} - '@vue/babel-plugin-jsx@1.5.0(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0)': + '@vue/babel-plugin-jsx@1.5.0(@babel/core@7.29.7)': dependencies: - '@babel/helper-module-imports': 7.29.7(supports-color@7.2.0) + '@babel/helper-module-imports': 7.29.7 '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-syntax-jsx': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0)) + '@babel/plugin-syntax-jsx': 7.29.7(@babel/core@7.29.7) '@babel/template': 7.29.7 - '@babel/traverse': 7.29.7(supports-color@7.2.0) + '@babel/traverse': 7.29.7 '@babel/types': 7.29.8 '@vue/babel-helper-vue-transform-on': 1.5.0 - '@vue/babel-plugin-resolve-type': 1.5.0(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0) + '@vue/babel-plugin-resolve-type': 1.5.0(@babel/core@7.29.7) '@vue/shared': 3.5.43 optionalDependencies: - '@babel/core': 7.29.7(supports-color@7.2.0) + '@babel/core': 7.29.7 transitivePeerDependencies: - supports-color - '@vue/babel-plugin-jsx@2.0.1(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0)': + '@vue/babel-plugin-jsx@2.0.1(@babel/core@7.29.7)': dependencies: - '@babel/helper-module-imports': 7.29.7(supports-color@7.2.0) + '@babel/helper-module-imports': 7.29.7 '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-syntax-jsx': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0)) + '@babel/plugin-syntax-jsx': 7.29.7(@babel/core@7.29.7) '@babel/template': 7.29.7 - '@babel/traverse': 7.29.7(supports-color@7.2.0) + '@babel/traverse': 7.29.7 '@babel/types': 7.29.8 '@vue/babel-helper-vue-transform-on': 2.0.1 - '@vue/babel-plugin-resolve-type': 2.0.1(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0) + '@vue/babel-plugin-resolve-type': 2.0.1(@babel/core@7.29.7) '@vue/shared': 3.5.43 optionalDependencies: - '@babel/core': 7.29.7(supports-color@7.2.0) + '@babel/core': 7.29.7 transitivePeerDependencies: - supports-color - '@vue/babel-plugin-resolve-type@1.5.0(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0)': + '@vue/babel-plugin-resolve-type@1.5.0(@babel/core@7.29.7)': dependencies: '@babel/code-frame': 7.29.7 - '@babel/core': 7.29.7(supports-color@7.2.0) - '@babel/helper-module-imports': 7.29.7(supports-color@7.2.0) + '@babel/core': 7.29.7 + '@babel/helper-module-imports': 7.29.7 '@babel/helper-plugin-utils': 7.29.7 '@babel/parser': 7.29.8 '@vue/compiler-sfc': 3.5.43 transitivePeerDependencies: - supports-color - '@vue/babel-plugin-resolve-type@2.0.1(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0)': + '@vue/babel-plugin-resolve-type@2.0.1(@babel/core@7.29.7)': dependencies: '@babel/code-frame': 7.29.7 - '@babel/core': 7.29.7(supports-color@7.2.0) - '@babel/helper-module-imports': 7.29.7(supports-color@7.2.0) + '@babel/core': 7.29.7 + '@babel/helper-module-imports': 7.29.7 '@babel/helper-plugin-utils': 7.29.7 '@babel/parser': 7.29.8 '@vue/compiler-sfc': 3.5.43 @@ -8929,23 +8856,23 @@ snapshots: '@vue/devtools-shared@8.2.1': {} - '@vue/eslint-config-prettier@10.2.0(eslint@10.13.0(jiti@2.7.0)(supports-color@7.2.0))(prettier@3.9.10)': + '@vue/eslint-config-prettier@10.2.0(eslint@10.13.0(jiti@2.7.0))(prettier@3.9.10)': dependencies: - eslint: 10.13.0(jiti@2.7.0)(supports-color@7.2.0) - eslint-config-prettier: 10.1.8(eslint@10.13.0(jiti@2.7.0)(supports-color@7.2.0)) - eslint-plugin-prettier: 5.5.6(eslint-config-prettier@10.1.8(eslint@10.13.0(jiti@2.7.0)(supports-color@7.2.0)))(eslint@10.13.0(jiti@2.7.0)(supports-color@7.2.0))(prettier@3.9.10) + eslint: 10.13.0(jiti@2.7.0) + eslint-config-prettier: 10.1.8(eslint@10.13.0(jiti@2.7.0)) + eslint-plugin-prettier: 5.5.6(eslint-config-prettier@10.1.8(eslint@10.13.0(jiti@2.7.0)))(eslint@10.13.0(jiti@2.7.0))(prettier@3.9.10) prettier: 3.9.10 transitivePeerDependencies: - '@types/eslint' - '@vue/eslint-config-typescript@14.9.0(eslint-plugin-vue@10.11.1(@typescript-eslint/parser@8.61.1(eslint@10.13.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint@10.13.0(jiti@2.7.0)(supports-color@7.2.0))(vue-eslint-parser@10.4.1(eslint@10.13.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)))(eslint@10.13.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)': + '@vue/eslint-config-typescript@14.9.0(eslint-plugin-vue@10.11.1(@typescript-eslint/parser@8.61.1(eslint@10.13.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.13.0(jiti@2.7.0))(vue-eslint-parser@10.4.1(eslint@10.13.0(jiti@2.7.0))))(eslint@10.13.0(jiti@2.7.0))(typescript@6.0.3)': dependencies: - '@typescript-eslint/utils': 8.61.1(eslint@10.13.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) - eslint: 10.13.0(jiti@2.7.0)(supports-color@7.2.0) - eslint-plugin-vue: 10.11.1(@typescript-eslint/parser@8.61.1(eslint@10.13.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint@10.13.0(jiti@2.7.0)(supports-color@7.2.0))(vue-eslint-parser@10.4.1(eslint@10.13.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)) + '@typescript-eslint/utils': 8.61.1(eslint@10.13.0(jiti@2.7.0))(typescript@6.0.3) + eslint: 10.13.0(jiti@2.7.0) + eslint-plugin-vue: 10.11.1(@typescript-eslint/parser@8.61.1(eslint@10.13.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.13.0(jiti@2.7.0))(vue-eslint-parser@10.4.1(eslint@10.13.0(jiti@2.7.0))) fast-glob: 3.3.3 - typescript-eslint: 8.61.1(eslint@10.13.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) - vue-eslint-parser: 10.4.1(eslint@10.13.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0) + typescript-eslint: 8.61.1(eslint@10.13.0(jiti@2.7.0))(typescript@6.0.3) + vue-eslint-parser: 10.4.1(eslint@10.13.0(jiti@2.7.0)) optionalDependencies: typescript: 6.0.3 transitivePeerDependencies: @@ -9009,23 +8936,23 @@ snapshots: '@vue/shared@3.5.44': {} - '@vue/test-utils@2.5.1(@vue/compiler-dom@3.5.43)(@vue/server-renderer@3.5.43)(vue@3.5.44(typescript@5.9.3))': + '@vue/test-utils@2.5.1(@vue/compiler-dom@3.5.44)(@vue/server-renderer@3.5.44)(vue@3.5.44(typescript@5.9.3))': dependencies: - '@vue/compiler-dom': 3.5.43 + '@vue/compiler-dom': 3.5.44 js-beautify: 2.0.3 vue: 3.5.44(typescript@5.9.3) vue-component-type-helpers: 3.3.5 optionalDependencies: - '@vue/server-renderer': 3.5.43 + '@vue/server-renderer': 3.5.44 - '@vue/test-utils@2.5.1(@vue/compiler-dom@3.5.43)(@vue/server-renderer@3.5.43)(vue@3.5.44(typescript@6.0.3))': + '@vue/test-utils@2.5.1(@vue/compiler-dom@3.5.44)(@vue/server-renderer@3.5.44)(vue@3.5.44(typescript@6.0.3))': dependencies: - '@vue/compiler-dom': 3.5.43 + '@vue/compiler-dom': 3.5.44 js-beautify: 2.0.3 vue: 3.5.44(typescript@6.0.3) vue-component-type-helpers: 3.3.5 optionalDependencies: - '@vue/server-renderer': 3.5.43 + '@vue/server-renderer': 3.5.44 '@vue/tsconfig@0.9.1(typescript@5.9.3)(vue@3.5.44(typescript@5.9.3))': optionalDependencies: @@ -9127,9 +9054,9 @@ snapshots: acorn@8.17.0: {} - agent-base@6.0.2(supports-color@7.2.0): + agent-base@6.0.2: dependencies: - debug: 4.4.3(supports-color@7.2.0) + debug: 4.4.3 transitivePeerDependencies: - supports-color @@ -9231,17 +9158,15 @@ snapshots: at-least-node@1.0.0: {} - atob@2.1.2: {} - available-typed-arrays@1.0.7: dependencies: possible-typed-array-names: 1.1.0 - axios@1.20.0(debug@4.4.3(supports-color@7.2.0))(supports-color@7.2.0): + axios@1.20.0: dependencies: - follow-redirects: 1.16.1(debug@4.4.3(supports-color@7.2.0)) + follow-redirects: 1.16.2 form-data: 4.0.6 - https-proxy-agent: 5.0.1(supports-color@7.2.0) + https-proxy-agent: 5.0.1 proxy-from-env: 2.1.0 transitivePeerDependencies: - debug @@ -9253,27 +9178,27 @@ snapshots: cosmiconfig: 7.1.0 resolve: 1.22.12 - babel-plugin-polyfill-corejs2@0.4.17(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0): + babel-plugin-polyfill-corejs2@0.4.17(@babel/core@7.29.7): dependencies: '@babel/compat-data': 7.29.7 - '@babel/core': 7.29.7(supports-color@7.2.0) - '@babel/helper-define-polyfill-provider': 0.6.8(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0) + '@babel/core': 7.29.7 + '@babel/helper-define-polyfill-provider': 0.6.8(@babel/core@7.29.7) semver: 6.3.1 transitivePeerDependencies: - supports-color - babel-plugin-polyfill-corejs3@0.14.2(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0): + babel-plugin-polyfill-corejs3@0.14.2(@babel/core@7.29.7): dependencies: - '@babel/core': 7.29.7(supports-color@7.2.0) - '@babel/helper-define-polyfill-provider': 0.6.8(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0) + '@babel/core': 7.29.7 + '@babel/helper-define-polyfill-provider': 0.6.8(@babel/core@7.29.7) core-js-compat: 3.49.0 transitivePeerDependencies: - supports-color - babel-plugin-polyfill-regenerator@0.6.8(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0): + babel-plugin-polyfill-regenerator@0.6.8(@babel/core@7.29.7): dependencies: - '@babel/core': 7.29.7(supports-color@7.2.0) - '@babel/helper-define-polyfill-provider': 0.6.8(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0) + '@babel/core': 7.29.7 + '@babel/helper-define-polyfill-provider': 0.6.8(@babel/core@7.29.7) transitivePeerDependencies: - supports-color @@ -9291,11 +9216,11 @@ snapshots: birpc@4.0.0: {} - body-parser@2.3.0(supports-color@7.2.0): + body-parser@2.3.0: dependencies: bytes: 3.1.2 content-type: 2.1.0 - debug: 4.4.3(supports-color@7.2.0) + debug: 4.4.3 http-errors: 2.0.1 iconv-lite: 0.7.3 on-finished: 2.4.1 @@ -9513,12 +9438,6 @@ snapshots: css-what@6.2.2: {} - css@3.0.0: - dependencies: - inherits: 2.0.4 - source-map: 0.6.1 - source-map-resolve: 0.6.0 - cssesc@3.0.0: {} csstype@3.2.3: {} @@ -9727,22 +9646,16 @@ snapshots: date-fns@4.4.0: {} - debug@3.2.7(supports-color@7.2.0): + debug@3.2.7: dependencies: ms: 2.1.3 - optionalDependencies: - supports-color: 7.2.0 - debug@4.4.3(supports-color@7.2.0): + debug@4.4.3: dependencies: ms: 2.1.3 - optionalDependencies: - supports-color: 7.2.0 decimal.js@10.6.0: {} - decode-uri-component@0.2.2: {} - dedent@1.7.2(babel-plugin-macros@3.1.0): optionalDependencies: babel-plugin-macros: 3.1.0 @@ -9983,31 +9896,31 @@ snapshots: escape-string-regexp@4.0.0: {} - eslint-config-prettier@10.1.8(eslint@10.13.0(jiti@2.7.0)(supports-color@7.2.0)): + eslint-config-prettier@10.1.8(eslint@10.13.0(jiti@2.7.0)): dependencies: - eslint: 10.13.0(jiti@2.7.0)(supports-color@7.2.0) + eslint: 10.13.0(jiti@2.7.0) - eslint-plugin-prettier@5.5.6(eslint-config-prettier@10.1.8(eslint@10.13.0(jiti@2.7.0)(supports-color@7.2.0)))(eslint@10.13.0(jiti@2.7.0)(supports-color@7.2.0))(prettier@3.9.10): + eslint-plugin-prettier@5.5.6(eslint-config-prettier@10.1.8(eslint@10.13.0(jiti@2.7.0)))(eslint@10.13.0(jiti@2.7.0))(prettier@3.9.10): dependencies: - eslint: 10.13.0(jiti@2.7.0)(supports-color@7.2.0) + eslint: 10.13.0(jiti@2.7.0) prettier: 3.9.10 prettier-linter-helpers: 1.0.1 synckit: 0.11.13 optionalDependencies: - eslint-config-prettier: 10.1.8(eslint@10.13.0(jiti@2.7.0)(supports-color@7.2.0)) + eslint-config-prettier: 10.1.8(eslint@10.13.0(jiti@2.7.0)) - eslint-plugin-vue@10.11.1(@typescript-eslint/parser@8.61.1(eslint@10.13.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint@10.13.0(jiti@2.7.0)(supports-color@7.2.0))(vue-eslint-parser@10.4.1(eslint@10.13.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)): + eslint-plugin-vue@10.11.1(@typescript-eslint/parser@8.61.1(eslint@10.13.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.13.0(jiti@2.7.0))(vue-eslint-parser@10.4.1(eslint@10.13.0(jiti@2.7.0))): dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.13.0(jiti@2.7.0)(supports-color@7.2.0)) - eslint: 10.13.0(jiti@2.7.0)(supports-color@7.2.0) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.13.0(jiti@2.7.0)) + eslint: 10.13.0(jiti@2.7.0) natural-compare: 1.4.0 nth-check: 2.1.1 postcss-selector-parser: 7.1.4 semver: 7.8.5 - vue-eslint-parser: 10.4.1(eslint@10.13.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0) + vue-eslint-parser: 10.4.1(eslint@10.13.0(jiti@2.7.0)) xml-name-validator: 5.0.0 optionalDependencies: - '@typescript-eslint/parser': 8.61.1(eslint@10.13.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) + '@typescript-eslint/parser': 8.61.1(eslint@10.13.0(jiti@2.7.0))(typescript@6.0.3) eslint-scope@9.1.2: dependencies: @@ -10020,11 +9933,11 @@ snapshots: eslint-visitor-keys@5.0.1: {} - eslint@10.13.0(jiti@2.7.0)(supports-color@7.2.0): + eslint@10.13.0(jiti@2.7.0): dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.13.0(jiti@2.7.0)(supports-color@7.2.0)) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.13.0(jiti@2.7.0)) '@eslint-community/regexpp': 4.12.2 - '@eslint/config-array': 0.23.5(supports-color@7.2.0) + '@eslint/config-array': 0.23.5 '@eslint/config-helpers': 0.7.0 '@eslint/core': 1.2.1 '@eslint/plugin-kit': 0.7.3 @@ -10034,7 +9947,7 @@ snapshots: '@types/estree': 1.0.9 ajv: 6.15.0 cross-spawn: 7.0.6 - debug: 4.4.3(supports-color@7.2.0) + debug: 4.4.3 escape-string-regexp: 4.0.0 eslint-scope: 9.1.2 eslint-visitor-keys: 5.0.1 @@ -10095,28 +10008,28 @@ snapshots: expect-type@1.4.0: {} - express-rate-limit@8.7.1(express@5.3.0(supports-color@7.2.0))(supports-color@7.2.0): + express-rate-limit@8.7.1(express@5.3.0): dependencies: - debug: 4.4.3(supports-color@7.2.0) - express: 5.3.0(supports-color@7.2.0) + debug: 4.4.3 + express: 5.3.0 ip-address: 10.7.4 transitivePeerDependencies: - supports-color - express@5.3.0(supports-color@7.2.0): + express@5.3.0: dependencies: accepts: 2.0.0 - body-parser: 2.3.0(supports-color@7.2.0) + body-parser: 2.3.0 content-disposition: 2.0.1 content-type: 2.1.0 cookie: 0.7.2 cookie-signature: 1.2.2 - debug: 4.4.3(supports-color@7.2.0) + debug: 4.4.3 depd: 2.0.0 encodeurl: 2.0.0 escape-html: 1.0.3 etag: 1.8.1 - finalhandler: 2.1.1(supports-color@7.2.0) + finalhandler: 2.1.1 fresh: 2.0.0 http-errors: 2.0.1 merge-descriptors: 2.0.0 @@ -10127,9 +10040,9 @@ snapshots: proxy-addr: 2.0.8 qs: 6.16.0 range-parser: 1.3.0 - router: 2.2.0(supports-color@7.2.0) - send: 1.2.1(supports-color@7.2.0) - serve-static: 2.2.1(supports-color@7.2.0) + router: 2.2.0 + send: 1.2.1 + serve-static: 2.2.1 statuses: 2.0.2 type-is: 2.1.0 vary: 1.1.2 @@ -10186,9 +10099,9 @@ snapshots: dependencies: to-regex-range: 5.0.1 - finalhandler@2.1.1(supports-color@7.2.0): + finalhandler@2.1.1: dependencies: - debug: 4.4.3(supports-color@7.2.0) + debug: 4.4.3 encodeurl: 2.0.0 escape-html: 1.0.3 on-finished: 2.4.1 @@ -10212,9 +10125,7 @@ snapshots: flatted@3.4.4: {} - follow-redirects@1.16.1(debug@4.4.3(supports-color@7.2.0)): - optionalDependencies: - debug: 4.4.3(supports-color@7.2.0) + follow-redirects@1.16.2: {} for-each@0.3.5: dependencies: @@ -10384,9 +10295,6 @@ snapshots: has-bigints@1.1.0: {} - has-flag@4.0.0: - optional: true - has-property-descriptors@1.0.2: dependencies: es-define-property: 1.0.1 @@ -10437,10 +10345,10 @@ snapshots: http-parser-js@0.5.10: {} - https-proxy-agent@5.0.1(supports-color@7.2.0): + https-proxy-agent@5.0.1: dependencies: - agent-base: 6.0.2(supports-color@7.2.0) - debug: 4.4.3(supports-color@7.2.0) + agent-base: 6.0.2 + debug: 4.4.3 transitivePeerDependencies: - supports-color @@ -11339,13 +11247,13 @@ snapshots: cssesc: 3.0.0 util-deprecate: 1.0.2 - postcss-styl@0.12.3(supports-color@7.2.0): + postcss-styl@0.12.3: dependencies: - debug: 4.4.3(supports-color@7.2.0) + debug: 4.4.3 fast-diff: 1.3.0 lodash.sortedlastindex: 4.1.0 postcss: 8.5.26 - stylus: 0.57.0(supports-color@7.2.0) + stylus: 0.59.0 transitivePeerDependencies: - supports-color @@ -11598,9 +11506,9 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.62.0 fsevents: 2.3.3 - router@2.2.0(supports-color@7.2.0): + router@2.2.0: dependencies: - debug: 4.4.3(supports-color@7.2.0) + debug: 4.4.3 depd: 2.0.0 is-promise: 4.0.0 parseurl: 1.3.3 @@ -11653,9 +11561,9 @@ snapshots: semver@7.8.5: {} - send@1.2.1(supports-color@7.2.0): + send@1.2.1: dependencies: - debug: 4.4.3(supports-color@7.2.0) + debug: 4.4.3 encodeurl: 2.0.0 escape-html: 1.0.3 etag: 1.8.1 @@ -11671,12 +11579,12 @@ snapshots: serialize-javascript@7.0.6: {} - serve-static@2.2.1(supports-color@7.2.0): + serve-static@2.2.1: dependencies: encodeurl: 2.0.0 escape-html: 1.0.3 parseurl: 1.3.3 - send: 1.2.1(supports-color@7.2.0) + send: 1.2.1 transitivePeerDependencies: - supports-color @@ -11704,11 +11612,11 @@ snapshots: setprototypeof@1.2.0: {} - shadcn-vue@2.8.2(babel-plugin-macros@3.1.0)(eslint@10.13.0(jiti@2.7.0)(supports-color@7.2.0))(magicast@0.5.5)(supports-color@7.2.0)(vue@3.5.44(typescript@5.9.3)): + shadcn-vue@2.8.2(babel-plugin-macros@3.1.0)(eslint@10.13.0(jiti@2.7.0))(magicast@0.5.5)(vue@3.5.44(typescript@5.9.3)): dependencies: '@dotenvx/dotenvx': 2.34.2 - '@modelcontextprotocol/sdk': 1.32.1(supports-color@7.2.0)(zod@3.25.76) - '@unovue/detypes': 0.8.5(supports-color@7.2.0) + '@modelcontextprotocol/sdk': 1.32.1(zod@3.25.76) + '@unovue/detypes': 0.8.5 '@vue/compiler-sfc': 3.5.43 c12: 3.3.4(magicast@0.5.5) commander: 15.0.0 @@ -11738,7 +11646,7 @@ snapshots: ts-morph: 28.0.0 undici: 8.11.2 validate-npm-package-name: 8.0.0 - vue-metamorph: 3.3.4(eslint@10.13.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0) + vue-metamorph: 3.3.4(eslint@10.13.0(jiti@2.7.0)) zod: 3.25.76 zod-to-json-schema: 3.25.2(zod@3.25.76) transitivePeerDependencies: @@ -11805,9 +11713,9 @@ snapshots: smol-toml@1.9.1: {} - sockjs-client@1.6.1(supports-color@7.2.0): + sockjs-client@1.6.1: dependencies: - debug: 3.2.7(supports-color@7.2.0) + debug: 3.2.7 eventsource: 2.0.2 faye-websocket: 0.11.4 inherits: 2.0.4 @@ -11817,11 +11725,6 @@ snapshots: source-map-js@1.2.2: {} - source-map-resolve@0.6.0: - dependencies: - atob: 2.1.2 - decode-uri-component: 0.2.2 - source-map-support@0.5.21: dependencies: buffer-from: 1.1.2 @@ -11933,12 +11836,11 @@ snapshots: stylis@4.2.0: {} - stylus@0.57.0(supports-color@7.2.0): + stylus@0.59.0: dependencies: - css: 3.0.0 - debug: 4.4.3(supports-color@7.2.0) + '@adobe/css-tools': 4.5.0 + debug: 4.4.3 glob: 7.2.3 - safer-buffer: 2.1.2 sax: 1.2.4 source-map: 0.7.6 transitivePeerDependencies: @@ -11958,11 +11860,6 @@ snapshots: dependencies: copy-anything: 4.0.5 - supports-color@7.2.0: - dependencies: - has-flag: 4.0.0 - optional: true - supports-preserve-symlinks-flag@1.0.0: {} synckit@0.11.13: @@ -12116,13 +12013,13 @@ snapshots: possible-typed-array-names: 1.1.0 reflect.getprototypeof: 1.0.10 - typescript-eslint@8.61.1(eslint@10.13.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3): + typescript-eslint@8.61.1(eslint@10.13.0(jiti@2.7.0))(typescript@6.0.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.61.1(@typescript-eslint/parser@8.61.1(eslint@10.13.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint@10.13.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) - '@typescript-eslint/parser': 8.61.1(eslint@10.13.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) - '@typescript-eslint/typescript-estree': 8.61.1(supports-color@7.2.0)(typescript@6.0.3) - '@typescript-eslint/utils': 8.61.1(eslint@10.13.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) - eslint: 10.13.0(jiti@2.7.0)(supports-color@7.2.0) + '@typescript-eslint/eslint-plugin': 8.61.1(@typescript-eslint/parser@8.61.1(eslint@10.13.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.13.0(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/parser': 8.61.1(eslint@10.13.0(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/typescript-estree': 8.61.1(typescript@6.0.3) + '@typescript-eslint/utils': 8.61.1(eslint@10.13.0(jiti@2.7.0))(typescript@6.0.3) + eslint: 10.13.0(jiti@2.7.0) typescript: 6.0.3 transitivePeerDependencies: - supports-color @@ -12171,15 +12068,15 @@ snapshots: unpipe@1.0.0: {} - unplugin-icons@24.0.0(@vue/compiler-sfc@3.5.43)(rolldown@1.2.14)(rollup@4.62.0)(vite@8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.57.0(supports-color@7.2.0))(terser@5.48.0)(yaml@2.9.1)): + unplugin-icons@24.0.0(@vue/compiler-sfc@3.5.44)(rolldown@1.2.14)(rollup@4.62.0)(vite@8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.59.0)(terser@5.48.0)(yaml@2.9.1)): dependencies: '@antfu/install-pkg': 1.1.0 '@iconify/utils': 3.1.7 local-pkg: 1.2.1 obug: 2.2.1 - unplugin: 3.3.0(rolldown@1.2.14)(rollup@4.62.0)(vite@8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.57.0(supports-color@7.2.0))(terser@5.48.0)(yaml@2.9.1)) + unplugin: 3.3.0(rolldown@1.2.14)(rollup@4.62.0)(vite@8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.59.0)(terser@5.48.0)(yaml@2.9.1)) optionalDependencies: - '@vue/compiler-sfc': 3.5.43 + '@vue/compiler-sfc': 3.5.44 transitivePeerDependencies: - '@farmfe/core' - '@rspack/core' @@ -12196,7 +12093,7 @@ snapshots: pathe: 2.0.3 picomatch: 4.0.7 - unplugin@3.3.0(rolldown@1.2.14)(rollup@4.62.0)(vite@8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.57.0(supports-color@7.2.0))(terser@5.48.0)(yaml@2.9.1)): + unplugin@3.3.0(rolldown@1.2.14)(rollup@4.62.0)(vite@8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.59.0)(terser@5.48.0)(yaml@2.9.1)): dependencies: '@jridgewell/remapping': 2.3.5 picomatch: 4.0.7 @@ -12204,9 +12101,9 @@ snapshots: optionalDependencies: rolldown: 1.2.14 rollup: 4.62.0 - vite: 8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.57.0(supports-color@7.2.0))(terser@5.48.0)(yaml@2.9.1) + vite: 8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.59.0)(terser@5.48.0)(yaml@2.9.1) - unplugin@3.4.0(rolldown@1.2.14)(rollup@4.62.0)(vite@8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.57.0(supports-color@7.2.0))(terser@5.48.0)(yaml@2.9.1)): + unplugin@3.4.0(rolldown@1.2.14)(rollup@4.62.0)(vite@8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.59.0)(terser@5.48.0)(yaml@2.9.1)): dependencies: '@jridgewell/remapping': 2.3.5 picomatch: 4.0.7 @@ -12214,7 +12111,7 @@ snapshots: optionalDependencies: rolldown: 1.2.14 rollup: 4.62.0 - vite: 8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.57.0(supports-color@7.2.0))(terser@5.48.0)(yaml@2.9.1) + vite: 8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.59.0)(terser@5.48.0)(yaml@2.9.1) upath@1.2.0: {} @@ -12259,17 +12156,17 @@ snapshots: type-fest: 4.41.0 vue: 3.5.44(typescript@6.0.3) - vite-dev-rpc@2.0.0(vite@8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.57.0(supports-color@7.2.0))(terser@5.48.0)(yaml@2.9.1)): + vite-dev-rpc@2.0.0(vite@8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.59.0)(terser@5.48.0)(yaml@2.9.1)): dependencies: birpc: 4.0.0 - vite: 8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.57.0(supports-color@7.2.0))(terser@5.48.0)(yaml@2.9.1) - vite-hot-client: 2.2.0(vite@8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.57.0(supports-color@7.2.0))(terser@5.48.0)(yaml@2.9.1)) + vite: 8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.59.0)(terser@5.48.0)(yaml@2.9.1) + vite-hot-client: 2.2.0(vite@8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.59.0)(terser@5.48.0)(yaml@2.9.1)) - vite-hot-client@2.2.0(vite@8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.57.0(supports-color@7.2.0))(terser@5.48.0)(yaml@2.9.1)): + vite-hot-client@2.2.0(vite@8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.59.0)(terser@5.48.0)(yaml@2.9.1)): dependencies: - vite: 8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.57.0(supports-color@7.2.0))(terser@5.48.0)(yaml@2.9.1) + vite: 8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.59.0)(terser@5.48.0)(yaml@2.9.1) - vite-plugin-inspect@11.4.1(vite@8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.57.0(supports-color@7.2.0))(terser@5.48.0)(yaml@2.9.1)): + vite-plugin-inspect@11.4.1(vite@8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.59.0)(terser@5.48.0)(yaml@2.9.1)): dependencies: ansis: 4.3.1 error-stack-parser-es: 1.0.5 @@ -12279,50 +12176,50 @@ snapshots: perfect-debounce: 2.1.0 sirv: 3.0.2 unplugin-utils: 0.3.2 - vite: 8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.57.0(supports-color@7.2.0))(terser@5.48.0)(yaml@2.9.1) - vite-dev-rpc: 2.0.0(vite@8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.57.0(supports-color@7.2.0))(terser@5.48.0)(yaml@2.9.1)) + vite: 8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.59.0)(terser@5.48.0)(yaml@2.9.1) + vite-dev-rpc: 2.0.0(vite@8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.59.0)(terser@5.48.0)(yaml@2.9.1)) - vite-plugin-pwa@2.0.0(supports-color@7.2.0)(vite@8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.57.0(supports-color@7.2.0))(terser@5.48.0)(yaml@2.9.1))(workbox-build@7.4.1(supports-color@7.2.0))(workbox-window@7.4.1): + vite-plugin-pwa@2.0.0(vite@8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.59.0)(terser@5.48.0)(yaml@2.9.1))(workbox-build@7.4.1)(workbox-window@7.4.1): dependencies: - debug: 4.4.3(supports-color@7.2.0) + debug: 4.4.3 pretty-bytes: 6.1.1 tinyglobby: 0.2.17 - vite: 8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.57.0(supports-color@7.2.0))(terser@5.48.0)(yaml@2.9.1) - workbox-build: 7.4.1(supports-color@7.2.0) + vite: 8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.59.0)(terser@5.48.0)(yaml@2.9.1) + workbox-build: 7.4.1 workbox-window: 7.4.1 transitivePeerDependencies: - supports-color - vite-plugin-vue-devtools@8.2.1(supports-color@7.2.0)(vite@8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.57.0(supports-color@7.2.0))(terser@5.48.0)(yaml@2.9.1))(vue@3.5.44(typescript@6.0.3)): + vite-plugin-vue-devtools@8.2.1(vite@8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.59.0)(terser@5.48.0)(yaml@2.9.1))(vue@3.5.44(typescript@6.0.3)): dependencies: '@vue/devtools-core': 8.2.1(vue@3.5.44(typescript@6.0.3)) '@vue/devtools-kit': 8.2.1 '@vue/devtools-shared': 8.2.1 sirv: 3.0.2 - vite: 8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.57.0(supports-color@7.2.0))(terser@5.48.0)(yaml@2.9.1) - vite-plugin-inspect: 11.4.1(vite@8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.57.0(supports-color@7.2.0))(terser@5.48.0)(yaml@2.9.1)) - vite-plugin-vue-inspector: 6.0.0(supports-color@7.2.0)(vite@8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.57.0(supports-color@7.2.0))(terser@5.48.0)(yaml@2.9.1)) - vue: 3.5.44(typescript@6.0.3) + vite: 8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.59.0)(terser@5.48.0)(yaml@2.9.1) + vite-plugin-inspect: 11.4.1(vite@8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.59.0)(terser@5.48.0)(yaml@2.9.1)) + vite-plugin-vue-inspector: 6.0.0(vite@8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.59.0)(terser@5.48.0)(yaml@2.9.1)) transitivePeerDependencies: - '@nuxt/kit' - supports-color + - vue - vite-plugin-vue-inspector@6.0.0(supports-color@7.2.0)(vite@8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.57.0(supports-color@7.2.0))(terser@5.48.0)(yaml@2.9.1)): + vite-plugin-vue-inspector@6.0.0(vite@8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.59.0)(terser@5.48.0)(yaml@2.9.1)): dependencies: - '@babel/core': 7.29.7(supports-color@7.2.0) - '@babel/plugin-proposal-decorators': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0) - '@babel/plugin-syntax-import-attributes': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0)) - '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.29.7(supports-color@7.2.0)) - '@babel/plugin-transform-typescript': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0) - '@vue/babel-plugin-jsx': 1.5.0(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0) + '@babel/core': 7.29.7 + '@babel/plugin-proposal-decorators': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-syntax-import-attributes': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.29.7) + '@babel/plugin-transform-typescript': 7.29.7(@babel/core@7.29.7) + '@vue/babel-plugin-jsx': 1.5.0(@babel/core@7.29.7) '@vue/compiler-dom': 3.5.43 kolorist: 1.8.0 magic-string: 0.30.21 - vite: 8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.57.0(supports-color@7.2.0))(terser@5.48.0)(yaml@2.9.1) + vite: 8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.59.0)(terser@5.48.0)(yaml@2.9.1) transitivePeerDependencies: - supports-color - vite@8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.57.0(supports-color@7.2.0))(terser@5.48.0)(yaml@2.9.1): + vite@8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.59.0)(terser@5.48.0)(yaml@2.9.1): dependencies: lightningcss: 1.33.0 picomatch: 4.0.7 @@ -12333,14 +12230,14 @@ snapshots: '@types/node': 26.6.5 fsevents: 2.3.3 jiti: 2.7.0 - stylus: 0.57.0(supports-color@7.2.0) + stylus: 0.59.0 terser: 5.48.0 yaml: 2.9.1 - vitest@5.0.5(@types/node@26.6.5)(@vitest/coverage-v8@5.0.5)(@vitest/ui@5.0.5)(happy-dom@21.0.3)(jsdom@30.1.3)(vite@8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.57.0(supports-color@7.2.0))(terser@5.48.0)(yaml@2.9.1)): + vitest@5.0.5(@types/node@26.6.5)(@vitest/coverage-v8@5.0.5)(@vitest/ui@5.0.5)(happy-dom@21.0.3)(jsdom@30.1.3)(vite@8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.59.0)(terser@5.48.0)(yaml@2.9.1)): dependencies: '@types/chai': 5.2.3 - '@vitest/mocker': 5.0.5(vite@8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.57.0(supports-color@7.2.0))(terser@5.48.0)(yaml@2.9.1)) + '@vitest/mocker': 5.0.5(vite@8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.59.0)(terser@5.48.0)(yaml@2.9.1)) chai: 6.3.0 es-module-lexer: 2.3.2 expect-type: 1.4.0 @@ -12351,7 +12248,7 @@ snapshots: tinybench: 6.2.2 tinyexec: 1.3.1 tinyglobby: 0.2.17 - vite: 8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.57.0(supports-color@7.2.0))(terser@5.48.0)(yaml@2.9.1) + vite: 8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.59.0)(terser@5.48.0)(yaml@2.9.1) why-is-node-running: 3.2.1 optionalDependencies: '@types/node': 26.6.5 @@ -12375,10 +12272,10 @@ snapshots: echarts: 6.1.0 vue: 3.5.44(typescript@6.0.3) - vue-eslint-parser@10.4.1(eslint@10.13.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0): + vue-eslint-parser@10.4.1(eslint@10.13.0(jiti@2.7.0)): dependencies: - debug: 4.4.3(supports-color@7.2.0) - eslint: 10.13.0(jiti@2.7.0)(supports-color@7.2.0) + debug: 4.4.3 + eslint: 10.13.0(jiti@2.7.0) eslint-scope: 9.1.2 eslint-visitor-keys: 5.0.1 espree: 11.2.0 @@ -12387,11 +12284,11 @@ snapshots: transitivePeerDependencies: - supports-color - vue-i18n@11.4.13(vue@3.5.44(typescript@5.9.3)): + vue-i18n@11.4.16(vue@3.5.44(typescript@5.9.3)): dependencies: - '@intlify/core-base': 11.4.13 - '@intlify/devtools-types': 11.4.13 - '@intlify/shared': 11.4.13 + '@intlify/core-base': 11.4.16 + '@intlify/devtools-types': 11.4.16 + '@intlify/shared': 11.4.16 '@vue/devtools-api': 6.6.4 vue: 3.5.44(typescript@5.9.3) @@ -12411,7 +12308,7 @@ snapshots: transitivePeerDependencies: - typescript - vue-metamorph@3.3.4(eslint@10.13.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0): + vue-metamorph@3.3.4(eslint@10.13.0(jiti@2.7.0)): dependencies: '@babel/parser': 8.0.0-alpha.12 ast-types-x: 1.18.0 @@ -12429,15 +12326,15 @@ snapshots: postcss-less: 6.0.0(postcss@8.5.26) postcss-sass: 0.5.0 postcss-scss: 4.0.9(postcss@8.5.26) - postcss-styl: 0.12.3(supports-color@7.2.0) + postcss-styl: 0.12.3 recast-x: 1.0.5 table: 6.9.0 - vue-eslint-parser: 10.4.1(eslint@10.13.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0) + vue-eslint-parser: 10.4.1(eslint@10.13.0(jiti@2.7.0)) transitivePeerDependencies: - eslint - supports-color - vue-router@5.3.1(@vue/compiler-sfc@3.5.43)(pinia@4.1.0(@vue/devtools-api@8.2.1)(typescript@5.9.3)(vue@3.5.44(typescript@5.9.3)))(rolldown@1.2.14)(rollup@4.62.0)(vite@8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.57.0(supports-color@7.2.0))(terser@5.48.0)(yaml@2.9.1))(vue@3.5.44(typescript@5.9.3)): + vue-router@5.3.1(@vue/compiler-sfc@3.5.44)(pinia@4.1.0(@vue/devtools-api@8.2.1)(typescript@5.9.3)(vue@3.5.44(typescript@5.9.3)))(rolldown@1.2.14)(rollup@4.62.0)(vite@8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.59.0)(terser@5.48.0)(yaml@2.9.1))(vue@3.5.44(typescript@5.9.3)): dependencies: '@vue-macros/common': 3.1.4(vue@3.5.44(typescript@5.9.3)) '@vue/devtools-api': 8.2.1 @@ -12453,13 +12350,13 @@ snapshots: picomatch: 4.0.7 scule: 1.3.0 tinyglobby: 0.2.17 - unplugin: 3.3.0(rolldown@1.2.14)(rollup@4.62.0)(vite@8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.57.0(supports-color@7.2.0))(terser@5.48.0)(yaml@2.9.1)) + unplugin: 3.3.0(rolldown@1.2.14)(rollup@4.62.0)(vite@8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.59.0)(terser@5.48.0)(yaml@2.9.1)) unplugin-utils: 0.3.2 vue: 3.5.44(typescript@5.9.3) optionalDependencies: - '@vue/compiler-sfc': 3.5.43 + '@vue/compiler-sfc': 3.5.44 pinia: 4.1.0(@vue/devtools-api@8.2.1)(typescript@5.9.3)(vue@3.5.44(typescript@5.9.3)) - vite: 8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.57.0(supports-color@7.2.0))(terser@5.48.0)(yaml@2.9.1) + vite: 8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.59.0)(terser@5.48.0)(yaml@2.9.1) transitivePeerDependencies: - '@farmfe/core' - '@rspack/core' @@ -12470,7 +12367,7 @@ snapshots: - unloader - webpack - vue-router@5.4.0(@vue/compiler-sfc@3.5.44)(pinia@4.1.0(@vue/devtools-api@8.2.1)(typescript@5.9.3)(vue@3.5.44(typescript@5.9.3)))(rolldown@1.2.14)(rollup@4.62.0)(vite@8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.57.0(supports-color@7.2.0))(terser@5.48.0)(yaml@2.9.1))(vue@3.5.44(typescript@5.9.3)): + vue-router@5.4.0(@vue/compiler-sfc@3.5.44)(pinia@4.1.0(@vue/devtools-api@8.2.1)(typescript@5.9.3)(vue@3.5.44(typescript@5.9.3)))(rolldown@1.2.14)(rollup@4.62.0)(vite@8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.59.0)(terser@5.48.0)(yaml@2.9.1))(vue@3.5.44(typescript@5.9.3)): dependencies: '@vue-macros/common': 3.1.4(vue@3.5.44(typescript@5.9.3)) '@vue/devtools-api': 8.2.1 @@ -12486,13 +12383,13 @@ snapshots: picomatch: 4.0.7 scule: 1.3.0 tinyglobby: 0.2.17 - unplugin: 3.4.0(rolldown@1.2.14)(rollup@4.62.0)(vite@8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.57.0(supports-color@7.2.0))(terser@5.48.0)(yaml@2.9.1)) + unplugin: 3.4.0(rolldown@1.2.14)(rollup@4.62.0)(vite@8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.59.0)(terser@5.48.0)(yaml@2.9.1)) unplugin-utils: 0.3.2 vue: 3.5.44(typescript@5.9.3) optionalDependencies: '@vue/compiler-sfc': 3.5.44 pinia: 4.1.0(@vue/devtools-api@8.2.1)(typescript@5.9.3)(vue@3.5.44(typescript@5.9.3)) - vite: 8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.57.0(supports-color@7.2.0))(terser@5.48.0)(yaml@2.9.1) + vite: 8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.59.0)(terser@5.48.0)(yaml@2.9.1) transitivePeerDependencies: - '@farmfe/core' - '@rsbuild/core' @@ -12504,7 +12401,7 @@ snapshots: - unloader - webpack - vue-router@5.4.0(@vue/compiler-sfc@3.5.44)(pinia@4.1.0(@vue/devtools-api@8.2.1)(typescript@6.0.3)(vue@3.5.44(typescript@6.0.3)))(rolldown@1.2.14)(rollup@4.62.0)(vite@8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.57.0(supports-color@7.2.0))(terser@5.48.0)(yaml@2.9.1))(vue@3.5.44(typescript@6.0.3)): + vue-router@5.4.0(@vue/compiler-sfc@3.5.44)(pinia@4.1.0(@vue/devtools-api@8.2.1)(typescript@6.0.3)(vue@3.5.44(typescript@6.0.3)))(rolldown@1.2.14)(rollup@4.62.0)(vite@8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.59.0)(terser@5.48.0)(yaml@2.9.1))(vue@3.5.44(typescript@6.0.3)): dependencies: '@vue-macros/common': 3.1.4(vue@3.5.44(typescript@6.0.3)) '@vue/devtools-api': 8.2.1 @@ -12520,13 +12417,13 @@ snapshots: picomatch: 4.0.7 scule: 1.3.0 tinyglobby: 0.2.17 - unplugin: 3.4.0(rolldown@1.2.14)(rollup@4.62.0)(vite@8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.57.0(supports-color@7.2.0))(terser@5.48.0)(yaml@2.9.1)) + unplugin: 3.4.0(rolldown@1.2.14)(rollup@4.62.0)(vite@8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.59.0)(terser@5.48.0)(yaml@2.9.1)) unplugin-utils: 0.3.2 vue: 3.5.44(typescript@6.0.3) optionalDependencies: '@vue/compiler-sfc': 3.5.44 pinia: 4.1.0(@vue/devtools-api@8.2.1)(typescript@6.0.3)(vue@3.5.44(typescript@6.0.3)) - vite: 8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.57.0(supports-color@7.2.0))(terser@5.48.0)(yaml@2.9.1) + vite: 8.3.4(@types/node@26.6.5)(jiti@2.7.0)(stylus@0.59.0)(terser@5.48.0)(yaml@2.9.1) transitivePeerDependencies: - '@farmfe/core' - '@rsbuild/core' @@ -12542,13 +12439,13 @@ snapshots: vue-tsc@3.3.13(typescript@5.9.3): dependencies: - '@volar/typescript': 2.4.28(typescript@5.9.3) + '@volar/typescript': 2.4.28 '@vue/language-core': 3.3.13 typescript: 5.9.3 vue-tsc@3.3.13(typescript@6.0.3): dependencies: - '@volar/typescript': 2.4.28(typescript@6.0.3) + '@volar/typescript': 2.4.28 '@vue/language-core': 3.3.13 typescript: 6.0.3 @@ -12684,13 +12581,13 @@ snapshots: dependencies: workbox-core: 7.4.1 - workbox-build@7.4.1(supports-color@7.2.0): + workbox-build@7.4.1: dependencies: '@apideck/better-ajv-errors': 0.3.7(ajv@8.20.0) - '@babel/core': 7.29.7(supports-color@7.2.0) - '@babel/preset-env': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0) + '@babel/core': 7.29.7 + '@babel/preset-env': 7.29.7(@babel/core@7.29.7) '@babel/runtime': 7.29.7 - '@rollup/plugin-babel': 6.1.0(@babel/core@7.29.7(supports-color@7.2.0))(rollup@4.62.0)(supports-color@7.2.0) + '@rollup/plugin-babel': 6.1.0(@babel/core@7.29.7)(rollup@4.62.0) '@rollup/plugin-node-resolve': 16.0.3(rollup@4.62.0) '@rollup/plugin-replace': 6.0.3(rollup@4.62.0) '@rollup/plugin-terser': 1.0.0(rollup@4.62.0) diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 3bfa8bab3b..cd1c2e788b 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -8,8 +8,12 @@ packages: - "apps/*" - "packages/*" overrides: + # Keep shared auto-installed peers on the patched Vue I18n line. + vue-i18n: 11.4.16 + # Remove css/source-map-resolve and its vulnerable URI decoder. + stylus: 0.59.0 dompurify: 3.4.17 - follow-redirects: 1.16.1 + follow-redirects: 1.16.2 markdown-it: 15.0.3 # Patch transitive CLI/parser dependencies without crossing consumer majors. brace-expansion@5: 5.0.12 diff --git a/scripts/ci/acceptance_readiness.py b/scripts/ci/acceptance_readiness.py new file mode 100644 index 0000000000..8dd00efebb --- /dev/null +++ b/scripts/ci/acceptance_readiness.py @@ -0,0 +1,182 @@ +#!/usr/bin/env python3 +"""Fixed-schema launch evidence, never raw PM2 records or log messages.""" +import json +import os +import re +import stat +import sys +import time +from pathlib import Path + +SERVICES = tuple('ulticode-' + name for name in ( + 'auth', 'admin', 'app', 'core', 'notification', 'submission', 'judge', + 'search', '9002', '9003', '9004')) +STATUSES = ('online', 'stopped', 'stopping', 'launching', 'errored', 'one-launch-status', 'waiting restart') +COLLECTION = ('ok', 'missing', 'failed', 'invalid') +LIMIT = 65536 +FILE = 'launch-readiness.json' + + +def number(value, maximum=2147483647): + return value if type(value) is int and 0 <= value <= maximum else None + + +def empty(collection='missing'): + return {'schema_version': 1, 'collection': collection, 'services': {}} + + +def sanitize(value): + if not isinstance(value, dict) or type(value.get('schema_version')) is not int or value.get('schema_version') != 1: + return empty('invalid') + result = empty(value.get('collection') if value.get('collection') in COLLECTION else 'invalid') + items = value.get('services') + if not isinstance(items, dict): + return empty('invalid') + for name in SERVICES: + item = items.get(name) + if not isinstance(item, dict): + continue + result['services'][name] = { + 'last_readiness': item.get('last_readiness') if item.get('last_readiness') in ('ready', 'not_ready', 'not_checked') else 'unknown', + 'pm2_collection': item.get('pm2_collection') if item.get('pm2_collection') in COLLECTION else 'invalid', + 'status': item.get('status') if item.get('status') in STATUSES else 'unknown', + 'restart_count': number(item.get('restart_count')), + 'exit_code': number(item.get('exit_code'), 255), + 'uptime_seconds': number(item.get('uptime_seconds')), + } + return result + + +def valid_project(project): + return isinstance(project, str) and re.fullmatch(r'ulticode-acceptance-[0-9]{1,20}-[0-9]{1,20}', project) + + +def directory_fd(root): + """Walk relative directories without following symlinks (including races).""" + fd = os.open(root, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW) + try: + for part in ('.local', 'acceptance'): + child = os.open(part, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW, dir_fd=fd) + os.close(fd) + fd = child + return fd + except OSError: + os.close(fd) + raise + + +def read_snapshot(root, project): + try: + directory = directory_fd(root) + try: + fd = os.open(FILE, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK, dir_fd=directory) + finally: + os.close(directory) + with os.fdopen(fd, 'rb') as stream: + info = os.fstat(stream.fileno()) + if not stat.S_ISREG(info.st_mode) or info.st_size > LIMIT: + return empty('invalid') + raw = stream.read(LIMIT + 1) + if len(raw) > LIMIT: + return empty('invalid') + value = json.loads(raw) + if not isinstance(value, dict) or value.get('run_project') != project or not valid_project(project): + return empty('invalid') + return sanitize(value) + except FileNotFoundError: + return empty('missing') + except (OSError, ValueError, TypeError, RecursionError): + return empty('invalid') + + +def pm2_snapshot(stream, collection): + # Reuse the existing readiness query; never run another PM2 command (which + # could autostart a daemon). Bound input before parsing and discard raw data. + if collection != 'ok': + return 'failed', [] + try: + raw = stream.read(1048577) + if len(raw) > 1048576: + return 'invalid', [] + records = json.loads(raw) + return ('ok', records) if isinstance(records, list) else ('invalid', []) + except (OSError, ValueError, RecursionError): + return 'invalid', [] + + +def capture(selected, observations, collection, records, now_ms): + result = empty('ok') + last = {} + for observation in observations: + name, _, outcome = observation.partition('=') + if name in SERVICES and outcome in ('ready', 'not_ready'): + last[name] = outcome + for name in SERVICES: + if name not in selected.split(','): + continue + matches = [item for item in records if isinstance(item, dict) and item.get('name') == name] + env = matches[0].get('pm2_env') if len(matches) == 1 else None + valid = isinstance(env, dict) + env = env if valid else {} + started = number(env.get('pm_uptime'), 9007199254740991) + result['services'][name] = { + 'last_readiness': last.get(name, 'not_checked'), + 'pm2_collection': collection if collection != 'ok' else ('ok' if valid else 'invalid' if matches else 'missing'), + 'status': env.get('status'), + 'restart_count': env.get('restart_time'), + 'exit_code': env.get('exit_code'), + 'uptime_seconds': (now_ms - started) // 1000 if env.get('status') == 'online' and started is not None and 0 < started <= now_ms else None, + } + return sanitize(result) + + +def submission_first_start(snapshot): + """A fresh, isolated Submission launch must not rely on PM2 recovery.""" + snapshot = sanitize(snapshot) + item = snapshot['services'].get('ulticode-submission', {}) + return (snapshot['collection'] == 'ok' + and item.get('pm2_collection') == 'ok' + and item.get('last_readiness') == 'ready' + and item.get('status') == 'online' + and item.get('restart_count') == 0) + + +def main(): + if sys.argv[1:2] == ['--require-submission-first-start']: + if os.environ.get('ACCEPTANCE_DISPOSABLE') != 'true' or len(sys.argv) != 3: + return 1 + snapshot = read_snapshot(Path(sys.argv[2]), os.environ.get('COMPOSE_PROJECT_NAME', '')) + passed = submission_first_start(snapshot) + print('Submission first start without restart: ' + ('PASS' if passed else 'FAIL')) + return 0 if passed else 1 + if os.environ.get('ACCEPTANCE_DISPOSABLE') != 'true': + return 0 + try: + root, selected, collection, *observations = sys.argv[1:] + collection, records = pm2_snapshot(sys.stdin.buffer, collection) + result = capture(selected, observations, collection, records, int(time.time() * 1000)) + project = os.environ.get('COMPOSE_PROJECT_NAME', '') + result['run_project'] = project if valid_project(project) else 'unknown' + directory = directory_fd(Path(root)) + temporary = f'.launch-readiness-{os.getpid()}.tmp' + try: + fd = os.open(temporary, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, + 0o600, dir_fd=directory) + try: + with os.fdopen(fd, 'w') as stream: + json.dump(result, stream) + os.replace(temporary, FILE, src_dir_fd=directory, dst_dir_fd=directory) + finally: + try: + os.unlink(temporary, dir_fd=directory) + except FileNotFoundError: + pass + finally: + os.close(directory) + return 0 + except (OSError, ValueError, TypeError): + return 1 + + +if __name__ == '__main__': + raise SystemExit(main()) diff --git a/scripts/ci/acceptance_report.py b/scripts/ci/acceptance_report.py index 1c4257dcec..9fdeaccf38 100644 --- a/scripts/ci/acceptance_report.py +++ b/scripts/ci/acceptance_report.py @@ -3,8 +3,13 @@ import json import os import re +import stat +import subprocess +import sys from pathlib import Path +from acceptance_readiness import directory_fd, read_snapshot, valid_project + LIMIT = 1_000_000 KINDS = re.compile(r"\b(?:org|com|java|javax|jakarta|io)\.(?:[A-Za-z_$][A-Za-z0-9_$]*\.)*[A-Za-z_$][A-Za-z0-9_$]*(?:Exception|Error)\b") FRAMES = re.compile(r"\bat (com\.ulticode\.[A-Za-z0-9_.$]+)\(([A-Za-z0-9_$]+\.java:[0-9]+)\)") @@ -12,6 +17,175 @@ "auth-environment", "fixtures", "browser-base", "browser-business", "complete"} +# Match the entire generated payload. Prefixes are private logger metadata, never emitted. +COUNT_TOKEN = r"(?:0|[1-9][0-9]{0,4}|UNKNOWN|OVERFLOW|ERROR)" +DISCOVERY = re.compile( + r"^SubmissionDiscoveryObservation outcome=(?Pready|deadline) elapsed_ms=(?P[0-9]{1,8}) " + r"interface=(?Pproblem_facts|user_read) " + r"state=(?PMANAGER_MISSING|REFERENCE_MISSING|CONFIG_MISSING|INVOKER_MISSING|INVOKER_PRESENT|UNSUPPORTED|INSPECTION_ERROR) " + r"invoker_kind=(?PSERVICE_DISCOVERY_MIGRATION|MIGRATION|CLUSTER|OTHER|MISSING) " + r"directory_kind=(?PSERVICE_DISCOVERY|DYNAMIC|OTHER|MISSING) " + r"destroyed=(?PTRUE|FALSE|UNKNOWN|ERROR) " + + " ".join(rf"{name}=(?P<{name}>{COUNT_TOKEN})" for name in ("all", "valid", "reconnect", "disabled")) + + r"$" +) + +APP_DISCOVERY = re.compile( + r"^AppDiscoveryObservation interface=(?Psubmission_user_query|auth_account_query) " + r"state=(?PMANAGER_MISSING|REFERENCE_MISSING|CONFIG_MISSING|INVOKER_MISSING|INVOKER_PRESENT|UNSUPPORTED|INSPECTION_ERROR) " + r"invoker_kind=(?PSERVICE_DISCOVERY_MIGRATION|MIGRATION|CLUSTER|OTHER|MISSING) " + r"directory_kind=(?PSERVICE_DISCOVERY|DYNAMIC|OTHER|MISSING) " + r"destroyed=(?PTRUE|FALSE|UNKNOWN|ERROR) " + + " ".join(rf"{name}=(?P<{name}>{COUNT_TOKEN})" for name in ("all", "valid", "reconnect", "disabled")) + + r"$" +) + + +APP_APPLICATION_DISCOVERY = re.compile( + APP_DISCOVERY.pattern.removesuffix(r"$").replace("^AppDiscoveryObservation ", "^AppApplicationDiscoveryObservation ") + + r" listener_state=(?PMISSING|PRESENT|UNSUPPORTED|ERROR) " + + " ".join(rf"{name}=(?P<{name}>{COUNT_TOKEN})" for name in ("mapped_apps", "observed_apps", "instances")) + + r"$" +) +ADMIN_FILE = "private-admin-readiness.json" +ADMIN_LIMIT = 65536 +POST_LAUNCH_PHASES = {"launch", "auth-environment", "fixtures", "browser-base", "browser-business", "complete"} + + +def admin_context(phase, project): + return (os.environ.get("ACCEPTANCE_DISPOSABLE") == "true" + and phase in POST_LAUNCH_PHASES and valid_project(project)) + + +def capture_admin_readiness(root, phase, project): + """One credentials-free, loopback-only observation, never a readiness gate.""" + if not admin_context(phase, project): + return + # The launcher already records PM2 evidence. Do not probe an endpoint when + # launch failed before creating Admin, or start/query any process ourselves. + admin = read_snapshot(root, project)["services"].get("ulticode-admin", {}) + if admin.get("pm2_collection") != "ok": + return + directory = directory_fd(root) + temporary = f".admin-readiness-{os.getpid()}.tmp" + created = False + try: + fd = os.open(temporary, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, + 0o600, dir_fd=directory) + created = True + with os.fdopen(fd, "wb") as stream: + # -q first disables curlrc; no redirects, proxies, netrc, auth or retries. + result = subprocess.run( + ["curl", "-q", "--silent", "--noproxy", "*", "--proxy", "", + "--proto", "=http", "--max-time", "3", "--connect-timeout", "3", + "--max-filesize", str(ADMIN_LIMIT), + "http://127.0.0.1:9102/api/v1/admin/health/ready"], + stdout=stream, stderr=subprocess.DEVNULL, timeout=4, check=False) + # curl without --fail preserves HTTP 503 bodies as well as 200 bodies. + if result.returncode == 0: + os.replace(temporary, ADMIN_FILE, src_dir_fd=directory, dst_dir_fd=directory) + finally: + if created: + try: + os.unlink(temporary, dir_fd=directory) + except FileNotFoundError: + pass + os.close(directory) + + +def admin_readiness_observation(root, phase, project): + result = {"status": "UNKNOWN", "components": {name: "UNKNOWN" for name in + ("db", "redis", "storage", "problemOwnerRpc")}} + if not admin_context(phase, project): + return result + try: + directory = directory_fd(root) + try: + fd = os.open(ADMIN_FILE, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK, dir_fd=directory) + finally: + os.close(directory) + with os.fdopen(fd, "rb") as stream: + info = os.fstat(stream.fileno()) + if (not stat.S_ISREG(info.st_mode) or info.st_size > ADMIN_LIMIT + or info.st_mtime < int(project.split("-")[2])): + return result + raw = stream.read(ADMIN_LIMIT + 1) + if len(raw) > ADMIN_LIMIT: + return result + value = json.loads(raw) + if not isinstance(value, dict): + return result + if value.get("status") in ("UP", "DOWN"): + result["status"] = value["status"] + components = value.get("components") + if isinstance(components, dict): + for name in result["components"]: + if components.get(name) in ("UP", "DOWN"): + result["components"][name] = components[name] + except (OSError, ValueError, TypeError, RecursionError): + pass + return result + + +def app_application_discovery_observations(root): + return _discovery_observations(root / "logs/backend-app.out.log", + "AppApplicationDiscoveryObservation ", APP_APPLICATION_DISCOVERY) + + +def discovery_observations(root): + return _discovery_observations(root / "logs/backend-submission.out.log", + "SubmissionDiscoveryObservation ", DISCOVERY) + + +def app_discovery_observations(root): + return _discovery_observations(root / "logs/backend-app.out.log", + "AppDiscoveryObservation ", APP_DISCOVERY) + + +def _discovery_observations(path, marker, pattern): + # Historical bounded log tail, not a same-process timeline or root-cause claim. + try: + directory = os.open(path.parent, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW) + try: + fd = os.open(path.name, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK, dir_fd=directory) + finally: + os.close(directory) + with os.fdopen(fd, "rb") as stream: + info = os.fstat(stream.fileno()) + if not stat.S_ISREG(info.st_mode): + return [] + offset = max(0, info.st_size - LIMIT) + stream.seek(offset) + text = stream.read(LIMIT).decode("utf-8", errors="replace") + if offset: + text = text.partition("\n")[2] # discard a potentially truncated record + except OSError: + return [] + observations = [] + for line in text.splitlines(): + # Logger prefixes are discarded, not trusted. Require one complete fixed payload. + if line.count(marker) != 1: + continue + payload = marker + line.partition(marker)[2] + match = pattern.fullmatch(payload) + if not match: + continue + item = match.groupdict() + if "elapsed_ms" in item: + item["elapsed_ms"] = int(item["elapsed_ms"]) + if item["elapsed_ms"] > 86_400_000: + continue + counts = [name for name in ("all", "valid", "reconnect", "disabled", "mapped_apps", "observed_apps", "instances") if name in item] + for name in counts: + if item[name].isdigit(): + item[name] = int(item[name]) + if any(isinstance(item[name], int) and item[name] > 10_000 + for name in counts): + continue + observations.append(item) + return observations[-16:] + + def summary(paths): kinds, frames, messages = set(), [], [] for path in paths: @@ -61,12 +235,26 @@ def build_report(root, phase, exit_code, project): "raw_logs_published": False, "exception_classes": private["exception_classes"], "service_diagnostics": services, + "service_diagnostics_scope": "historical-log-observations-not-root-cause", + "launch_readiness": read_snapshot(root, project), + "admin_post_launch_readiness": admin_readiness_observation(root, phase, project), + "admin_post_launch_readiness_scope": "post-launch-observation-not-same-timestamp-or-root-cause", + "submission_discovery_observations": discovery_observations(root), + "submission_discovery_scope": "historical-bounded-tail-not-process-correlated", + "app_discovery_observations": app_discovery_observations(root), + "app_discovery_scope": "historical-bounded-tail-not-process-correlated", + "app_application_discovery_observations": app_application_discovery_observations(root), + "app_application_discovery_scope": "historical-bounded-tail-not-process-correlated", } def main(): try: root = Path.cwd() + if sys.argv[1:] == ["--capture-admin-readiness"]: + capture_admin_readiness(root, os.environ.get("ACCEPTANCE_PHASE", "unknown"), + os.environ.get("COMPOSE_PROJECT_NAME", "")) + return 0 report = build_report(root, os.environ.get("ACCEPTANCE_PHASE", "unknown"), int(os.environ.get("ACCEPTANCE_EXIT", "1")), os.environ.get("COMPOSE_PROJECT_NAME", "")) @@ -75,7 +263,9 @@ def main(): output.write_text(json.dumps(report, indent=2) + "\n") if not report["passed"]: print(json.dumps({"acceptanceFailureDiagnostics": report}, separators=(",", ":"))) - except (OSError, ValueError): + except (OSError, ValueError, subprocess.SubprocessError): + if sys.argv[1:] == ["--capture-admin-readiness"]: + return 0 # Optional diagnostics cannot change the acceptance outcome. print("Acceptance report could not be written; private logs were not printed") return 1 return 0 diff --git a/scripts/ci/business-acceptance.sh b/scripts/ci/business-acceptance.sh index 065e81ee45..4ced509e3f 100644 --- a/scripts/ci/business-acceptance.sh +++ b/scripts/ci/business-acceptance.sh @@ -31,12 +31,16 @@ unset MIGRATION_MYSQL_CONTAINER MIGRATION_MYSQL_CONTAINER_PORT MYSQL_CONTAINER \ DEV_LOCAL_OWNER_BASELINE_CONFIRM 2>/dev/null || true phase=environment prepared=false +launch_started=false finish() { status=$? trap - EXIT # Report generation must never prevent cleanup of our disposable processes. set +e export ACCEPTANCE_PHASE="$phase" ACCEPTANCE_EXIT="$status" + if [[ "$launch_started" == true ]]; then + python3 scripts/ci/acceptance_report.py --capture-admin-readiness >/dev/null 2>&1 || true + fi python3 scripts/ci/acceptance_report.py report_status=$? if [[ "$status" == 0 && "$report_status" != 0 ]]; then status="$report_status"; fi @@ -96,9 +100,11 @@ SUBMISSION_CUTOVER_CONFIRM=I_UNDERSTAND_SUBMISSION_CUTOVER \ ./scripts/runbooks/submission-schema-cutover.sh verify >> .local/acceptance/private-cutover.log 2>&1 export SUBMISSION_CUTOVER_COMPLETE=true phase=launch +launch_started=true # Follow the documented second startup: migrations are idempotent, and the # supported DEV-LOCAL App seed adapter runs only when migrations are enabled. ./scripts/dev/up.sh --scope full-stack --skip-infra --skip-install --rebuild > .local/acceptance/private-launch.log 2>&1 +python3 scripts/ci/acceptance_readiness.py --require-submission-first-start "$ROOT" phase=auth-environment node --input-type=module <<'JS' import { execFileSync } from 'node:child_process' diff --git a/scripts/dev/lib/pm2.sh b/scripts/dev/lib/pm2.sh index db4bd21530..ef3812476c 100644 --- a/scripts/dev/lib/pm2.sh +++ b/scripts/dev/lib/pm2.sh @@ -11,10 +11,18 @@ if ! [[ -v __ULTICODE_PM2_SOURCED ]]; then pm2_load_records() { PM2_RECORDS="" + if [[ "${ACCEPTANCE_DISPOSABLE:-}" == true ]]; then + ACCEPTANCE_PM2_JSON="" + ACCEPTANCE_PM2_COLLECTION=failed + fi command -v pm2 >/dev/null 2>&1 || return 0 local raw parsed raw="$(pm2 jlist 2>/dev/null)" || return 0 + if [[ "${ACCEPTANCE_DISPOSABLE:-}" == true ]]; then + ACCEPTANCE_PM2_JSON="$raw" + ACCEPTANCE_PM2_COLLECTION=ok + fi [[ -n "$raw" && "$raw" != "[]" ]] || return 0 command -v node >/dev/null 2>&1 || return 0 diff --git a/scripts/dev/up-test.sh b/scripts/dev/up-test.sh index 97fb7ded72..9ca7f6bded 100755 --- a/scripts/dev/up-test.sh +++ b/scripts/dev/up-test.sh @@ -84,4 +84,5 @@ if ((failures > 0)); then echo "up-test.sh: $failures failure(s)" >&2 exit 1 fi +python3 "$ROOT_DIR/scripts/test/test_pm2_startup_order.py" echo "up-test.sh: all assertions passed" diff --git a/scripts/dev/up.sh b/scripts/dev/up.sh index 9d8f36d8ba..e12531cf75 100755 --- a/scripts/dev/up.sh +++ b/scripts/dev/up.sh @@ -350,6 +350,26 @@ if [[ ",$INFRA_TARGETS," == *,rustfs,* ]]; then fi mapfile -t SELECTED_READINESS < <(devstack_readiness_for_selection "$DEV_SCOPE" "$PM2_APPS") +# Capture observations only for disposable acceptance. The EXIT hook never changes +# launch status; PM2 metadata comes from the last existing readiness query. +declare -A ACCEPTANCE_LAST_READINESS=() +acceptance_launch_snapshot() { + local status=$? app + local -a observations=() + for app in "${!ACCEPTANCE_LAST_READINESS[@]}"; do + observations+=("$app=${ACCEPTANCE_LAST_READINESS[$app]}") + done + printf '%s' "${ACCEPTANCE_PM2_JSON:-}" | \ + python3 "$ROOT_DIR/scripts/ci/acceptance_readiness.py" "$ROOT_DIR" "$PM2_APPS" \ + "${ACCEPTANCE_PM2_COLLECTION:-failed}" "${observations[@]}" >/dev/null 2>&1 || true + return "$status" +} +if [[ "${ACCEPTANCE_DISPOSABLE:-}" == true && "$PREPARE_SUBMISSION_OWNER" != true ]]; then + ACCEPTANCE_PM2_JSON="" + ACCEPTANCE_PM2_COLLECTION=failed + trap acceptance_launch_snapshot EXIT +fi + resolve_mysql_container() { local configured="${MIGRATION_MYSQL_CONTAINER:-${MYSQL_CONTAINER:-}}" if [[ -n "$configured" ]] && container_running "$configured"; then @@ -620,6 +640,7 @@ if [[ "$REBUILD" == true && "$FRONTEND_ONLY" != true ]] && [[ ",$PM2_APPS," == * fi # ===== 步骤 4: dev-admin bootstrap (依赖 auth 的 Dubbo provider, 故先启动 auth) ===== +AUTH_STARTED_FOR_BOOTSTRAP=false if [[ "$SKIP_BOOTSTRAP" != true && "$DEV_SEED_USERS_ENABLED" == "true" \ && ",$PM2_APPS," == *,ulticode-auth,* ]]; then # UserProvisioningAdapter 通过 Dubbo RPC 调用 backend-auth 的 @@ -707,6 +728,7 @@ if [[ "$SKIP_BOOTSTRAP" != true && "$DEV_SEED_USERS_ENABLED" == "true" \ esac # The bootstrap key is handed only to the short-lived provisioning JVM; # no legacy symmetric bootstrap secret is exported by this launcher. + AUTH_STARTED_FOR_BOOTSTRAP=true else echo "Skipping dev-admin bootstrap (--skip-bootstrap / --quick / --frontend-only)." fi @@ -787,10 +809,9 @@ preflight_service_ports() { preflight_service_ports # ===== 步骤 6: PM2 服务 ===== -# Judge 依赖 backend-app 的 Dubbo provider (SubmissionFencePort 等)。 -# 若本次同时启动 app 与 judge, 先起 app 并等其健康, 再起 judge, -# 避免 judge 冷启动时 provider 尚未注册导致的连接超时噪音; -# Dubbo 框架本身会自动重连, 此顺序只为让首次启动即干净连接。 +# Submission 与 Judge 依赖 backend-app 的 Dubbo provider。 +# 同时选择 App 时先等待其 HTTP readiness,再启动这两个消费者。 +# HTTP readiness 不证明 Dubbo 注册完成;各服务自己的 discovery gate 保持不变。 echo "Starting PM2 services: $PM2_APPS" ( cd "$ROOT_DIR" @@ -825,20 +846,57 @@ echo "Starting PM2 services: $PM2_APPS" echo "Named scope $DEV_SCOPE: leaving processes of other scopes running (use stop.sh --scope/--all to tear down)." >&2 ;; esac - if [[ ",$PM2_APPS," == *",ulticode-submission,"* ]]; then + record_submission_offset() { mkdir -p "$ROOT_DIR/logs" if [[ -f "$ROOT_DIR/logs/backend-submission.out.log" ]]; then wc -c < "$ROOT_DIR/logs/backend-submission.out.log" > "$ROOT_DIR/logs/.submission-ready-offset" else echo 0 > "$ROOT_DIR/logs/.submission-ready-offset" fi + } + # Reuse Auth started with this invocation's configuration for bootstrap. + # Restarting it alongside App withdraws a provider while App subscribes. + auth_started="$AUTH_STARTED_FOR_BOOTSTRAP" + if [[ ",$PM2_APPS," == *",ulticode-app,"* && ",$PM2_APPS," == *",ulticode-auth,"* ]]; then + if [[ "$auth_started" != true ]]; then + pm2 startOrRestart ecosystem.config.cjs --only ulticode-auth --update-env + auth_started=true + fi + auth_ready=false + for _ in $(seq 1 "$DEVSTACK_SERVICE_READINESS_ATTEMPTS"); do + if [[ "$(curl -sS -o /dev/null -w '%{http_code}' --max-time 5 \ + http://127.0.0.1:9101/api/v1/auth/health/ready 2>/dev/null || true)" == "200" ]]; then + auth_ready=true + break + fi + sleep "$DEVSTACK_READINESS_INTERVAL_SECONDS" + done + if [[ "$auth_ready" != true ]]; then + echo "backend-auth did not become healthy; not starting App or its deferred consumers. Fix Auth readiness and re-run up.sh." >&2 + exit 1 + fi fi - if [[ ",$PM2_APPS," == *",ulticode-judge,"* && ",$PM2_APPS," == *",ulticode-app,"* ]]; then - first="${PM2_APPS//ulticode-judge,/}" - first="${first//,ulticode-judge/}" - pm2 startOrRestart ecosystem.config.cjs \ - --only "$first" \ - --update-env + # Partition the exact selected set without adding dependencies to partial scopes. + first=() + deferred=() + IFS=',' read -ra selected <<< "$PM2_APPS" + for app in "${selected[@]}"; do + if [[ "$app" == ulticode-auth && "$auth_started" == true ]]; then + continue + fi + if [[ ",$PM2_APPS," == *",ulticode-app,"* ]] \ + && [[ "$app" == ulticode-submission || "$app" == ulticode-judge ]]; then + deferred+=("$app") + else + first+=("$app") + fi + done + if [[ "${#deferred[@]}" -gt 0 ]]; then + if [[ "${#first[@]}" -gt 0 ]]; then + pm2 startOrRestart ecosystem.config.cjs \ + --only "$(devstack_apps_csv "${first[@]}")" \ + --update-env + fi app_ready=false for _ in $(seq 1 "$DEVSTACK_SERVICE_READINESS_ATTEMPTS"); do if [[ "$(curl -sS -o /dev/null -w '%{http_code}' --max-time 5 \ @@ -849,19 +907,33 @@ echo "Starting PM2 services: $PM2_APPS" sleep "$DEVSTACK_READINESS_INTERVAL_SECONDS" done if [[ "$app_ready" != true ]]; then + if [[ ",$PM2_APPS," == *",ulticode-submission,"* ]]; then + echo "backend-app did not become healthy; not starting deferred Submission/Judge services. Fix App readiness and re-run up.sh." >&2 + exit 1 + fi echo "backend-app did not become healthy; starting ulticode-judge anyway (Dubbo auto-reconnect)." >&2 fi - record_judge_offset - pm2 startOrRestart ecosystem.config.cjs \ - --only ulticode-judge \ - --update-env + for app in "${deferred[@]}"; do + case "$app" in + ulticode-submission) record_submission_offset ;; + ulticode-judge) record_judge_offset ;; + esac + pm2 startOrRestart ecosystem.config.cjs \ + --only "$app" \ + --update-env + done else + if [[ ",$PM2_APPS," == *",ulticode-submission,"* ]]; then + record_submission_offset + fi if [[ ",$PM2_APPS," == *",ulticode-judge,"* ]]; then record_judge_offset fi - pm2 startOrRestart ecosystem.config.cjs \ - --only "$PM2_APPS" \ - --update-env + if [[ "${#first[@]}" -gt 0 ]]; then + pm2 startOrRestart ecosystem.config.cjs \ + --only "$(devstack_apps_csv "${first[@]}")" \ + --update-env + fi fi pm2 save ) @@ -910,10 +982,15 @@ for _ in $(seq 1 "$DEVSTACK_SERVICE_READINESS_ATTEMPTS"); do all_ok=true for readiness in "${SELECTED_READINESS[@]}"; do IFS='|' read -r app readiness_kind readiness_port readiness_path <<< "$readiness" + observation=ready case "$readiness_kind" in - http) check_port "$readiness_port" "$readiness_path" || all_ok=false ;; - pm2) check_pm2_online "$app" || all_ok=false ;; + http) check_port "$readiness_port" "$readiness_path" || observation=not_ready ;; + pm2) check_pm2_online "$app" || observation=not_ready ;; esac + [[ "$observation" == ready ]] || all_ok=false + if [[ "${ACCEPTANCE_DISPOSABLE:-}" == true ]]; then + ACCEPTANCE_LAST_READINESS["$app"]="$observation" + fi done if [[ "$all_ok" == true ]]; then printf 'Development stack is ready (scope: %s; mode: %s; services: %s).\n' \ diff --git a/scripts/test/cicd-contract.sh b/scripts/test/cicd-contract.sh index c525e5e401..1de14d2ecb 100644 --- a/scripts/test/cicd-contract.sh +++ b/scripts/test/cicd-contract.sh @@ -17,4 +17,5 @@ bash "$ROOT_DIR/scripts/test/deploy-runtime-contract.sh" bash "$ROOT_DIR/scripts/test/ssh-host-identity-contract.sh" python3 "$ROOT_DIR/scripts/test/verified-release-package-contract.py" -v python3 "$ROOT_DIR/scripts/test/test_acceptance_report.py" -v +python3 "$ROOT_DIR/scripts/test/test_acceptance_readiness.py" -v node "$ROOT_DIR/scripts/test/browser-auth-diagnostics.test.mjs" diff --git a/scripts/test/supply-chain-contract.sh b/scripts/test/supply-chain-contract.sh index 6807390394..448827b24f 100755 --- a/scripts/test/supply-chain-contract.sh +++ b/scripts/test/supply-chain-contract.sh @@ -177,7 +177,7 @@ for service in owner_services: owner_pom = repo / 'services' / service['module'].split('/', 1)[0] / 'pom.xml' owner_properties = ET.parse(owner_pom).getroot().find('m:properties', ns) owner_version = owner_properties.findtext(f'm:{prop}', namespaces=ns) - expected_version = '1.0.5' + expected_version = '1.0.6' assert root_version == owner_version == expected_version, f'{service["name"]}: release version mismatch (root={root_version!r}, owner={owner_version!r}, expected={expected_version!r})' assert 'actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333' in workflow print('digest-only candidates, common normalized repo and all-service promote gate: PASS') diff --git a/scripts/test/test_acceptance_readiness.py b/scripts/test/test_acceptance_readiness.py new file mode 100644 index 0000000000..766f4d3ee4 --- /dev/null +++ b/scripts/test/test_acceptance_readiness.py @@ -0,0 +1,204 @@ +#!/usr/bin/env python3 +import io +import json +import os +import shutil +import subprocess +from pathlib import Path +import sys +import tempfile +import unittest +from unittest.mock import patch + +ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(ROOT / 'scripts/ci')) +import acceptance_readiness as module + +PROJECT = 'ulticode-acceptance-123-456' + + +class ReadinessTests(unittest.TestCase): + def test_submission_first_start_requires_ready_without_any_restart(self): + good = module.capture('ulticode-submission', ['ulticode-submission=ready'], + 'ok', [{'name': 'ulticode-submission', 'pm2_env': { + 'status': 'online', 'restart_time': 0, 'pm_uptime': 1000}}], 7000) + self.assertTrue(module.submission_first_start(good)) + for field, values in { + 'restart_count': (1, None, True), + 'status': ('errored', 'unknown'), + 'last_readiness': ('not_ready', 'not_checked'), + 'pm2_collection': ('failed', 'missing', 'invalid'), + }.items(): + for value in values: + with self.subTest(field=field, value=value): + bad = json.loads(json.dumps(good)) + bad['services']['ulticode-submission'][field] = value + self.assertFalse(module.submission_first_start(bad)) + for collection in ('missing', 'invalid', 'failed'): + self.assertFalse(module.submission_first_start(good | {'collection': collection})) + self.assertFalse(module.submission_first_start(module.empty('ok'))) + + def test_first_start_command_rejects_restart_missing_and_stale_evidence(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + parent = root / '.local/acceptance' + parent.mkdir(parents=True) + good = module.capture('ulticode-submission', ['ulticode-submission=ready'], + 'ok', [{'name': 'ulticode-submission', 'pm2_env': { + 'status': 'online', 'restart_time': 0, 'pm_uptime': 1000}}], 7000) + target = parent / module.FILE + environment = dict(os.environ, COMPOSE_PROJECT_NAME=PROJECT, ACCEPTANCE_DISPOSABLE='true') + command = [sys.executable, str(ROOT / 'scripts/ci/acceptance_readiness.py'), + '--require-submission-first-start', directory] + for restart, project, expected in ((0, PROJECT, 0), (1, PROJECT, 1), + (0, PROJECT + '1', 1)): + good['services']['ulticode-submission']['restart_count'] = restart + target.write_text(json.dumps(good | {'run_project': project})) + run = subprocess.run(command, env=environment, capture_output=True) + self.assertEqual(run.returncode, expected, run.stderr.decode()) + target.unlink() + self.assertEqual(subprocess.run(command, env=environment, capture_output=True).returncode, 1) + environment['ACCEPTANCE_DISPOSABLE'] = 'false' + self.assertEqual(subprocess.run(command, env=environment, capture_output=True).returncode, 1) + source = (ROOT / 'scripts/ci/business-acceptance.sh').read_text() + self.assertLess(source.index('--rebuild >'), source.index('--require-submission-first-start')) + self.assertLess(source.index('--require-submission-first-start'), source.index('phase=auth-environment')) + + def test_all_services_and_last_observation_are_independent_of_pm2(self): + result = module.capture(','.join(module.SERVICES), + ['ulticode-app=not_ready', 'ulticode-app=ready', 'ulticode-search=not_ready'], + 'ok', [{'name': 'ulticode-search', 'pm2_env': { + 'status': 'online', 'restart_time': 3, 'exit_code': 1, 'pm_uptime': 1000, + 'env': {'PASSWORD': 'secret-canary'}, 'pm_exec_path': 'secret-canary'}}], 7000) + self.assertEqual(set(result['services']), set(module.SERVICES)) + search = result['services']['ulticode-search'] + self.assertEqual(search, dict(last_readiness='not_ready', pm2_collection='ok', + status='online', restart_count=3, exit_code=1, uptime_seconds=6)) + self.assertEqual(result['services']['ulticode-app']['last_readiness'], 'ready') + self.assertEqual(result['services']['ulticode-9002']['last_readiness'], 'not_checked') + self.assertEqual(result['services']['ulticode-notification']['pm2_collection'], 'missing') + self.assertNotIn('secret-canary', json.dumps(result)) + + def test_malicious_fields_are_not_serialized(self): + result = module.capture('ulticode-app,secret-canary', ['ulticode-app=secret-canary'], + 'ok', [{'name': 'ulticode-app', 'pm2_env': {'status': ['secret-canary'], + 'restart_time': True, 'exit_code': -1, 'pm_uptime': 1e100}}], 10000) + self.assertNotIn('secret-canary', json.dumps(result)) + item = result['services']['ulticode-app'] + self.assertEqual(item['status'], 'unknown') + self.assertIsNone(item['restart_count']) + self.assertIsNone(item['exit_code']) + self.assertIsNone(item['uptime_seconds']) + for value in (None, [], 'secret-canary', {'schema_version': True}, + {'schema_version': 1, 'collection': [], 'services': {}}): + self.assertNotIn('secret-canary', json.dumps(module.sanitize(value))) + + def test_collection_failure_duplicate_and_absent_records(self): + for status in ('failed', 'invalid'): + item = module.capture('ulticode-app', [], status, [], 1000)['services']['ulticode-app'] + self.assertEqual(item['pm2_collection'], status) + item = module.capture('ulticode-app', [], 'ok', [{'name': 'ulticode-app'}] * 2, 1000) + self.assertEqual(item['services']['ulticode-app']['pm2_collection'], 'invalid') + self.assertEqual(module.pm2_snapshot(io.BytesIO(b''), 'failed'), ('failed', [])) + for raw in (b'not-json-secret-canary', b'{}', b'x' * 1048577): + self.assertEqual(module.pm2_snapshot(io.BytesIO(raw), 'ok'), ('invalid', [])) + self.assertEqual(module.pm2_snapshot(io.BytesIO(b'[]'), 'ok'), ('ok', [])) + + def test_snapshot_rejects_missing_stale_symlink_fifo_oversize_and_bad_json(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + parent = root / '.local/acceptance' + parent.mkdir(parents=True) + target = parent / module.FILE + self.assertEqual(module.read_snapshot(root, PROJECT)['collection'], 'missing') + value = module.empty('ok') | {'run_project': PROJECT, 'secret': 'secret-canary'} + target.write_text(json.dumps(value)) + self.assertEqual(module.read_snapshot(root, PROJECT), module.empty('ok')) + self.assertEqual(module.read_snapshot(root, PROJECT + '1')['collection'], 'invalid') + for raw in ('secret-canary', 'x' * (module.LIMIT + 1), '[' * 2000): + target.write_text(raw) + self.assertEqual(module.read_snapshot(root, PROJECT)['collection'], 'invalid') + target.unlink() + target.symlink_to(root / 'private') + self.assertEqual(module.read_snapshot(root, PROJECT)['collection'], 'invalid') + target.unlink() + os.mkfifo(target) + self.assertEqual(module.read_snapshot(root, PROJECT)['collection'], 'invalid') + target.unlink() + parent.rmdir() + parent.symlink_to(root, target_is_directory=True) + self.assertEqual(module.read_snapshot(root, PROJECT)['collection'], 'invalid') + + def test_actual_launcher_loop_preserves_probes_exit_and_prepare_isolation(self): + source = (ROOT / 'scripts/dev/up.sh').read_text() + hook = source.split('declare -A ACCEPTANCE_LAST_READINESS=()', 1)[1].split('resolve_mysql_container()', 1)[0] + hook = 'declare -A ACCEPTANCE_LAST_READINESS=()' + hook + loop = source[source.rindex('for _ in $(seq 1 "$DEVSTACK_SERVICE_READINESS_ATTEMPTS"); do'):] + for failed, prepare, broken_writer in ((False, False, False), (True, False, False), + (True, True, False), (True, False, True)): + with self.subTest(failed=failed, prepare=prepare, broken_writer=broken_writer), tempfile.TemporaryDirectory() as directory: + root = Path(directory) + (root / '.local/acceptance').mkdir(parents=True) + (root / 'scripts/ci').mkdir(parents=True) + shutil.copy(ROOT / 'scripts/ci/acceptance_readiness.py', root / 'scripts/ci') + setup = r'''set -euo pipefail +ROOT_DIR="$1" +ACCEPTANCE_DISPOSABLE=true +export ACCEPTANCE_DISPOSABLE COMPOSE_PROJECT_NAME=ulticode-acceptance-123-456 +PREPARE_SUBMISSION_OWNER="$2" +FAILED="$3" +PM2_APPS=ulticode-auth,ulticode-search,ulticode-9002 +SELECTED_READINESS=('ulticode-auth|http|9101|/' 'ulticode-search|pm2|9107|/' 'ulticode-9002|http|9002|/') +DEVSTACK_SERVICE_READINESS_ATTEMPTS=2 +DEVSTACK_READINESS_INTERVAL_SECONDS=0 +DEV_SCOPE=full-stack DEV_MODE=dev-full INFRA_TARGETS='' +DEVSTACK_OWNER_MIGRATION_ORDER=() DEV_SEED_USERS_ENABLED=false +check_port() { echo "$1" >> "$ROOT_DIR/probes"; [[ "$FAILED" != true || "$1" != 9002 ]]; } +check_pm2_online() { echo search >> "$ROOT_DIR/probes"; ACCEPTANCE_PM2_JSON='[]'; ACCEPTANCE_PM2_COLLECTION=ok; } +devstack_apps_csv() { :; } +devstack_app_label() { :; } +pm2() { :; } +sleep() { :; } +''' + if broken_writer: + setup += 'python3() { return 42; }\n' + run = subprocess.run(['bash', '-c', setup + hook + loop, 'test', directory, + str(prepare).lower(), str(failed).lower()], capture_output=True) + self.assertEqual(run.returncode, int(failed), run.stderr.decode()) + self.assertEqual((root / 'probes').read_text().splitlines(), + ['9101', 'search', '9002'] * (2 if failed else 1)) + snapshot = module.read_snapshot(root, PROJECT) + if prepare or broken_writer: + self.assertEqual(snapshot['collection'], 'missing') + else: + self.assertEqual(snapshot['services']['ulticode-9002']['last_readiness'], + 'not_ready' if failed else 'ready') + self.assertEqual(snapshot['services']['ulticode-auth']['last_readiness'], 'ready') + + def test_pm2_adapter_reuses_one_query_and_clears_failed_query(self): + script = r'''set -euo pipefail +source "$1/scripts/dev/lib/pm2.sh" +ACCEPTANCE_DISPOSABLE=true +CALLS="$2" +pm2() { echo called >> "$CALLS"; printf '[]'; } +pm2_load_records +[[ "$ACCEPTANCE_PM2_JSON" == '[]' && "$ACCEPTANCE_PM2_COLLECTION" == ok ]] +pm2() { echo called >> "$CALLS"; return 1; } +pm2_load_records +[[ -z "$ACCEPTANCE_PM2_JSON" && "$ACCEPTANCE_PM2_COLLECTION" == failed ]] +''' + with tempfile.TemporaryDirectory() as directory: + calls = Path(directory) / 'calls' + run = subprocess.run(['bash', '-c', script, 'test', str(ROOT), str(calls)], capture_output=True) + self.assertEqual(run.returncode, 0, run.stderr.decode()) + self.assertEqual(calls.read_text().splitlines(), ['called', 'called']) + + def test_normal_development_does_not_collect(self): + with patch.dict(os.environ, {'ACCEPTANCE_DISPOSABLE': 'false'}), \ + patch.object(module, 'pm2_snapshot') as collect: + self.assertEqual(module.main(), 0) + collect.assert_not_called() + + +if __name__ == '__main__': + unittest.main() diff --git a/scripts/test/test_acceptance_report.py b/scripts/test/test_acceptance_report.py index 0c70ba82ab..346931b00d 100644 --- a/scripts/test/test_acceptance_report.py +++ b/scripts/test/test_acceptance_report.py @@ -1,11 +1,17 @@ #!/usr/bin/env python3 import importlib.util import json +import os +import subprocess +import time import tempfile +import sys import unittest +from unittest import mock from pathlib import Path ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(ROOT / "scripts/ci")) spec = importlib.util.spec_from_file_location("acceptance_report", ROOT / "scripts/ci/acceptance_report.py") module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) @@ -51,6 +57,281 @@ def test_symlinks_unrelated_files_and_untrusted_metadata_are_excluded(self): self.assertEqual(report["project"], "unknown") self.assertNotIn("private-canary", json.dumps(report)) + def test_discovery_observations_are_strict_bounded_and_do_not_expose_messages(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + (root / "logs").mkdir() + payload = ("SubmissionDiscoveryObservation outcome=deadline elapsed_ms=120000 interface=problem_facts " + "state=INVOKER_PRESENT invoker_kind=SERVICE_DISCOVERY_MIGRATION directory_kind=SERVICE_DISCOVERY " + "destroyed=FALSE all=1 valid=0 reconnect=1 disabled=1") + invalid = [payload + " password=secret-canary", payload.replace("INVOKER_PRESENT", "secret-canary"), + payload.replace("120000", "99999999"), payload.replace("120000", "-1"), + payload.replace("deadline", "https://secret-canary"), payload.replace("120000", "1e5"), payload + " " + payload, + payload.replace("all=1", "all=10001"), payload.replace("valid=0", "valid=-1"), + payload.replace("valid=0", "valid=00"), payload.replace("reconnect=1", "reconnect=1e5"), + payload.replace("disabled=1", "disabled=secret-canary"), + payload.replace("interface=problem_facts", "interface=secret-canary"), + payload.replace("invoker_kind=SERVICE_DISCOVERY_MIGRATION", "invoker_kind=secret-canary"), + payload.replace("directory_kind=SERVICE_DISCOVERY", "directory_kind=secret-canary"), + payload.replace("destroyed=FALSE", "destroyed=secret-canary"), + payload.replace("state=INVOKER_PRESENT", "state=AVAILABLE")] + (root / "logs/backend-submission.out.log").write_text( + "\n".join(invalid + ["private-prefix-canary " + payload] * 20)) + report = module.build_report(root, "launch", 1, "ulticode-acceptance-123-456") + entries = report["submission_discovery_observations"] + self.assertEqual(len(entries), 16) + self.assertEqual(entries[0], {"outcome": "deadline", "elapsed_ms": 120000, + "interface": "problem_facts", "state": "INVOKER_PRESENT", + "invoker_kind": "SERVICE_DISCOVERY_MIGRATION", "directory_kind": "SERVICE_DISCOVERY", + "destroyed": "FALSE", "all": 1, "valid": 0, "reconnect": 1, "disabled": 1}) + self.assertNotIn("canary", json.dumps(report)) + self.assertEqual(report["submission_discovery_scope"], "historical-bounded-tail-not-process-correlated") + # Malformed records alone do not become evidence. + (root / "logs/backend-submission.out.log").write_text("\n".join(invalid)) + self.assertEqual(module.discovery_observations(root), []) + + def test_discovery_observations_exclude_symlinks_and_truncated_records(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + (root / "logs").mkdir() + outside = root / "private-input" + payload = ("SubmissionDiscoveryObservation outcome=ready elapsed_ms=1 interface=user_read " + "state=INVOKER_PRESENT invoker_kind=CLUSTER directory_kind=DYNAMIC " + "destroyed=TRUE all=0 valid=UNKNOWN reconnect=OVERFLOW disabled=ERROR") + outside.write_text(payload) + log = root / "logs/backend-submission.out.log" + log.symlink_to(outside) + self.assertEqual(module.discovery_observations(root), []) + log.unlink() + log.write_text(payload) + observation = module.discovery_observations(root)[0] + self.assertEqual(observation["all"], 0) + self.assertEqual(observation["valid"], "UNKNOWN") + self.assertEqual(observation["reconnect"], "OVERFLOW") + self.assertEqual(observation["disabled"], "ERROR") + log.write_text(payload.replace("all=0", "all=10000")) + self.assertEqual(module.discovery_observations(root)[0]["all"], 10000) + log.write_text("x" * module.LIMIT + payload) + self.assertEqual(module.discovery_observations(root), []) + + def test_app_discovery_is_strict_bounded_and_redacted(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + (root / "logs").mkdir() + log = root / "logs/backend-app.out.log" + payload = ("AppDiscoveryObservation interface=submission_user_query " + "state=INVOKER_PRESENT invoker_kind=MIGRATION directory_kind=SERVICE_DISCOVERY " + "destroyed=FALSE all=1 valid=1 reconnect=0 disabled=0") + invalid = [payload + " password=secret-canary", payload + " " + payload, + payload.replace("interface=submission_user_query", "interface=secret-canary"), + payload.replace("state=INVOKER_PRESENT", "state=AVAILABLE"), + payload.replace("invoker_kind=MIGRATION", "invoker_kind=secret-canary"), + payload.replace("directory_kind=SERVICE_DISCOVERY", "directory_kind=secret-canary"), + payload.replace("destroyed=FALSE", "destroyed=secret-canary"), + payload.replace("all=1", "all=10001"), payload.replace("all=1", "all=00"), + payload.replace("valid=1", "valid=-1"), payload.replace("reconnect=0", "reconnect=1e5"), + payload.replace("disabled=0", "disabled=https://secret-canary"), + payload.replace("interface=", "outcome=ready elapsed_ms=1 interface="), + payload.replace("AppDiscoveryObservation", "SubmissionDiscoveryObservation")] + log.write_text("\n".join(invalid)) + self.assertEqual(module.app_discovery_observations(root), []) + valid = [payload.replace("all=1", f"all={index}") for index in range(20)] + log.write_text("\n".join(invalid + ["private-prefix-canary " + item for item in valid])) + report = module.build_report(root, "browser-base", 1, "ulticode-acceptance-123-456") + entries = report["app_discovery_observations"] + self.assertEqual(len(entries), 16) + self.assertEqual([item["all"] for item in entries], list(range(4, 20))) + self.assertEqual(entries[0], {"interface": "submission_user_query", "state": "INVOKER_PRESENT", + "invoker_kind": "MIGRATION", "directory_kind": "SERVICE_DISCOVERY", + "destroyed": "FALSE", "all": 4, "valid": 1, "reconnect": 0, "disabled": 0}) + self.assertEqual(report["app_discovery_scope"], "historical-bounded-tail-not-process-correlated") + self.assertEqual(report["submission_discovery_observations"], []) + self.assertNotIn("canary", json.dumps(report)) + self.assertNotIn("https://", json.dumps(report)) + + def test_app_discovery_excludes_symlinks_missing_and_truncated_records(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + (root / "logs").mkdir() + self.assertEqual(module.app_discovery_observations(root), []) + outside = root / "private-input" + payload = ("AppDiscoveryObservation interface=auth_account_query " + "state=INVOKER_PRESENT invoker_kind=CLUSTER directory_kind=DYNAMIC " + "destroyed=TRUE all=10000 valid=UNKNOWN reconnect=OVERFLOW disabled=ERROR") + outside.write_text(payload) + log = root / "logs/backend-app.out.log" + log.symlink_to(outside) + self.assertEqual(module.app_discovery_observations(root), []) + log.unlink() + log.write_text(payload) + item = module.app_discovery_observations(root)[0] + self.assertEqual(item["interface"], "auth_account_query") + self.assertEqual(item["all"], 10000) + self.assertEqual(item["valid"], "UNKNOWN") + self.assertEqual(item["reconnect"], "OVERFLOW") + self.assertEqual(item["disabled"], "ERROR") + log.write_text("x" * module.LIMIT + payload) + self.assertEqual(module.app_discovery_observations(root), []) + log.write_text("x" * module.LIMIT + "\n" + payload + "\n" + payload[:-5]) + self.assertEqual(module.app_discovery_observations(root), [item]) + log.write_text(payload.replace("all=10000", "all=1000" + "\ufffd")) + self.assertEqual(module.app_discovery_observations(root), []) + + +class AdditionalObservationTests(unittest.TestCase): + def setUp(self): + self.temp = tempfile.TemporaryDirectory() + self.addCleanup(self.temp.cleanup) + self.root = Path(self.temp.name) + (self.root / "logs").mkdir() + (self.root / ".local/acceptance").mkdir(parents=True) + self.project = f"ulticode-acceptance-{int(time.time())}-456" + self.env = mock.patch.dict(os.environ, {"ACCEPTANCE_DISPOSABLE": "true"}) + self.env.start() + self.addCleanup(self.env.stop) + self.body = self.root / ".local/acceptance" / module.ADMIN_FILE + + def launched(self): + (self.body.parent / "launch-readiness.json").write_text(json.dumps({ + "schema_version": 1, "run_project": self.project, "collection": "ok", + "services": {"ulticode-admin": {"pm2_collection": "ok", "status": "online"}}})) + + def observe(self, phase="launch", project=None): + return module.admin_readiness_observation(self.root, phase, project or self.project) + + def test_application_discovery_schema_bounds_aliases_and_secrets(self): + log = self.root / "logs/backend-app.out.log" + payload = ("AppApplicationDiscoveryObservation interface=submission_user_query " + "state=INVOKER_PRESENT invoker_kind=SERVICE_DISCOVERY_MIGRATION directory_kind=SERVICE_DISCOVERY " + "destroyed=FALSE all=1 valid=0 reconnect=OVERFLOW disabled=ERROR " + "listener_state=PRESENT mapped_apps=1 observed_apps=UNKNOWN instances=10000") + invalid = [payload + " password=secret-canary", payload + " " + payload, + payload.replace("listener_state=PRESENT", "listener_state=secret-canary"), + payload.replace("AppApplicationDiscoveryObservation", "AppDiscoveryObservation"), + payload.replace("submission_user_query", "secret-canary")] + for name in ("all", "valid", "reconnect", "disabled", "mapped_apps", "observed_apps", "instances"): + for value in ("-1", "00", "10001", "999999", "1e2", "secret-canary", "1.0"): + import re + invalid.append(re.sub(rf"{name}=\S+", f"{name}={value}", payload)) + log.write_text("\n".join(invalid)) + self.assertEqual(module.app_application_discovery_observations(self.root), []) + log.write_text("\n".join(invalid + ["secret-prefix-canary " + payload] * 20)) + report = module.build_report(self.root, "launch", 1, self.project) + entries = report["app_application_discovery_observations"] + self.assertEqual(len(entries), 16) + self.assertEqual(entries[0]["mapped_apps"], 1) + self.assertEqual(entries[0]["observed_apps"], "UNKNOWN") + self.assertEqual(entries[0]["instances"], 10000) + self.assertEqual(entries[0]["listener_state"], "PRESENT") + self.assertNotIn("canary", json.dumps(report)) + for state in ("MISSING", "PRESENT", "UNSUPPORTED", "ERROR"): + log.write_text(payload.replace("PRESENT mapped", state + " mapped") + .replace("submission_user_query", "auth_account_query")) + self.assertEqual(module.app_application_discovery_observations(self.root)[0]["listener_state"], state) + log.write_text("x" * module.LIMIT + payload) + self.assertEqual(module.app_application_discovery_observations(self.root), []) + log.write_text("x" * module.LIMIT + "\n" + payload) + self.assertEqual(len(module.app_application_discovery_observations(self.root)), 1) + log.unlink() + self.assertEqual(module.app_application_discovery_observations(self.root), []) + target = self.root / "target" + target.write_text(payload) + log.symlink_to(target) + self.assertEqual(module.app_application_discovery_observations(self.root), []) + log.unlink() + (self.root / "logs").rmdir() + (self.root / "logs").symlink_to(self.root) + (self.root / "backend-app.out.log").write_text(payload) + self.assertEqual(module.app_application_discovery_observations(self.root), []) + + def test_admin_success_and_503_payload_only_allowlisted_fields(self): + for status in ("UP", "DOWN"): + self.body.write_text(json.dumps({"status": status, "message": "secret-canary", + "url": "http://secret-canary", "components": {"db": "UP", "redis": "UP", "storage": "UP", + "problemOwnerRpc": status, "secret-canary": "secret-canary"}})) + observation = self.observe() + self.assertEqual(observation, {"status": status, "components": { + "db": "UP", "redis": "UP", "storage": "UP", "problemOwnerRpc": status}}) + report = module.build_report(self.root, "launch", 1, self.project) + self.assertFalse(report["passed"]) + self.assertNotIn("canary", json.dumps(report)) + self.assertEqual(report["admin_post_launch_readiness"], observation) + self.assertIn("not-same-timestamp", report["admin_post_launch_readiness_scope"]) + + def test_admin_missing_malformed_oversize_aliases_and_invalid_context(self): + unknown = self.observe() + self.assertEqual(set(unknown), {"status", "components"}) + for value in ("{", "[]", "null", "x" * (module.ADMIN_LIMIT + 1), + '{"status":"secret-canary","components":{"db":true,"redis":"up","storage":[],"problemOwnerRpc":{}}}', + '{"status":"READY","components":{"database":"UP","problem_owner_rpc":"UP"}}'): + self.body.write_text(value) + self.assertEqual(self.observe(), unknown) + self.body.write_text('{"status":"UP","components":{"db":"UP"}}') + self.assertEqual(self.observe("environment"), unknown) + self.assertEqual(self.observe(project="secret-canary"), unknown) + with mock.patch.dict(os.environ, {"ACCEPTANCE_DISPOSABLE": "false"}): + self.assertEqual(self.observe(), unknown) + os.utime(self.body, (1, 1)) + self.assertEqual(self.observe(), unknown) + + def test_admin_refuses_symlinks_directories_and_fifos(self): + unknown = self.observe() + target = self.root / "target" + target.write_text('{"status":"UP"}') + self.body.symlink_to(target) + self.assertEqual(self.observe(), unknown) + self.body.unlink() + self.body.mkdir() + self.assertEqual(self.observe(), unknown) + self.body.rmdir() + os.mkfifo(self.body) + self.assertEqual(self.observe(), unknown) + self.body.unlink() + self.body.parent.rmdir() + self.body.parent.symlink_to(self.root) + (self.root / module.ADMIN_FILE).write_text('{"status":"UP"}') + self.assertEqual(self.observe(), unknown) + + def test_capture_is_once_bounded_credentials_free_and_preserves_body(self): + with mock.patch.object(module.subprocess, "run") as call: + module.capture_admin_readiness(self.root, "launch", self.project) + call.assert_not_called() + self.launched() + for status in ("UP", "DOWN"): + def run(command, **kwargs): + self.assertEqual(command[:2], ["curl", "-q"]) + self.assertIn("--noproxy", command) + self.assertIn("--max-filesize", command) + self.assertEqual(command[command.index("--max-time") + 1], "3") + self.assertEqual(command[-1], "http://127.0.0.1:9102/api/v1/admin/health/ready") + for forbidden in ("--fail", "--location", "--retry", "--netrc", "--user"): + self.assertNotIn(forbidden, command) + kwargs["stdout"].write(json.dumps({"status": status}).encode()) + return subprocess.CompletedProcess(command, 0) + with mock.patch.object(module.subprocess, "run", side_effect=run) as call: + module.capture_admin_readiness(self.root, "launch", self.project) + self.assertEqual(call.call_count, 1) + self.assertEqual(self.observe()["status"], status) + self.assertEqual(self.body.stat().st_mode & 0o777, 0o600) + with mock.patch.object(module.subprocess, "run") as call: + module.capture_admin_readiness(self.root, "environment", self.project) + call.assert_not_called() + script = (ROOT / "scripts/ci/business-acceptance.sh").read_text() + self.assertEqual(script.count("--capture-admin-readiness"), 1) + self.assertLess(script.index("--capture-admin-readiness"), script.index(" python3 scripts/ci/acceptance_report.py\n")) + self.assertIn('if [[ "$launch_started" == true ]]', script) + self.assertIn("launch_started=false", script) + + def test_capture_failure_does_not_publish_partial_body(self): + self.launched() + with mock.patch.object(module.subprocess, "run", return_value=subprocess.CompletedProcess([], 28)): + module.capture_admin_readiness(self.root, "launch", self.project) + self.assertFalse(self.body.exists()) + with mock.patch.object(module.subprocess, "run", side_effect=subprocess.TimeoutExpired("curl", 4)): + with self.assertRaises(subprocess.TimeoutExpired): + module.capture_admin_readiness(self.root, "launch", self.project) + self.assertEqual([p.name for p in self.body.parent.iterdir()], ["launch-readiness.json"]) + if __name__ == "__main__": unittest.main() diff --git a/scripts/test/test_pm2_startup_order.py b/scripts/test/test_pm2_startup_order.py new file mode 100644 index 0000000000..b4aaaaed3d --- /dev/null +++ b/scripts/test/test_pm2_startup_order.py @@ -0,0 +1,209 @@ +#!/usr/bin/env python3 +"""Execute the launcher's PM2 stage with fake processes and HTTP readiness.""" +import subprocess +import tempfile +import unittest +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[2] +SOURCE = (ROOT / 'scripts/dev/up.sh').read_text() +STARTUP = SOURCE.split('# ===== 步骤 6: PM2 服务 =====', 1)[1].split('# ===== 步骤 7:', 1)[0] +BOOTSTRAP = SOURCE.split('# ===== 步骤 4:', 1)[1].split('# ===== 步骤 5:', 1)[0] +BOOTSTRAP = '# ===== 步骤 4:' + BOOTSTRAP +CHECK = SOURCE.split('check_pm2_online() {', 1)[1].split('\nfor _ in ', 1)[0] +CHECK = 'check_pm2_online() {' + CHECK + +SETUP = r'''set -euo pipefail +ROOT_DIR="$1"; PM2_APPS="$2"; DEV_SCOPE="$3"; FAILED="$4"; FRESH="$5" +AUTH_FAILED="$7"; AUTH_STARTED_FOR_BOOTSTRAP="$8" +source "$6/scripts/dev/devstack-manifest.sh" +DEVSTACK_SERVICE_READINESS_ATTEMPTS=2 +DEVSTACK_READINESS_INTERVAL_SECONDS=0 +pm2() { + echo "pm2 $*" >> "$ROOT_DIR/events" + if [[ "$1" == startOrRestart ]]; then + local app owner offset size + IFS=',' read -ra apps <<< "$4" + for app in "${apps[@]}"; do + case "$app" in + ulticode-submission|ulticode-judge) + owner="${app#ulticode-}" + offset="$(cat "$ROOT_DIR/logs/.$owner-ready-offset")" + size=0 + if [[ -f "$ROOT_DIR/logs/backend-$owner.out.log" ]]; then + size="$(wc -c < "$ROOT_DIR/logs/backend-$owner.out.log")" + fi + [[ "$offset" -eq "$size" ]] || return 42 + echo "offset $owner fresh" >> "$ROOT_DIR/events" + if [[ "$FRESH" == true ]]; then + devstack_readiness_banner "$app" >> "$ROOT_DIR/logs/backend-$owner.out.log" + fi + ;; + esac + done + fi +} +curl() { + if [[ "$*" == *'9101/api/v1/auth/health/ready'* ]]; then + echo auth_probe >> "$ROOT_DIR/events" + if [[ "$AUTH_FAILED" == true ]]; then printf 503; else printf 200; fi + return 0 + fi + echo probe >> "$ROOT_DIR/events" + # Simulate banners emitted by old auto-restarting consumers while App starts. + mkdir -p "$ROOT_DIR/logs" + for app in ulticode-submission ulticode-judge; do + devstack_readiness_banner "$app" >> "$ROOT_DIR/logs/backend-${app#ulticode-}.out.log" + done + if [[ "$FAILED" == true ]]; then printf 503; else printf 200; fi +} +sleep() { :; } +pm2_load_records() { :; } +pm2_record_is_online() { return 0; } +''' + + +class StartupOrderTests(unittest.TestCase): + def run_stage(self, apps, scope='admin', failed=False, fresh=True, old_logs=True, + auth_failed=False, bootstrapped=False, run_bootstrap=False, skip_bootstrap=False): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + (root / 'services').mkdir() + if old_logs: + (root / 'logs').mkdir() + for owner, banner in [('submission', 'Started BackendSubmissionApplication'), + ('judge', 'Started BackendJudgeApplication')]: + (root / f'logs/backend-{owner}.out.log').write_text(banner + '\n') + (root / f'logs/.{owner}-ready-offset').write_text('0\n') + checks = r''' +for app in ${PM2_APPS//,/ }; do + case "$app" in + ulticode-submission|ulticode-judge) + if check_pm2_online "$app"; then + echo "ready $app" >> "$ROOT_DIR/events" + else + echo "not_ready $app" >> "$ROOT_DIR/events" + fi ;; + esac +done +''' + bootstrap_setup = r''' +DEV_SEED_USERS_ENABLED=true +DEVSTACK_DUBBO_REGISTRATION_DELAY_SECONDS=0 +DEVSTACK_BOOTSTRAP_KILL_AFTER_SECONDS=1 +DEVSTACK_BOOTSTRAP_TIMEOUT_SECONDS=1 +for key in INTERNAL_DELEGATION_PRIVATE_KEY INTERNAL_DELEGATION_KEY_ID \ + BOOTSTRAP_DELEGATION_PRIVATE_KEY BOOTSTRAP_DELEGATION_KEY_ID \ + AUTH_NACOS_USERNAME AUTH_NACOS_PASSWORD DEV_SEED_ADMIN_USERNAME \ + DEV_SEED_ADMIN_EMAIL DEV_SEED_ADMIN_PASSWORD DEV_SEED_ADMIN_ROLE; do + printf -v "$key" '%s' test-only +done +timeout() { :; } +''' + bootstrap = ('SKIP_BOOTSTRAP=' + str(skip_bootstrap).lower() + '\n' + + bootstrap_setup + BOOTSTRAP) if run_bootstrap else '' + run = subprocess.run(['bash', '-c', SETUP + bootstrap + STARTUP + CHECK + checks, + 'test', directory, apps, scope, str(failed).lower(), + str(fresh).lower(), str(ROOT), str(auth_failed).lower(), + str(bootstrapped).lower()], capture_output=True, text=True) + events = (root / 'events').read_text().splitlines() + return run, events + + def starts(self, events): + return [event.split('--only ', 1)[1].split(' ', 1)[0] + for event in events if event.startswith('pm2 startOrRestart')] + + def test_app_submission_without_judge(self): + run, events = self.run_stage('ulticode-auth,ulticode-app,ulticode-submission') + self.assertEqual(run.returncode, 0, run.stderr) + self.assertEqual(self.starts(events), ['ulticode-auth', 'ulticode-app', 'ulticode-submission']) + self.assertLess(events.index('auth_probe'), events.index('pm2 startOrRestart ecosystem.config.cjs --only ulticode-app --update-env')) + self.assertLess(events.index('probe'), events.index('offset submission fresh')) + self.assertIn('ready ulticode-submission', events) + self.assertFalse(any('delete' in event for event in events)) + + def test_full_stack_and_selection_order(self): + for apps in ('ulticode-auth,ulticode-app,ulticode-submission,ulticode-judge,ulticode-search,ulticode-9002', + 'ulticode-judge,ulticode-submission,ulticode-app'): + with self.subTest(apps=apps): + run, events = self.run_stage(apps, scope='full-stack') + self.assertEqual(run.returncode, 0, run.stderr) + selected = apps.split(',') + consumers = [app for app in selected if app in ('ulticode-submission', 'ulticode-judge')] + auth = ['ulticode-auth'] if 'ulticode-auth' in selected else [] + self.assertEqual(self.starts(events), auth + [','.join(app for app in selected if app not in consumers + auth)] + consumers) + self.assertEqual(sum(event == 'probe' for event in events), 1) + self.assertIn('pm2 save', events) + self.assertIn('pm2 delete ulticode-admin', events) + for app in selected: + self.assertNotIn('pm2 delete ' + app, events) + + def test_failed_app_does_not_start_submission_or_judge(self): + for apps in ('ulticode-app,ulticode-submission', 'ulticode-app,ulticode-submission,ulticode-judge'): + with self.subTest(apps=apps): + run, events = self.run_stage(apps, failed=True) + self.assertEqual(run.returncode, 1) + self.assertEqual(self.starts(events), ['ulticode-app']) + self.assertEqual(events.count('probe'), 2) + self.assertNotIn('pm2 save', events) + self.assertIn('Fix App readiness', run.stderr) + + def test_auth_bootstrap_process_is_reused_in_both_launch_branches(self): + for apps, expected in ( + ('ulticode-auth', ['ulticode-auth']), + ('ulticode-auth,ulticode-app', ['ulticode-auth', 'ulticode-app']), + ('ulticode-auth,ulticode-app,ulticode-submission,ulticode-judge', + ['ulticode-auth', 'ulticode-app', 'ulticode-submission', 'ulticode-judge']), + ): + with self.subTest(apps=apps): + run, events = self.run_stage(apps, run_bootstrap=True) + self.assertEqual(run.returncode, 0, run.stderr) + self.assertEqual(self.starts(events), expected) + self.assertIn('pm2 save', events) + + def test_auth_timeout_does_not_start_app_or_deferred_consumers(self): + for reused in (False, True): + with self.subTest(reused=reused): + run, events = self.run_stage('ulticode-auth,ulticode-app,ulticode-submission,ulticode-judge', + auth_failed=True, bootstrapped=reused) + self.assertEqual(run.returncode, 1) + self.assertEqual(self.starts(events), [] if reused else ['ulticode-auth']) + self.assertEqual(events.count('auth_probe'), 2) + self.assertNotIn('probe', events) + self.assertNotIn('pm2 save', events) + + def test_ambient_reuse_flag_cannot_skip_start_when_bootstrap_is_disabled(self): + run, events = self.run_stage('ulticode-auth,ulticode-app', run_bootstrap=True, + skip_bootstrap=True, bootstrapped=True) + self.assertEqual(run.returncode, 0, run.stderr) + self.assertEqual(self.starts(events), ['ulticode-auth', 'ulticode-app']) + + def test_judge_only_dependency_failure_retains_legacy_fallback(self): + run, events = self.run_stage('ulticode-app,ulticode-judge', failed=True) + self.assertEqual(run.returncode, 0, run.stderr) + self.assertEqual(self.starts(events), ['ulticode-app', 'ulticode-judge']) + self.assertIn('Dubbo auto-reconnect', run.stderr) + + def test_partial_selections_without_app_do_not_add_dependency(self): + for apps in ('ulticode-submission', 'ulticode-judge', 'ulticode-submission,ulticode-judge', + 'ulticode-core,ulticode-judge', 'ulticode-9002', 'ulticode-app'): + with self.subTest(apps=apps): + run, events = self.run_stage(apps, old_logs=False) + self.assertEqual(run.returncode, 0, run.stderr) + self.assertEqual(self.starts(events), [apps]) + self.assertNotIn('probe', events) + self.assertFalse(any('delete' in event for event in events)) + + def test_stale_banners_including_during_app_wait_do_not_pass_readiness(self): + for apps in ('ulticode-app,ulticode-submission,ulticode-judge', + 'ulticode-submission,ulticode-judge'): + with self.subTest(apps=apps): + run, events = self.run_stage(apps, fresh=False) + self.assertEqual(run.returncode, 0, run.stderr) + for owner in ('submission', 'judge'): + self.assertIn(f'not_ready ulticode-{owner}', events) + self.assertNotIn(f'ready ulticode-{owner}', events) + + +if __name__ == '__main__': + unittest.main() diff --git a/services/admin/pom.xml b/services/admin/pom.xml index 532bdde2b2..41285429d5 100644 --- a/services/admin/pom.xml +++ b/services/admin/pom.xml @@ -13,7 +13,7 @@ - 1.0.5 + 1.0.6 backend-admin ${service.version.admin} diff --git a/services/admin/src/main/java/com/ulticode/modules/admin/port/adapter/AdminDubboReferenceRegistry.java b/services/admin/src/main/java/com/ulticode/modules/admin/port/adapter/AdminDubboReferenceRegistry.java index 0c92cab512..bffb5b7519 100644 --- a/services/admin/src/main/java/com/ulticode/modules/admin/port/adapter/AdminDubboReferenceRegistry.java +++ b/services/admin/src/main/java/com/ulticode/modules/admin/port/adapter/AdminDubboReferenceRegistry.java @@ -74,43 +74,43 @@ public class AdminDubboReferenceRegistry { timeout = RpcPolicy.QUERY_TIMEOUT_MS, retries = RpcPolicy.QUERY_RETRIES, check = false) private ReconciliationQueryService reconciliationQueryReference; - @DubboReference(group = "backend-app", version = "1.0.0", + @DubboReference(group = "backend-app", providedBy = "${ulticode.rpc.providers.app:backend-app}", version = "1.0.0", timeout = RpcPolicy.QUERY_TIMEOUT_MS, retries = RpcPolicy.QUERY_RETRIES, check = false) private ContestAdminReadPort contestAdminReadReference; - @DubboReference(group = "backend-app", version = "1.0.0", + @DubboReference(group = "backend-app", providedBy = "${ulticode.rpc.providers.app:backend-app}", version = "1.0.0", timeout = RpcPolicy.QUERY_TIMEOUT_MS, retries = RpcPolicy.QUERY_RETRIES, check = false) private DashboardAdminReadPort dashboardAdminReadReference; - @DubboReference(group = "backend-app", version = "1.0.0", + @DubboReference(group = "backend-app", providedBy = "${ulticode.rpc.providers.app:backend-app}", version = "1.0.0", timeout = RpcPolicy.QUERY_TIMEOUT_MS, retries = RpcPolicy.QUERY_RETRIES, check = false) private ContestAnnouncementReadPort contestAnnouncementReadReference; - @DubboReference(group = "backend-app", version = "1.0.0", + @DubboReference(group = "backend-app", providedBy = "${ulticode.rpc.providers.app:backend-app}", version = "1.0.0", timeout = RpcPolicy.QUERY_TIMEOUT_MS, retries = RpcPolicy.QUERY_RETRIES, check = false) private ContestLiveRankingReadPort contestLiveRankingReadReference; - @DubboReference(group = "backend-app", version = "1.0.0", + @DubboReference(group = "backend-app", providedBy = "${ulticode.rpc.providers.app:backend-app}", version = "1.0.0", timeout = RpcPolicy.QUERY_TIMEOUT_MS, retries = RpcPolicy.QUERY_RETRIES, check = false) private ContestParticipantReadPort contestParticipantReadReference; - @DubboReference(group = "backend-app", version = "1.0.0", + @DubboReference(group = "backend-app", providedBy = "${ulticode.rpc.providers.app:backend-app}", version = "1.0.0", timeout = RpcPolicy.WRITE_TIMEOUT_MS, retries = RpcPolicy.WRITE_RETRIES, check = false) private ForumCommentAdministrationService forumCommentAdministrationReference; - @DubboReference(group = "backend-app", version = "1.0.0", + @DubboReference(group = "backend-app", providedBy = "${ulticode.rpc.providers.app:backend-app}", version = "1.0.0", timeout = RpcPolicy.QUERY_TIMEOUT_MS, retries = RpcPolicy.QUERY_RETRIES, check = false) private ForumCommentReadPort forumCommentReadReference; - @DubboReference(group = "backend-app", version = "1.0.0", + @DubboReference(group = "backend-app", providedBy = "${ulticode.rpc.providers.app:backend-app}", version = "1.0.0", timeout = RpcPolicy.WRITE_TIMEOUT_MS, retries = RpcPolicy.WRITE_RETRIES, check = false) private ForumPostAdministrationService forumPostAdministrationReference; - @DubboReference(group = "backend-app", version = "1.0.0", + @DubboReference(group = "backend-app", providedBy = "${ulticode.rpc.providers.app:backend-app}", version = "1.0.0", timeout = RpcPolicy.WRITE_TIMEOUT_MS, retries = RpcPolicy.WRITE_RETRIES, check = false) private ForumTagAdministrationService forumTagAdministrationReference; - @DubboReference(group = "backend-app", version = "1.0.0", + @DubboReference(group = "backend-app", providedBy = "${ulticode.rpc.providers.app:backend-app}", version = "1.0.0", timeout = RpcPolicy.QUERY_TIMEOUT_MS, retries = RpcPolicy.QUERY_RETRIES, check = false) private ForumTagReadPort forumTagReadReference; @@ -119,75 +119,75 @@ public class AdminDubboReferenceRegistry { timeout = RpcPolicy.QUERY_TIMEOUT_MS, retries = RpcPolicy.QUERY_RETRIES, check = false) private NotificationAdminReadPort notificationAdminReadReference; - @DubboReference(group = "backend-app", version = "1.0.0", + @DubboReference(group = "backend-app", providedBy = "${ulticode.rpc.providers.app:backend-app}", version = "1.0.0", timeout = RpcPolicy.QUERY_TIMEOUT_MS, retries = RpcPolicy.QUERY_RETRIES, check = false) private ProblemAdminReadPort problemAdminReadReference; - @DubboReference(group = "backend-app", version = "1.0.0", + @DubboReference(group = "backend-app", providedBy = "${ulticode.rpc.providers.app:backend-app}", version = "1.0.0", timeout = RpcPolicy.QUERY_TIMEOUT_MS, retries = RpcPolicy.QUERY_RETRIES, check = false) private ProblemAnalyticsReadPort problemAnalyticsReadReference; - @DubboReference(group = "backend-app", version = "1.0.0", + @DubboReference(group = "backend-app", providedBy = "${ulticode.rpc.providers.app:backend-app}", version = "1.0.0", timeout = RpcPolicy.WRITE_TIMEOUT_MS, retries = RpcPolicy.WRITE_RETRIES, check = false) private ProblemListAdministrationService problemListAdministrationReference; - @DubboReference(group = "backend-app", version = "1.0.0", + @DubboReference(group = "backend-app", providedBy = "${ulticode.rpc.providers.app:backend-app}", version = "1.0.0", timeout = RpcPolicy.QUERY_TIMEOUT_MS, retries = RpcPolicy.QUERY_RETRIES, check = false) private ProblemListChainReadPort problemListChainReadReference; - @DubboReference(group = "backend-app", version = "1.0.0", + @DubboReference(group = "backend-app", providedBy = "${ulticode.rpc.providers.app:backend-app}", version = "1.0.0", timeout = RpcPolicy.QUERY_TIMEOUT_MS, retries = RpcPolicy.QUERY_RETRIES, check = false) private ProblemListSearchReadPort problemListSearchReadReference; - @DubboReference(group = "backend-app", version = "1.0.0", + @DubboReference(group = "backend-app", providedBy = "${ulticode.rpc.providers.app:backend-app}", version = "1.0.0", timeout = RpcPolicy.WRITE_TIMEOUT_MS, retries = RpcPolicy.WRITE_RETRIES, check = false) private ProblemOwnerPort problemOwnerReference; - @DubboReference(group = "backend-app", version = "1.0.0", + @DubboReference(group = "backend-app", providedBy = "${ulticode.rpc.providers.app:backend-app}", version = "1.0.0", timeout = RpcPolicy.WRITE_TIMEOUT_MS, retries = RpcPolicy.WRITE_RETRIES, check = false) private ProblemTagOwnerPort problemTagOwnerReference; - @DubboReference(group = "backend-app", version = "1.0.0", + @DubboReference(group = "backend-app", providedBy = "${ulticode.rpc.providers.app:backend-app}", version = "1.0.0", timeout = RpcPolicy.QUERY_TIMEOUT_MS, retries = RpcPolicy.QUERY_RETRIES, check = false) private SolutionAdminReadPort solutionAdminReadReference; - @DubboReference(group = "backend-app", version = "1.0.0", + @DubboReference(group = "backend-app", providedBy = "${ulticode.rpc.providers.app:backend-app}", version = "1.0.0", timeout = RpcPolicy.WRITE_TIMEOUT_MS, retries = RpcPolicy.WRITE_RETRIES, check = false) private SolutionCommentOwnerPort solutionCommentOwnerReference; - @DubboReference(group = "backend-app", version = "1.0.0", + @DubboReference(group = "backend-app", providedBy = "${ulticode.rpc.providers.app:backend-app}", version = "1.0.0", timeout = RpcPolicy.QUERY_TIMEOUT_MS, retries = RpcPolicy.QUERY_RETRIES, check = false) private SolutionCommentReadPort solutionCommentReadReference; - @DubboReference(group = "backend-app", version = "1.0.0", + @DubboReference(group = "backend-app", providedBy = "${ulticode.rpc.providers.app:backend-app}", version = "1.0.0", timeout = RpcPolicy.WRITE_TIMEOUT_MS, retries = RpcPolicy.WRITE_RETRIES, check = false) private SolutionOwnerPort solutionOwnerReference; - @DubboReference(group = "backend-app", version = "1.0.0", + @DubboReference(group = "backend-app", providedBy = "${ulticode.rpc.providers.app:backend-app}", version = "1.0.0", timeout = RpcPolicy.QUERY_TIMEOUT_MS, retries = RpcPolicy.QUERY_RETRIES, check = false) private SolutionReadPort solutionReadReference; - @DubboReference(group = "backend-submission", version = "1.0.0", + @DubboReference(group = "backend-submission", providedBy = "${ulticode.rpc.providers.submission:backend-submission}", version = "1.0.0", timeout = RpcPolicy.QUERY_TIMEOUT_MS, retries = RpcPolicy.QUERY_RETRIES, check = false) private SubmissionAdminReadPort submissionAdminReadReference; - @DubboReference(group = "backend-submission", version = "1.0.0", + @DubboReference(group = "backend-submission", providedBy = "${ulticode.rpc.providers.submission:backend-submission}", version = "1.0.0", timeout = RpcPolicy.QUERY_TIMEOUT_MS, retries = RpcPolicy.QUERY_RETRIES, check = false) private SubmissionStreakPort submissionStreakReference; - @DubboReference(group = "backend-submission", version = "1.1.0", + @DubboReference(group = "backend-submission", providedBy = "${ulticode.rpc.providers.submission:backend-submission}", version = "1.1.0", timeout = RpcPolicy.QUERY_TIMEOUT_MS, retries = RpcPolicy.QUERY_RETRIES, check = false) private SubmissionUserStatsPort submissionUserStatsReference; - @DubboReference(group = "backend-app", version = "1.0.0", + @DubboReference(group = "backend-app", providedBy = "${ulticode.rpc.providers.app:backend-app}", version = "1.0.0", timeout = RpcPolicy.QUERY_TIMEOUT_MS, retries = RpcPolicy.QUERY_RETRIES, check = false) private SubscriptionReadPort subscriptionReadReference; - @DubboReference(group = "backend-app", version = "1.0.0", + @DubboReference(group = "backend-app", providedBy = "${ulticode.rpc.providers.app:backend-app}", version = "1.0.0", timeout = RpcPolicy.WRITE_TIMEOUT_MS, retries = RpcPolicy.WRITE_RETRIES, check = false) private TestCaseOwnerPort testCaseOwnerReference; - @DubboReference(group = "backend-app", version = "1.0.0", + @DubboReference(group = "backend-app", providedBy = "${ulticode.rpc.providers.app:backend-app}", version = "1.0.0", timeout = RpcPolicy.QUERY_TIMEOUT_MS, retries = RpcPolicy.QUERY_RETRIES, check = false) private UserProfileQueryService userProfileQueryReference; diff --git a/services/admin/src/main/java/com/ulticode/modules/admin/port/adapter/AdminUserProfileAdapter.java b/services/admin/src/main/java/com/ulticode/modules/admin/port/adapter/AdminUserProfileAdapter.java index 33c0b733c2..b4a3f25e81 100644 --- a/services/admin/src/main/java/com/ulticode/modules/admin/port/adapter/AdminUserProfileAdapter.java +++ b/services/admin/src/main/java/com/ulticode/modules/admin/port/adapter/AdminUserProfileAdapter.java @@ -54,7 +54,7 @@ public class AdminUserProfileAdapter implements UserProfilePort { @Autowired(required = false) - @DubboReference(group = "backend-app", version = "1.0.0", + @DubboReference(group = "backend-app", providedBy = "${ulticode.rpc.providers.app:backend-app}", version = "1.0.0", timeout = RpcPolicy.WRITE_TIMEOUT_MS, retries = RpcPolicy.WRITE_RETRIES, check = false) private ProfileWriteService profileWriteService; diff --git a/services/admin/src/main/java/com/ulticode/modules/admin/port/adapter/DubboAdminForumReadAdapter.java b/services/admin/src/main/java/com/ulticode/modules/admin/port/adapter/DubboAdminForumReadAdapter.java index c1e8a6d85b..dc367b266e 100644 --- a/services/admin/src/main/java/com/ulticode/modules/admin/port/adapter/DubboAdminForumReadAdapter.java +++ b/services/admin/src/main/java/com/ulticode/modules/admin/port/adapter/DubboAdminForumReadAdapter.java @@ -31,11 +31,11 @@ @Component public class DubboAdminForumReadAdapter implements AdminForumReadPort { - @DubboReference(group = "backend-app", version = "1.0.0", + @DubboReference(group = "backend-app", providedBy = "${ulticode.rpc.providers.app:backend-app}", version = "1.0.0", timeout = RpcPolicy.QUERY_TIMEOUT_MS, retries = RpcPolicy.QUERY_RETRIES, check = false) private AdminForumReadPort forumReadPort; - @DubboReference(group = "backend-app", version = "1.0.0", + @DubboReference(group = "backend-app", providedBy = "${ulticode.rpc.providers.app:backend-app}", version = "1.0.0", timeout = RpcPolicy.QUERY_TIMEOUT_MS, retries = RpcPolicy.QUERY_RETRIES, check = false) private ForumPostVoteCountReadPort voteCountReadPort; diff --git a/services/admin/src/main/java/com/ulticode/modules/admin/port/adapter/DubboSubmissionActivityAnalyticsAdapter.java b/services/admin/src/main/java/com/ulticode/modules/admin/port/adapter/DubboSubmissionActivityAnalyticsAdapter.java index c131873ba9..3b340f479f 100644 --- a/services/admin/src/main/java/com/ulticode/modules/admin/port/adapter/DubboSubmissionActivityAnalyticsAdapter.java +++ b/services/admin/src/main/java/com/ulticode/modules/admin/port/adapter/DubboSubmissionActivityAnalyticsAdapter.java @@ -20,7 +20,7 @@ @Component public class DubboSubmissionActivityAnalyticsAdapter implements SubmissionActivityAnalyticsPort { - @DubboReference(group = "backend-submission", version = "1.0.0", + @DubboReference(group = "backend-submission", providedBy = "${ulticode.rpc.providers.submission:backend-submission}", version = "1.0.0", timeout = RpcPolicy.QUERY_TIMEOUT_MS, retries = RpcPolicy.QUERY_RETRIES, check = false) private SubmissionActivityAnalyticsPort submissionActivityAnalyticsPort; diff --git a/services/admin/src/main/java/com/ulticode/modules/admin/port/adapter/SubmissionUserDetailStatsReadAdapter.java b/services/admin/src/main/java/com/ulticode/modules/admin/port/adapter/SubmissionUserDetailStatsReadAdapter.java index eba22b2886..47b6bf8c93 100644 --- a/services/admin/src/main/java/com/ulticode/modules/admin/port/adapter/SubmissionUserDetailStatsReadAdapter.java +++ b/services/admin/src/main/java/com/ulticode/modules/admin/port/adapter/SubmissionUserDetailStatsReadAdapter.java @@ -16,7 +16,7 @@ public class SubmissionUserDetailStatsReadAdapter implements AdminSubmissionUserDetailStatsReadPort { - @DubboReference(group = "backend-submission", version = "1.0.0", + @DubboReference(group = "backend-submission", providedBy = "${ulticode.rpc.providers.submission:backend-submission}", version = "1.0.0", timeout = RpcPolicy.QUERY_TIMEOUT_MS, retries = RpcPolicy.QUERY_RETRIES, check = false) private SubmissionUserDetailStatsPort submissionUserDetailStatsPort; diff --git a/services/admin/src/main/java/com/ulticode/modules/admin/service/ContentModerationCutoverService.java b/services/admin/src/main/java/com/ulticode/modules/admin/service/ContentModerationCutoverService.java index aeff885cc4..da2aebfe8f 100644 --- a/services/admin/src/main/java/com/ulticode/modules/admin/service/ContentModerationCutoverService.java +++ b/services/admin/src/main/java/com/ulticode/modules/admin/service/ContentModerationCutoverService.java @@ -53,7 +53,7 @@ public ContentModerationCutoverService( this.cutoverGate = cutoverGate; } - @DubboReference(group = "backend-app", version = "1.0.0", + @DubboReference(group = "backend-app", providedBy = "${ulticode.rpc.providers.app:backend-app}", version = "1.0.0", timeout = RpcPolicy.WRITE_TIMEOUT_MS, retries = RpcPolicy.WRITE_RETRIES, check = false) private ContentModerationService dubboProvider; diff --git a/services/admin/src/main/java/com/ulticode/modules/admin/service/ContestCutoverService.java b/services/admin/src/main/java/com/ulticode/modules/admin/service/ContestCutoverService.java index 40e878daa9..006cdb9f44 100644 --- a/services/admin/src/main/java/com/ulticode/modules/admin/service/ContestCutoverService.java +++ b/services/admin/src/main/java/com/ulticode/modules/admin/service/ContestCutoverService.java @@ -49,7 +49,7 @@ public ContestCutoverService( this.cutoverGate = cutoverGate; } - @DubboReference(group = "backend-app", version = "1.0.0", + @DubboReference(group = "backend-app", providedBy = "${ulticode.rpc.providers.app:backend-app}", version = "1.0.0", timeout = RpcPolicy.WRITE_TIMEOUT_MS, retries = RpcPolicy.WRITE_RETRIES, check = false) private ContestAdministrationService dubboProvider; diff --git a/services/admin/src/main/java/com/ulticode/modules/admin/service/ProblemCutoverService.java b/services/admin/src/main/java/com/ulticode/modules/admin/service/ProblemCutoverService.java index d5386cfb09..290cdbb040 100644 --- a/services/admin/src/main/java/com/ulticode/modules/admin/service/ProblemCutoverService.java +++ b/services/admin/src/main/java/com/ulticode/modules/admin/service/ProblemCutoverService.java @@ -50,7 +50,7 @@ public class ProblemCutoverService { private final AdminProblemMapper mapper; private final CurrentUserProvider currentUserProvider; - @DubboReference(group = "backend-app", version = "1.0.0", + @DubboReference(group = "backend-app", providedBy = "${ulticode.rpc.providers.app:backend-app}", version = "1.0.0", timeout = RpcPolicy.WRITE_TIMEOUT_MS, retries = RpcPolicy.WRITE_RETRIES, check = false) private ProblemAdministrationService dubboProvider; diff --git a/services/admin/src/main/java/com/ulticode/modules/admin/service/SubmissionCutoverService.java b/services/admin/src/main/java/com/ulticode/modules/admin/service/SubmissionCutoverService.java index 330e259e62..9a72f1eadd 100644 --- a/services/admin/src/main/java/com/ulticode/modules/admin/service/SubmissionCutoverService.java +++ b/services/admin/src/main/java/com/ulticode/modules/admin/service/SubmissionCutoverService.java @@ -32,7 +32,7 @@ public class SubmissionCutoverService { private final CurrentUserProvider currentUserProvider; - @DubboReference(group = "backend-submission", version = "1.0.0", + @DubboReference(group = "backend-submission", providedBy = "${ulticode.rpc.providers.submission:backend-submission}", version = "1.0.0", timeout = RpcPolicy.WRITE_TIMEOUT_MS, retries = RpcPolicy.WRITE_RETRIES, check = false) private SubmissionAdministrationService dubboProvider; diff --git a/services/admin/src/main/java/com/ulticode/modules/admin/service/impl/AdminForumServiceImpl.java b/services/admin/src/main/java/com/ulticode/modules/admin/service/impl/AdminForumServiceImpl.java index 5500a9f889..13b5d89646 100644 --- a/services/admin/src/main/java/com/ulticode/modules/admin/service/impl/AdminForumServiceImpl.java +++ b/services/admin/src/main/java/com/ulticode/modules/admin/service/impl/AdminForumServiceImpl.java @@ -72,7 +72,7 @@ public class AdminForumServiceImpl implements AdminForumService { private final AdminBulkExecutor bulkExecutor; private final AdminForumReadPort adminForumReadPort; - @DubboReference(group = "backend-app", version = "1.0.0", + @DubboReference(group = "backend-app", providedBy = "${ulticode.rpc.providers.app:backend-app}", version = "1.0.0", timeout = RpcPolicy.WRITE_TIMEOUT_MS, retries = RpcPolicy.WRITE_RETRIES, check = false) private ContentModerationService contentModerationService; diff --git a/services/admin/src/main/java/com/ulticode/modules/reconciliation/port/adapter/DubboAppReconciliationReadAdapter.java b/services/admin/src/main/java/com/ulticode/modules/reconciliation/port/adapter/DubboAppReconciliationReadAdapter.java index 7d846520b7..cc5c41e765 100644 --- a/services/admin/src/main/java/com/ulticode/modules/reconciliation/port/adapter/DubboAppReconciliationReadAdapter.java +++ b/services/admin/src/main/java/com/ulticode/modules/reconciliation/port/adapter/DubboAppReconciliationReadAdapter.java @@ -12,7 +12,7 @@ @Component public class DubboAppReconciliationReadAdapter implements AppReconciliationReadPort { - @DubboReference(group = "backend-app", version = "1.0.0", + @DubboReference(group = "backend-app", providedBy = "${ulticode.rpc.providers.app:backend-app}", version = "1.0.0", timeout = RpcPolicy.QUERY_TIMEOUT_MS, retries = RpcPolicy.QUERY_RETRIES, check = false) private AppReconciliationReadPort appReconciliationReadPort; diff --git a/services/admin/src/main/java/com/ulticode/modules/reconciliation/port/adapter/DubboSubmissionReconciliationReadAdapter.java b/services/admin/src/main/java/com/ulticode/modules/reconciliation/port/adapter/DubboSubmissionReconciliationReadAdapter.java index ec4173cd96..f35d8d3cfa 100644 --- a/services/admin/src/main/java/com/ulticode/modules/reconciliation/port/adapter/DubboSubmissionReconciliationReadAdapter.java +++ b/services/admin/src/main/java/com/ulticode/modules/reconciliation/port/adapter/DubboSubmissionReconciliationReadAdapter.java @@ -13,7 +13,7 @@ @Component public class DubboSubmissionReconciliationReadAdapter implements SubmissionReconciliationReadPort { - @DubboReference(group = "backend-submission", version = "1.0.0", + @DubboReference(group = "backend-submission", providedBy = "${ulticode.rpc.providers.submission:backend-submission}", version = "1.0.0", timeout = RpcPolicy.QUERY_TIMEOUT_MS, retries = RpcPolicy.QUERY_RETRIES, check = false) private SubmissionReconciliationReadPort submissionReconciliationReadPort; diff --git a/services/admin/src/test/java/com/ulticode/admin/AdminProviderApplicationTest.java b/services/admin/src/test/java/com/ulticode/admin/AdminProviderApplicationTest.java new file mode 100644 index 0000000000..0f737deaed --- /dev/null +++ b/services/admin/src/test/java/com/ulticode/admin/AdminProviderApplicationTest.java @@ -0,0 +1,83 @@ +package com.ulticode.admin; + +import java.lang.reflect.Field; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Map; +import java.util.Set; +import org.apache.dubbo.common.URL; +import org.apache.dubbo.config.annotation.DubboReference; +import org.apache.dubbo.config.spring.util.AnnotationUtils; +import org.apache.dubbo.metadata.ServiceNameMapping; +import org.junit.jupiter.api.Test; +import org.springframework.core.env.MapPropertySource; +import org.springframework.core.env.StandardEnvironment; +import org.springframework.core.env.SystemEnvironmentPropertySource; + +import static org.junit.jupiter.api.Assertions.*; + +class AdminProviderApplicationTest { + @Test + void everyOwnerReferenceResolvesItsExplicitApplicationWithoutMappingLookup() throws Exception { + int app = 0; + int submission = 0; + int other = 0; + Path sources = Path.of("src/main/java"); + try (var files = Files.walk(sources)) { + for (Path file : files.filter(p -> p.toString().endsWith(".java")).toList()) { + String className = sources.relativize(file).toString() + .replace('/', '.').replace('\\', '.').replaceAll("\\.java$", ""); + Class type = Class.forName(className, false, getClass().getClassLoader()); + for (Field field : type.getDeclaredFields()) { + DubboReference reference = field.getAnnotation(DubboReference.class); + if (reference == null) { + continue; + } + String owner = switch (reference.group()) { + case "backend-app" -> "app"; + case "backend-submission" -> "submission"; + default -> null; + }; + if (owner == null) { + assertArrayEquals(new String[0], reference.providedBy(), field.toString()); + other++; + continue; + } + if (owner.equals("app")) app++; else submission++; + String property = "ulticode.rpc.providers." + owner; + assertArrayEquals(new String[]{"${" + property + ":backend-" + owner + "}"}, + reference.providedBy(), field.toString()); + StandardEnvironment defaults = isolatedEnvironment(); + assertMapping(reference, defaults, "backend-" + owner); + defaults.getPropertySources().addFirst(new MapPropertySource("override", + Map.of(property, "custom-" + owner))); + assertMapping(reference, defaults, "custom-" + owner); + StandardEnvironment environmentAlias = isolatedEnvironment(); + environmentAlias.getPropertySources().addFirst(new SystemEnvironmentPropertySource("alias", + Map.of("ULTICODE_RPC_PROVIDERS_" + owner.toUpperCase(java.util.Locale.ROOT), + "env-" + owner))); + assertMapping(reference, environmentAlias, "env-" + owner); + } + } + } + assertEquals(40, app + submission, "All Admin App and Submission references must be covered"); + assertTrue(app > 0); + assertTrue(submission > 0); + assertTrue(other > 0, "Other owner groups remain untargeted"); + } + + private StandardEnvironment isolatedEnvironment() { + StandardEnvironment environment = new StandardEnvironment(); + environment.getPropertySources().remove(StandardEnvironment.SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME); + environment.getPropertySources().remove(StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME); + return environment; + } + + private void assertMapping(DubboReference reference, StandardEnvironment environment, String expected) { + Map attributes = AnnotationUtils.getAttributes(reference, environment, false); + String[] providedBy = (String[]) attributes.get("providedBy"); + assertArrayEquals(new String[]{expected}, providedBy); + URL url = new URL("consumer", "127.0.0.1", 0).addParameter("provided-by", String.join(",", providedBy)); + assertEquals(Set.of(expected), ServiceNameMapping.getMappingByUrl(url)); + } +} diff --git a/services/agent/data/repository_corpus_manifest.json b/services/agent/data/repository_corpus_manifest.json index c1a9b643ae..4e669d9c02 100644 --- a/services/agent/data/repository_corpus_manifest.json +++ b/services/agent/data/repository_corpus_manifest.json @@ -29,8 +29,8 @@ }, { "doc_id": "repository-development", - "version": "sha256-5417a89a3f26d82c4a92e2d643042659427ca9f2956bc300fdcace80b292d35c", - "chunk_id": "repository-development:sha256-5417a89a3f26d82c4a92e2d643042659427ca9f2956bc300fdcace80b292d35c:1", + "version": "sha256-55eb004505226f20e823f0b18192a463d37fdc8cd9aca5ef1c344cf4a3230069", + "chunk_id": "repository-development:sha256-55eb004505226f20e823f0b18192a463d37fdc8cd9aca5ef1c344cf4a3230069:1", "source_path": "docs/DEVELOPMENT.md", "access_scope": "repository-public", "sample_kind": "real", diff --git a/services/app/app-web/src/main/java/com/ulticode/app/adapter/in/web/AppReadinessController.java b/services/app/app-web/src/main/java/com/ulticode/app/adapter/in/web/AppReadinessController.java index a73ebac3a1..d583f1895f 100644 --- a/services/app/app-web/src/main/java/com/ulticode/app/adapter/in/web/AppReadinessController.java +++ b/services/app/app-web/src/main/java/com/ulticode/app/adapter/in/web/AppReadinessController.java @@ -2,9 +2,13 @@ import com.ulticode.common.health.ReadinessChecks; import com.ulticode.common.storage.StorageReadiness; +import com.ulticode.auth.api.service.AccountQueryService; +import com.ulticode.submission.api.service.SubmissionUserQueryPort; +import com.ulticode.rpc.resilience.DubboDiscoveryReadiness; import java.util.LinkedHashMap; import java.util.Map; import javax.sql.DataSource; +import org.apache.dubbo.config.spring.reference.ReferenceBeanManager; import org.springframework.data.redis.core.RedisCallback; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.http.HttpStatus; @@ -24,11 +28,15 @@ */ @RestController @RequestMapping("/api/v1/app") +@lombok.extern.slf4j.Slf4j public class AppReadinessController { private final DataSource dataSource; private final StringRedisTemplate redisTemplate; private final StorageReadiness storageReadiness; + private final ReferenceBeanManager references; + private final Map lastDiscovery = new LinkedHashMap<>(); + private final Map lastApplicationDiscovery = new LinkedHashMap<>(); /** * The DataSource is resolved through {@link ObjectProvider} so contexts @@ -38,11 +46,13 @@ public class AppReadinessController { */ public AppReadinessController(org.springframework.beans.factory.ObjectProvider dataSourceProvider, StringRedisTemplate redisTemplate, - org.springframework.beans.factory.ObjectProvider storageReadinessProvider) { + org.springframework.beans.factory.ObjectProvider storageReadinessProvider, + org.springframework.beans.factory.ObjectProvider referenceProvider) { this.dataSource = dataSourceProvider.getIfAvailable(); this.redisTemplate = redisTemplate; // Absent only in contexts that do not carry the shared storage module. this.storageReadiness = storageReadinessProvider.getIfAvailable(); + this.references = referenceProvider.getIfAvailable(); } @GetMapping("/health/ready") @@ -55,12 +65,46 @@ public ResponseEntity> ready() { storageReadiness.probeIfFailed(); } components.put("storage", storageReadiness == null || storageReadiness.isReady()); + observeDiscovery(); boolean allUp = components.values().stream().allMatch(Boolean::booleanValue); return ResponseEntity .status(allUp ? HttpStatus.OK : HttpStatus.SERVICE_UNAVAILABLE) .body(ReadinessChecks.report(components)); } + private synchronized void observeDiscovery() { + try { + observeInterface("submission_user_query", SubmissionUserQueryPort.class.getName()); + observeInterface("auth_account_query", AccountQueryService.class.getName()); + observeApplicationInterface("submission_user_query", SubmissionUserQueryPort.class.getName()); + observeApplicationInterface("auth_account_query", AccountQueryService.class.getName()); + } catch (RuntimeException ignored) { + // Diagnostic failures never affect HTTP readiness or invoke business RPC. + } + } + + private void observeInterface(String alias, String interfaceName) { + var observation = DubboDiscoveryReadiness.inspect(references, interfaceName); + if (observation.equals(lastDiscovery.put(alias, observation))) return; + // Two fixed aliases, metadata only, logged on change rather than on every probe. + log.info("AppDiscoveryObservation interface={} state={} invoker_kind={} directory_kind={} destroyed={} all={} valid={} reconnect={} disabled={}", + alias, observation.state(), observation.invokerKind(), observation.directoryKind(), + observation.destroyed(), observation.all().token(), observation.valid().token(), + observation.reconnect().token(), observation.disabled().token()); + } + + private void observeApplicationInterface(String alias, String interfaceName) { + var application = DubboDiscoveryReadiness.inspectApplication(references, interfaceName); + if (application.equals(lastApplicationDiscovery.put(alias, application))) return; + var observation = application.directory(); + log.info("AppApplicationDiscoveryObservation interface={} state={} invoker_kind={} directory_kind={} destroyed={} all={} valid={} reconnect={} disabled={} listener_state={} mapped_apps={} observed_apps={} instances={}", + alias, observation.state(), observation.invokerKind(), observation.directoryKind(), + observation.destroyed(), observation.all().token(), observation.valid().token(), + observation.reconnect().token(), observation.disabled().token(), application.listenerState(), + application.mappedApplications().token(), application.observedApplications().token(), + application.instances().token()); + } + private boolean redisUp() { try { String pong = redisTemplate.execute( diff --git a/services/app/app-web/src/main/java/com/ulticode/modules/contest/port/adapter/RemoteSubmissionAdjudicationReadAdapter.java b/services/app/app-web/src/main/java/com/ulticode/modules/contest/port/adapter/RemoteSubmissionAdjudicationReadAdapter.java index 612a2a8ac1..d6ccc6baf8 100644 --- a/services/app/app-web/src/main/java/com/ulticode/modules/contest/port/adapter/RemoteSubmissionAdjudicationReadAdapter.java +++ b/services/app/app-web/src/main/java/com/ulticode/modules/contest/port/adapter/RemoteSubmissionAdjudicationReadAdapter.java @@ -14,6 +14,7 @@ public class RemoteSubmissionAdjudicationReadAdapter implements SubmissionAdjudicationReadPort { @DubboReference(group = "backend-submission", version = "1.0.0", + providedBy = "${ulticode.rpc.providers.submission:backend-submission}", timeout = RpcPolicy.QUERY_TIMEOUT_MS, retries = RpcPolicy.QUERY_RETRIES, check = false) private SubmissionAdjudicationReadPort submissionAdjudicationReadPort; diff --git a/services/app/app-web/src/main/java/com/ulticode/modules/submission/port/RemoteSubmissionGenerationReadAdapter.java b/services/app/app-web/src/main/java/com/ulticode/modules/submission/port/RemoteSubmissionGenerationReadAdapter.java index b225e2e1f7..1887320b7c 100644 --- a/services/app/app-web/src/main/java/com/ulticode/modules/submission/port/RemoteSubmissionGenerationReadAdapter.java +++ b/services/app/app-web/src/main/java/com/ulticode/modules/submission/port/RemoteSubmissionGenerationReadAdapter.java @@ -12,6 +12,7 @@ public class RemoteSubmissionGenerationReadAdapter implements SubmissionGenerationReadPort { @DubboReference(group = "backend-submission", version = "1.0.0", + providedBy = "${ulticode.rpc.providers.submission:backend-submission}", timeout = RpcPolicy.QUERY_TIMEOUT_MS, retries = RpcPolicy.QUERY_RETRIES, check = false) private SubmissionGenerationReadPort submissionGenerationReadPort; diff --git a/services/app/app-web/src/main/java/com/ulticode/modules/submission/port/adapter/RemoteProblemSubmissionStatsAdapter.java b/services/app/app-web/src/main/java/com/ulticode/modules/submission/port/adapter/RemoteProblemSubmissionStatsAdapter.java index 9fc35d3179..adc784d734 100644 --- a/services/app/app-web/src/main/java/com/ulticode/modules/submission/port/adapter/RemoteProblemSubmissionStatsAdapter.java +++ b/services/app/app-web/src/main/java/com/ulticode/modules/submission/port/adapter/RemoteProblemSubmissionStatsAdapter.java @@ -18,6 +18,7 @@ public class RemoteProblemSubmissionStatsAdapter implements ProblemSubmissionStatsPort { @DubboReference(group = "backend-submission", version = "1.1.0", + providedBy = "${ulticode.rpc.providers.submission:backend-submission}", timeout = RpcPolicy.QUERY_TIMEOUT_MS, retries = RpcPolicy.QUERY_RETRIES, check = false) private ProblemSubmissionStatsPort problemSubmissionStatsPort; diff --git a/services/app/app-web/src/main/java/com/ulticode/modules/submission/port/adapter/RemoteSubmissionReadAdapter.java b/services/app/app-web/src/main/java/com/ulticode/modules/submission/port/adapter/RemoteSubmissionReadAdapter.java index 4c4375a403..d63c6ab550 100644 --- a/services/app/app-web/src/main/java/com/ulticode/modules/submission/port/adapter/RemoteSubmissionReadAdapter.java +++ b/services/app/app-web/src/main/java/com/ulticode/modules/submission/port/adapter/RemoteSubmissionReadAdapter.java @@ -16,6 +16,7 @@ public class RemoteSubmissionReadAdapter implements SubmissionReadPort { @DubboReference(group = "backend-submission", version = "1.0.0", + providedBy = "${ulticode.rpc.providers.submission:backend-submission}", timeout = RpcPolicy.QUERY_TIMEOUT_MS, retries = RpcPolicy.QUERY_RETRIES, check = false) private SubmissionReadPort submissionReadPort; diff --git a/services/app/app-web/src/main/java/com/ulticode/modules/submission/port/adapter/RemoteSubmissionStreakAdapter.java b/services/app/app-web/src/main/java/com/ulticode/modules/submission/port/adapter/RemoteSubmissionStreakAdapter.java index b70eb4001a..e652993658 100644 --- a/services/app/app-web/src/main/java/com/ulticode/modules/submission/port/adapter/RemoteSubmissionStreakAdapter.java +++ b/services/app/app-web/src/main/java/com/ulticode/modules/submission/port/adapter/RemoteSubmissionStreakAdapter.java @@ -12,6 +12,7 @@ public class RemoteSubmissionStreakAdapter implements SubmissionStreakPort { @DubboReference(group = "backend-submission", version = "1.0.0", + providedBy = "${ulticode.rpc.providers.submission:backend-submission}", timeout = RpcPolicy.QUERY_TIMEOUT_MS, retries = RpcPolicy.QUERY_RETRIES, check = false) private SubmissionStreakPort submissionStreakPort; diff --git a/services/app/app-web/src/main/java/com/ulticode/modules/submission/port/adapter/RemoteSubmissionUserQueryAdapter.java b/services/app/app-web/src/main/java/com/ulticode/modules/submission/port/adapter/RemoteSubmissionUserQueryAdapter.java index 0faa85f4c0..245f53f2e0 100644 --- a/services/app/app-web/src/main/java/com/ulticode/modules/submission/port/adapter/RemoteSubmissionUserQueryAdapter.java +++ b/services/app/app-web/src/main/java/com/ulticode/modules/submission/port/adapter/RemoteSubmissionUserQueryAdapter.java @@ -26,6 +26,7 @@ public class RemoteSubmissionUserQueryAdapter implements SubmissionUserQueryPort { @DubboReference(group = "backend-submission", version = "1.1.0", + providedBy = "${ulticode.rpc.providers.submission:backend-submission}", timeout = RpcPolicy.QUERY_TIMEOUT_MS, retries = RpcPolicy.QUERY_RETRIES, check = false) private SubmissionUserQueryPort submissionUserQuery; diff --git a/services/app/app-web/src/main/java/com/ulticode/modules/submission/port/adapter/RemoteSubmissionUserStatsAdapter.java b/services/app/app-web/src/main/java/com/ulticode/modules/submission/port/adapter/RemoteSubmissionUserStatsAdapter.java index 910683742d..36874fefb5 100644 --- a/services/app/app-web/src/main/java/com/ulticode/modules/submission/port/adapter/RemoteSubmissionUserStatsAdapter.java +++ b/services/app/app-web/src/main/java/com/ulticode/modules/submission/port/adapter/RemoteSubmissionUserStatsAdapter.java @@ -16,6 +16,7 @@ public class RemoteSubmissionUserStatsAdapter implements SubmissionUserStatsPort { @DubboReference(group = "backend-submission", version = "1.1.0", + providedBy = "${ulticode.rpc.providers.submission:backend-submission}", timeout = RpcPolicy.QUERY_TIMEOUT_MS, retries = RpcPolicy.QUERY_RETRIES, check = false) private SubmissionUserStatsPort submissionUserStatsPort; diff --git a/services/app/app-web/src/main/java/com/ulticode/modules/submission/port/adapter/RemoteSubmissionWritePort.java b/services/app/app-web/src/main/java/com/ulticode/modules/submission/port/adapter/RemoteSubmissionWritePort.java index 3e9888b83d..e8c413b24e 100644 --- a/services/app/app-web/src/main/java/com/ulticode/modules/submission/port/adapter/RemoteSubmissionWritePort.java +++ b/services/app/app-web/src/main/java/com/ulticode/modules/submission/port/adapter/RemoteSubmissionWritePort.java @@ -14,7 +14,8 @@ @Primary public class RemoteSubmissionWritePort implements SubmissionIntakePort { - @DubboReference(group = "backend-submission", version = "1.0.0", timeout = RpcPolicy.WRITE_TIMEOUT_MS, retries = RpcPolicy.WRITE_RETRIES, check = false) + @DubboReference(group = "backend-submission", version = "1.0.0", + providedBy = "${ulticode.rpc.providers.submission:backend-submission}", timeout = RpcPolicy.WRITE_TIMEOUT_MS, retries = RpcPolicy.WRITE_RETRIES, check = false) private SubmissionIntakePort submissionIntake; @Override diff --git a/services/app/app-web/src/test/java/com/ulticode/app/adapter/in/web/AppReadinessControllerTest.java b/services/app/app-web/src/test/java/com/ulticode/app/adapter/in/web/AppReadinessControllerTest.java index 4af7fe90cc..feb91e810a 100644 --- a/services/app/app-web/src/test/java/com/ulticode/app/adapter/in/web/AppReadinessControllerTest.java +++ b/services/app/app-web/src/test/java/com/ulticode/app/adapter/in/web/AppReadinessControllerTest.java @@ -1,8 +1,24 @@ package com.ulticode.app.adapter.in.web; +import com.ulticode.auth.api.service.AccountQueryService; import com.ulticode.common.storage.StorageReadiness; +import com.ulticode.submission.api.service.SubmissionUserQueryPort; +import java.util.List; +import java.util.Set; +import org.apache.dubbo.config.ReferenceConfig; +import org.apache.dubbo.config.spring.ReferenceBean; +import org.apache.dubbo.config.spring.reference.ReferenceBeanManager; +import org.apache.dubbo.registry.client.ServiceDiscoveryRegistryDirectory; +import org.apache.dubbo.registry.client.migration.MigrationInvoker; +import org.apache.dubbo.rpc.cluster.ClusterInvoker; +import org.apache.dubbo.registry.client.ServiceInstance; +import org.apache.dubbo.registry.client.event.listener.ServiceInstancesChangedListener; +import org.apache.dubbo.rpc.cluster.router.state.BitList; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.boot.test.system.CapturedOutput; +import org.springframework.boot.test.system.OutputCaptureExtension; import org.springframework.beans.factory.ObjectProvider; import org.springframework.data.redis.core.RedisCallback; import org.springframework.data.redis.core.StringRedisTemplate; @@ -18,6 +34,9 @@ import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; /** * Readiness gate for the App owner. @@ -26,8 +45,84 @@ * never look ready, while contexts without the shared module keep their previous behaviour. */ @DisplayName("AppReadinessController") +@ExtendWith(OutputCaptureExtension.class) class AppReadinessControllerTest { + @SuppressWarnings("unchecked") + private static ObjectProvider referenceProvider(ReferenceBeanManager references) { + ObjectProvider provider = mock(ObjectProvider.class); + when(provider.getIfAvailable()).thenReturn(references); + return provider; + } + + @Test + @SuppressWarnings({"rawtypes", "unchecked"}) + void discoveryDiagnosticsDoNotProbeOrChangeReadinessAndOnlyLogChanges(CapturedOutput output) throws Exception { + ReferenceBeanManager manager = mock(ReferenceBeanManager.class); + ReferenceBean reference = mock(ReferenceBean.class); + ReferenceConfig config = mock(ReferenceConfig.class); + MigrationInvoker invoker = mock(MigrationInvoker.class); + ServiceDiscoveryRegistryDirectory directory = mock(ServiceDiscoveryRegistryDirectory.class); + when(reference.getServiceInterface()).thenReturn(SubmissionUserQueryPort.class.getName()); + when(reference.getReferenceConfig()).thenReturn(config); + when(config.getInvoker()).thenReturn(invoker); + when(invoker.getDirectory()).thenReturn(directory); + when(directory.getAllInvokers()).thenReturn(List.of()); + when(directory.getValidInvokers()).thenReturn(new BitList(List.of())); + when(directory.getInvokersToReconnect()).thenReturn(List.of()); + when(directory.getDisabledInvokers()).thenReturn(Set.of()); + ClusterInvoker applicationInvoker = mock(ClusterInvoker.class); + ServiceDiscoveryRegistryDirectory applicationDirectory = mock(ServiceDiscoveryRegistryDirectory.class); + ServiceInstancesChangedListener listener = mock(ServiceInstancesChangedListener.class); + when(invoker.getServiceDiscoveryInvoker()).thenReturn(applicationInvoker); + when(applicationInvoker.getDirectory()).thenReturn(applicationDirectory); + when(applicationDirectory.getAllInvokers()).thenReturn(List.of(mock(org.apache.dubbo.rpc.Invoker.class))); + when(applicationDirectory.getValidInvokers()).thenReturn(new BitList(List.of())); + when(applicationDirectory.getInvokersToReconnect()).thenReturn(List.of()); + when(applicationDirectory.getDisabledInvokers()).thenReturn(Set.of()); + when(applicationDirectory.getServiceListener()).thenReturn(listener); + when(listener.getServiceNames()).thenReturn(Set.of("never-log-application-name")); + when(listener.getAllInstances()).thenReturn(Map.of("never-log-application-name", List.of(mock(ServiceInstance.class)))); + doThrow(new AssertionError("must not probe application")).when(applicationInvoker).isAvailable(); + doThrow(new AssertionError("must not read application URL")).when(applicationInvoker).getUrl(); + doThrow(new AssertionError("must not invoke application RPC")).when(applicationInvoker).invoke(any()); + ReferenceBean authReference = mock(ReferenceBean.class); + when(authReference.getServiceInterface()).thenReturn(AccountQueryService.class.getName()); + when(authReference.getReferenceConfig()).thenReturn(config); + when(manager.getReferences()).thenReturn(List.of(reference, authReference)); + doThrow(new AssertionError("must not probe")).when(invoker).isAvailable(); + doThrow(new AssertionError("must not read URLs")).when(invoker).getUrl(); + doThrow(new AssertionError("must not invoke RPC")).when(invoker).invoke(any()); + doThrow(new AssertionError("must not probe directory")).when(directory).isAvailable(); + AppReadinessController controller = new AppReadinessController( + dataSourceProvider(), pingingRedis(), readinessProvider(null), referenceProvider(manager)); + + assertThat(controller.ready().getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(controller.ready().getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(output.getAll()).contains("AppDiscoveryObservation interface=submission_user_query state=INVOKER_PRESENT invoker_kind=MIGRATION directory_kind=SERVICE_DISCOVERY destroyed=FALSE all=0 valid=0 reconnect=0 disabled=0"); + assertThat(output.getAll().split("AppDiscoveryObservation interface=submission_user_query", -1)).hasSize(2); + assertThat(output.getAll()).contains("AppDiscoveryObservation interface=auth_account_query state=INVOKER_PRESENT"); + assertThat(output.getAll().split("AppDiscoveryObservation interface=auth_account_query", -1)).hasSize(2); + assertThat(output.getAll()).contains("AppApplicationDiscoveryObservation interface=submission_user_query state=INVOKER_PRESENT invoker_kind=MIGRATION directory_kind=SERVICE_DISCOVERY destroyed=FALSE all=1 valid=0 reconnect=0 disabled=0 listener_state=PRESENT mapped_apps=1 observed_apps=1 instances=1") + .doesNotContain("never-log-application-name"); + assertThat(output.getAll().split("AppApplicationDiscoveryObservation interface=submission_user_query", -1)).hasSize(2); + assertThat(output.getAll().split("AppApplicationDiscoveryObservation interface=auth_account_query", -1)).hasSize(2); + when(manager.getReferences()).thenThrow(new IllegalStateException("credential-canary")); + assertThat(controller.ready().getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(output.getAll()).contains("interface=submission_user_query state=INSPECTION_ERROR") + .doesNotContain("credential-canary"); + verify(invoker, never()).isAvailable(); + verify(invoker, never()).getUrl(); + verify(invoker, never()).invoke(any()); + verify(directory, never()).isAvailable(); + verify(reference, never()).getObject(); + verify(authReference, never()).getObject(); + verify(applicationInvoker, never()).isAvailable(); + verify(applicationInvoker, never()).getUrl(); + verify(applicationInvoker, never()).invoke(any()); + verify(applicationDirectory, never()).isAvailable(); + } + @SuppressWarnings("unchecked") private static ObjectProvider dataSourceProvider() throws Exception { DataSource dataSource = mock(DataSource.class); @@ -64,7 +159,7 @@ void reportsVerifiedStorage() throws Exception { StorageReadiness readiness = new StorageReadiness(); readiness.markReady(); AppReadinessController controller = new AppReadinessController( - dataSourceProvider(), pingingRedis(), readinessProvider(readiness)); + dataSourceProvider(), pingingRedis(), readinessProvider(readiness), referenceProvider(null)); ResponseEntity> response = controller.ready(); @@ -78,7 +173,7 @@ void runtimeStorageFailureAndRecoveryUpdatesReadiness() throws Exception { StorageReadiness readiness = new StorageReadiness(); readiness.markFailed("runtime outage"); AppReadinessController controller = new AppReadinessController( - dataSourceProvider(), pingingRedis(), readinessProvider(readiness)); + dataSourceProvider(), pingingRedis(), readinessProvider(readiness), referenceProvider(null)); ResponseEntity> failedResponse = controller.ready(); @@ -98,7 +193,7 @@ void runtimeStorageFailureAndRecoveryUpdatesReadiness() throws Exception { @DisplayName("a context without the shared storage module keeps db/redis semantics") void absentStorageModuleIsNotReportedDown() throws Exception { AppReadinessController controller = new AppReadinessController( - dataSourceProvider(), pingingRedis(), readinessProvider(null)); + dataSourceProvider(), pingingRedis(), readinessProvider(null), referenceProvider(null)); ResponseEntity> response = controller.ready(); diff --git a/services/app/app-web/src/test/java/com/ulticode/app/dubbo/SubmissionProviderApplicationTest.java b/services/app/app-web/src/test/java/com/ulticode/app/dubbo/SubmissionProviderApplicationTest.java new file mode 100644 index 0000000000..6a4e34a8c4 --- /dev/null +++ b/services/app/app-web/src/test/java/com/ulticode/app/dubbo/SubmissionProviderApplicationTest.java @@ -0,0 +1,95 @@ +package com.ulticode.app.dubbo; + +import java.lang.reflect.Field; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import org.apache.dubbo.common.URL; +import org.apache.dubbo.config.annotation.DubboReference; +import org.apache.dubbo.config.spring.util.AnnotationUtils; +import org.apache.dubbo.metadata.ServiceNameMapping; +import org.junit.jupiter.api.Test; +import org.springframework.core.env.MapPropertySource; +import org.springframework.core.env.StandardEnvironment; +import org.springframework.core.env.SystemEnvironmentPropertySource; + +import static org.assertj.core.api.Assertions.assertThat; + +/** The owner is explicit; provider addresses still come from normal instance discovery. */ +class SubmissionProviderApplicationTest { + private static final String PROPERTY = "ulticode.rpc.providers.submission"; + private static final String DECLARATION = "${" + PROPERTY + ":backend-submission}"; + + @Test + void allSubmissionReferencesDeclareOwnerWithoutRetargetingOtherReferences() throws Exception { + int submissionReferences = 0; + for (Field field : referenceFields()) { + DubboReference reference = field.getAnnotation(DubboReference.class); + if (reference.group().equals("backend-submission")) { + submissionReferences++; + assertThat(reference.providedBy()).as(field.toString()).containsExactly(DECLARATION); + } else { + assertThat(reference.providedBy()).as(field.toString()).isEmpty(); + } + } + assertThat(submissionReferences).isGreaterThanOrEqualTo(8); + } + + @Test + void actualDubboAnnotationResolutionUsesDefaultAndConfiguredProviderName() throws Exception { + assertMapping(cleanEnvironment(), "backend-submission"); + StandardEnvironment environment = cleanEnvironment(); + environment.getPropertySources().addFirst(new MapPropertySource("override", Map.of( + PROPERTY, "custom-submission", "DUBBO_APPLICATION_NAME", "different-consumer-name"))); + assertMapping(environment, "custom-submission"); + } + + @Test + void environmentAliasResolvesWithoutUsingConsumersOwnApplicationName() throws Exception { + StandardEnvironment environment = cleanEnvironment(); + environment.getPropertySources().addFirst(new SystemEnvironmentPropertySource("fixture", Map.of( + "ULTICODE_RPC_PROVIDERS_SUBMISSION", "renamed-submission", + "DUBBO_APPLICATION_NAME", "renamed-consumer"))); + assertMapping(environment, "renamed-submission"); + } + + private static void assertMapping(StandardEnvironment environment, String application) throws Exception { + int checked = 0; + for (Field field : referenceFields()) { + DubboReference reference = field.getAnnotation(DubboReference.class); + if (!reference.group().equals("backend-submission")) continue; + String[] providedBy = (String[]) AnnotationUtils.getAttributes(reference, environment, true).get("providedBy"); + assertThat(providedBy).as(field.toString()).containsExactly(application); + URL consumer = URL.valueOf("consumer://localhost/" + field.getType().getName()) + .addParameter("provided-by", String.join(",", providedBy)); + assertThat(ServiceNameMapping.getMappingByUrl(consumer)).containsExactly(application); + checked++; + } + assertThat(checked).isGreaterThanOrEqualTo(8); + } + + private static StandardEnvironment cleanEnvironment() { + StandardEnvironment environment = new StandardEnvironment(); + environment.getPropertySources().remove(StandardEnvironment.SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME); + environment.getPropertySources().remove(StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME); + return environment; + } + + private static List referenceFields() throws Exception { + Path root = Path.of("target/classes"); + List references = new ArrayList<>(); + try (var classes = Files.walk(root.resolve("com/ulticode"))) { + for (Path path : classes.filter(p -> p.toString().endsWith(".class")).toList()) { + String name = root.relativize(path).toString().replace(java.io.File.separatorChar, '.'); + Class type = Class.forName(name.substring(0, name.length() - 6), false, + SubmissionProviderApplicationTest.class.getClassLoader()); + for (Field field : type.getDeclaredFields()) { + if (field.isAnnotationPresent(DubboReference.class)) references.add(field); + } + } + } + return references; + } +} diff --git a/services/app/pom.xml b/services/app/pom.xml index 93ae696d85..8dd2241407 100644 --- a/services/app/pom.xml +++ b/services/app/pom.xml @@ -13,7 +13,7 @@ - 1.0.5 + 1.0.6 backend-app ${service.version.app} diff --git a/services/auth/pom.xml b/services/auth/pom.xml index 24f6be3fc7..2e0e9d2832 100644 --- a/services/auth/pom.xml +++ b/services/auth/pom.xml @@ -13,7 +13,7 @@ - 1.0.5 + 1.0.6 backend-auth ${service.version.auth} diff --git a/services/judge/pom.xml b/services/judge/pom.xml index 372d4dd7f1..8b969ac89d 100644 --- a/services/judge/pom.xml +++ b/services/judge/pom.xml @@ -15,7 +15,7 @@ - 1.0.5 + 1.0.6 backend-judge diff --git a/services/judge/src/main/java/com/ulticode/judge/adapter/RemoteSubmissionFencePort.java b/services/judge/src/main/java/com/ulticode/judge/adapter/RemoteSubmissionFencePort.java index 5f0ffebe6d..1aea8a91eb 100644 --- a/services/judge/src/main/java/com/ulticode/judge/adapter/RemoteSubmissionFencePort.java +++ b/services/judge/src/main/java/com/ulticode/judge/adapter/RemoteSubmissionFencePort.java @@ -11,7 +11,9 @@ @Primary public class RemoteSubmissionFencePort implements SubmissionFencePort { - @DubboReference(group = "backend-submission", version = "1.1.0", timeout = RpcPolicy.WRITE_TIMEOUT_MS, retries = RpcPolicy.WRITE_RETRIES, check = false) + @DubboReference(group = "backend-submission", version = "1.1.0", + providedBy = "${ulticode.rpc.providers.submission:backend-submission}", + timeout = RpcPolicy.WRITE_TIMEOUT_MS, retries = RpcPolicy.WRITE_RETRIES, check = false) private SubmissionFencePort submissionFencePort; @Override diff --git a/services/judge/src/main/java/com/ulticode/judge/adapter/RemoteSubmissionVerdictWritePort.java b/services/judge/src/main/java/com/ulticode/judge/adapter/RemoteSubmissionVerdictWritePort.java index 96fdaf8482..75fd3b1ea2 100644 --- a/services/judge/src/main/java/com/ulticode/judge/adapter/RemoteSubmissionVerdictWritePort.java +++ b/services/judge/src/main/java/com/ulticode/judge/adapter/RemoteSubmissionVerdictWritePort.java @@ -13,6 +13,7 @@ public class RemoteSubmissionVerdictWritePort implements SubmissionVerdictWritePort { @DubboReference(group = "backend-submission", version = "1.0.0", + providedBy = "${ulticode.rpc.providers.submission:backend-submission}", timeout = RpcPolicy.WRITE_TIMEOUT_MS, retries = RpcPolicy.WRITE_RETRIES, check = false) private SubmissionVerdictWritePort submissionVerdict; diff --git a/services/judge/src/test/java/com/ulticode/judge/adapter/SubmissionProviderApplicationTest.java b/services/judge/src/test/java/com/ulticode/judge/adapter/SubmissionProviderApplicationTest.java new file mode 100644 index 0000000000..5a324dfb24 --- /dev/null +++ b/services/judge/src/test/java/com/ulticode/judge/adapter/SubmissionProviderApplicationTest.java @@ -0,0 +1,92 @@ +package com.ulticode.judge.adapter; + +import com.ulticode.common.rpc.RpcPolicy; +import java.lang.reflect.Field; +import java.util.List; +import java.util.Map; +import org.apache.dubbo.common.URL; +import org.apache.dubbo.config.annotation.DubboReference; +import org.apache.dubbo.config.spring.util.AnnotationUtils; +import org.apache.dubbo.metadata.ServiceNameMapping; +import org.junit.jupiter.api.Test; +import org.springframework.core.env.MapPropertySource; +import org.springframework.core.env.StandardEnvironment; +import org.springframework.core.env.SystemEnvironmentPropertySource; + +import static org.assertj.core.api.Assertions.assertThat; + +/** Explicit owner mapping lets Judge subscribe before Submission has registered. */ +class SubmissionProviderApplicationTest { + private static final String PROPERTY = "ulticode.rpc.providers.submission"; + private static final String DECLARATION = "${" + PROPERTY + ":backend-submission}"; + + @Test + void bothSubmissionReferencesDeclareOwnerAndPreserveWritePolicies() throws Exception { + for (Field field : referenceFields()) { + DubboReference reference = field.getAnnotation(DubboReference.class); + assertThat(reference).as(field.toString()).isNotNull(); + assertThat(reference.providedBy()).as(field.toString()).containsExactly(DECLARATION); + assertThat(reference.group()).isEqualTo("backend-submission"); + assertThat(reference.version()).isEqualTo( + field.getDeclaringClass() == RemoteSubmissionFencePort.class ? "1.1.0" : "1.0.0"); + assertThat(reference.timeout()).isEqualTo(RpcPolicy.WRITE_TIMEOUT_MS); + assertThat(reference.retries()).isEqualTo(RpcPolicy.WRITE_RETRIES).isZero(); + assertThat(reference.check()).isFalse(); + } + } + + @Test + void actualDubboAnnotationResolutionUsesDefaultProviderName() throws Exception { + assertMapping(cleanEnvironment(), "backend-submission"); + } + + @Test + void configuredProviderNameResolvesThroughDubboAnnotationAndMapping() throws Exception { + StandardEnvironment environment = cleanEnvironment(); + environment.getPropertySources().addFirst(new MapPropertySource("override", Map.of( + PROPERTY, "custom-submission", "DUBBO_APPLICATION_NAME", "different-judge"))); + assertMapping(environment, "custom-submission"); + } + + @Test + void environmentAliasResolvesWithoutUsingConsumersOwnApplicationName() throws Exception { + StandardEnvironment environment = cleanEnvironment(); + environment.getPropertySources().addFirst(new SystemEnvironmentPropertySource("fixture", Map.of( + "ULTICODE_RPC_PROVIDERS_SUBMISSION", "renamed-submission", + "DUBBO_APPLICATION_NAME", "renamed-judge"))); + assertMapping(environment, "renamed-submission"); + } + + @Test + void consumerApplicationNameAloneDoesNotRetargetDefaultOwner() throws Exception { + StandardEnvironment environment = cleanEnvironment(); + environment.getPropertySources().addFirst(new SystemEnvironmentPropertySource("fixture", Map.of( + "DUBBO_APPLICATION_NAME", "renamed-judge"))); + assertMapping(environment, "backend-submission"); + } + + private static void assertMapping(StandardEnvironment environment, String application) throws Exception { + for (Field field : referenceFields()) { + DubboReference reference = field.getAnnotation(DubboReference.class); + String[] providedBy = (String[]) AnnotationUtils.getAttributes(reference, environment, true) + .get("providedBy"); + assertThat(providedBy).as(field.toString()).containsExactly(application); + URL consumer = URL.valueOf("consumer://localhost/" + field.getType().getName()) + .addParameter("provided-by", String.join(",", providedBy)); + assertThat(ServiceNameMapping.getMappingByUrl(consumer)).containsExactly(application); + } + } + + private static StandardEnvironment cleanEnvironment() { + StandardEnvironment environment = new StandardEnvironment(); + environment.getPropertySources().remove(StandardEnvironment.SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME); + environment.getPropertySources().remove(StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME); + return environment; + } + + private static List referenceFields() throws NoSuchFieldException { + return List.of( + RemoteSubmissionVerdictWritePort.class.getDeclaredField("submissionVerdict"), + RemoteSubmissionFencePort.class.getDeclaredField("submissionFencePort")); + } +} diff --git a/services/notification/pom.xml b/services/notification/pom.xml index 9a2831a00c..70bdb9e697 100644 --- a/services/notification/pom.xml +++ b/services/notification/pom.xml @@ -14,7 +14,7 @@ - 1.0.5 + 1.0.6 backend-notification ${service.version.notification} diff --git a/services/platform/rpc-resilience/pom.xml b/services/platform/rpc-resilience/pom.xml index f1912280b3..06a9afffc4 100644 --- a/services/platform/rpc-resilience/pom.xml +++ b/services/platform/rpc-resilience/pom.xml @@ -14,6 +14,23 @@ UltiCode Backend (Dubbo dependency resilience) + + + com.alibaba.nacos + nacos-client + ${nacos-client.version} + pure + + + com.alibaba.nacos + nacos-common + ${nacos-client.version} + + + com.alibaba.nacos + nacos-api + ${nacos-client.version} + org.apache.dubbo dubbo-config-spring diff --git a/services/platform/rpc-resilience/src/main/java/com/ulticode/rpc/resilience/DubboDiscoveryReadiness.java b/services/platform/rpc-resilience/src/main/java/com/ulticode/rpc/resilience/DubboDiscoveryReadiness.java index 7ca64aa7a0..ebf154a72a 100644 --- a/services/platform/rpc-resilience/src/main/java/com/ulticode/rpc/resilience/DubboDiscoveryReadiness.java +++ b/services/platform/rpc-resilience/src/main/java/com/ulticode/rpc/resilience/DubboDiscoveryReadiness.java @@ -1,13 +1,204 @@ package com.ulticode.rpc.resilience; +import java.util.Collection; +import java.util.Map; +import org.apache.dubbo.registry.client.event.listener.ServiceInstancesChangedListener; +import java.util.function.Supplier; +import org.apache.dubbo.registry.client.ServiceDiscoveryRegistryDirectory; +import org.apache.dubbo.registry.client.migration.MigrationInvoker; +import org.apache.dubbo.registry.client.migration.ServiceDiscoveryMigrationInvoker; +import org.apache.dubbo.registry.integration.DynamicDirectory; +import org.apache.dubbo.rpc.Invoker; +import org.apache.dubbo.rpc.cluster.ClusterInvoker; import java.util.HashSet; import java.util.Set; import org.apache.dubbo.config.spring.reference.ReferenceBeanManager; -/** Read-only discovery check: never invokes a query or retries a command. */ +/** Existing readiness predicate plus metadata-only diagnostics; neither invokes business RPC. */ public final class DubboDiscoveryReadiness { private DubboDiscoveryReadiness() {} + /** Diagnostic metadata only. Never probe availability or stringify Dubbo objects. */ + public enum State { + MANAGER_MISSING, REFERENCE_MISSING, CONFIG_MISSING, INVOKER_MISSING, + INVOKER_PRESENT, UNSUPPORTED, INSPECTION_ERROR + } + + public enum InvokerKind { SERVICE_DISCOVERY_MIGRATION, MIGRATION, CLUSTER, OTHER, MISSING } + public enum DirectoryKind { SERVICE_DISCOVERY, DYNAMIC, OTHER, MISSING } + public enum Destroyed { TRUE, FALSE, UNKNOWN, ERROR } + public enum CountState { MEASURED, UNKNOWN, OVERFLOW, ERROR } + + public record Count(CountState state, int value) { + public Count { + if (state == null || value < 0 || value > 10_000) throw new IllegalArgumentException(); + } + + public String token() { + return state == CountState.MEASURED ? Integer.toString(value) : state.name(); + } + } + + public record Observation(State state, InvokerKind invokerKind, DirectoryKind directoryKind, + Destroyed destroyed, Count all, Count valid, Count reconnect, Count disabled) {} + + private static final Count UNKNOWN = new Count(CountState.UNKNOWN, 0); + + private static Observation absent(State state, InvokerKind invoker, DirectoryKind directory) { + return new Observation(state, invoker, directory, Destroyed.UNKNOWN, UNKNOWN, UNKNOWN, UNKNOWN, UNKNOWN); + } + + public static Observation inspect(ReferenceBeanManager manager, String requiredInterface) { + if (manager == null) return absent(State.MANAGER_MISSING, InvokerKind.MISSING, DirectoryKind.MISSING); + try { + for (var reference : manager.getReferences()) { + if (!requiredInterface.equals(reference.getServiceInterface())) continue; + var config = reference.getReferenceConfig(); + if (config == null) return absent(State.CONFIG_MISSING, InvokerKind.MISSING, DirectoryKind.MISSING); + return inspectInvoker(config.getInvoker()); + } + return absent(State.REFERENCE_MISSING, InvokerKind.MISSING, DirectoryKind.MISSING); + } catch (RuntimeException notInitialized) { + return absent(State.INSPECTION_ERROR, InvokerKind.MISSING, DirectoryKind.MISSING); + } + } + + private static Observation inspectInvoker(Invoker invoker) { + if (invoker == null) return absent(State.INVOKER_MISSING, InvokerKind.MISSING, DirectoryKind.MISSING); + InvokerKind kind = invoker instanceof ServiceDiscoveryMigrationInvoker ? InvokerKind.SERVICE_DISCOVERY_MIGRATION + : invoker instanceof MigrationInvoker ? InvokerKind.MIGRATION + : invoker instanceof ClusterInvoker ? InvokerKind.CLUSTER : InvokerKind.OTHER; + if (!(invoker instanceof ClusterInvoker cluster)) { + return absent(State.UNSUPPORTED, kind, DirectoryKind.MISSING); + } + try { + // MigrationInvoker delegates this getter without checking availability. + // Do not traverse its children or call isAvailable(), list(), refresh(), or getUrl(). + var directory = cluster.getDirectory(); + if (directory == null) return absent(State.INVOKER_PRESENT, kind, DirectoryKind.MISSING); + if (!(directory instanceof DynamicDirectory dynamic)) { + return absent(State.UNSUPPORTED, kind, DirectoryKind.OTHER); + } + DirectoryKind directoryKind = dynamic instanceof ServiceDiscoveryRegistryDirectory + ? DirectoryKind.SERVICE_DISCOVERY : DirectoryKind.DYNAMIC; + // These getters are non-atomic observations; counts have no equality/readiness invariant. + return new Observation(State.INVOKER_PRESENT, kind, directoryKind, destroyed(dynamic), + count(dynamic::getAllInvokers), count(dynamic::getValidInvokers), + count(dynamic::getInvokersToReconnect), count(dynamic::getDisabledInvokers)); + } catch (RuntimeException notInitialized) { + return absent(State.INSPECTION_ERROR, kind, DirectoryKind.MISSING); + } + } + + public enum ListenerState { MISSING, PRESENT, UNSUPPORTED, ERROR } + + /** Existing application-discovery child only, independent of the selected migration branch. */ + public record ApplicationObservation(Observation directory, ListenerState listenerState, + Count mappedApplications, Count observedApplications, Count instances) {} + + private static ApplicationObservation applicationAbsent(Observation directory, ListenerState listener) { + return new ApplicationObservation(directory, listener, UNKNOWN, UNKNOWN, UNKNOWN); + } + + public static ApplicationObservation inspectApplication(ReferenceBeanManager manager, String requiredInterface) { + if (manager == null) return applicationAbsent( + absent(State.MANAGER_MISSING, InvokerKind.MISSING, DirectoryKind.MISSING), ListenerState.MISSING); + try { + for (var reference : manager.getReferences()) { + if (!requiredInterface.equals(reference.getServiceInterface())) continue; + var config = reference.getReferenceConfig(); + if (config == null) return applicationAbsent( + absent(State.CONFIG_MISSING, InvokerKind.MISSING, DirectoryKind.MISSING), ListenerState.MISSING); + return inspectApplicationInvoker(config.getInvoker()); + } + return applicationAbsent(absent(State.REFERENCE_MISSING, InvokerKind.MISSING, DirectoryKind.MISSING), + ListenerState.MISSING); + } catch (RuntimeException notInitialized) { + return applicationAbsent(absent(State.INSPECTION_ERROR, InvokerKind.MISSING, DirectoryKind.MISSING), + ListenerState.ERROR); + } + } + + private static ApplicationObservation inspectApplicationInvoker(Invoker invoker) { + InvokerKind kind = invoker instanceof ServiceDiscoveryMigrationInvoker ? InvokerKind.SERVICE_DISCOVERY_MIGRATION + : invoker instanceof MigrationInvoker ? InvokerKind.MIGRATION + : invoker instanceof ClusterInvoker ? InvokerKind.CLUSTER + : invoker == null ? InvokerKind.MISSING : InvokerKind.OTHER; + try { + // ServiceDiscoveryMigrationInvoker also stores its application invoker in this inherited field. + // Never use the selected branch, initialize a reference, or probe transport availability. + Invoker application = invoker instanceof MigrationInvoker migration + ? migration.getServiceDiscoveryInvoker() : invoker; + if (application == null) return applicationAbsent( + absent(State.INVOKER_MISSING, kind, DirectoryKind.MISSING), ListenerState.MISSING); + if (!(application instanceof ClusterInvoker cluster)) return applicationAbsent( + absent(State.UNSUPPORTED, kind, DirectoryKind.MISSING), ListenerState.UNSUPPORTED); + var directory = cluster.getDirectory(); + if (directory == null) return applicationAbsent( + absent(State.INVOKER_PRESENT, kind, DirectoryKind.MISSING), ListenerState.MISSING); + if (!(directory instanceof ServiceDiscoveryRegistryDirectory dynamic)) return applicationAbsent( + absent(State.UNSUPPORTED, kind, directory instanceof DynamicDirectory + ? DirectoryKind.DYNAMIC : DirectoryKind.OTHER), ListenerState.UNSUPPORTED); + Observation observation = new Observation(State.INVOKER_PRESENT, kind, DirectoryKind.SERVICE_DISCOVERY, + destroyed(dynamic), count(dynamic::getAllInvokers), count(dynamic::getValidInvokers), + count(dynamic::getInvokersToReconnect), count(dynamic::getDisabledInvokers)); + try { + ServiceInstancesChangedListener listener = dynamic.getServiceListener(); + if (listener == null) return applicationAbsent(observation, ListenerState.MISSING); + Count mapped = count(listener::getServiceNames); + // These are bounded, non-atomic samples. A concurrent update can produce ERROR; + // absence/zero alone does not prove a missing mapping or explain an RPC failure. + Map> instances = listener.getAllInstances(); + Count observed = instances == null ? UNKNOWN : count(instances::values); + return new ApplicationObservation(observation, ListenerState.PRESENT, mapped, observed, + instanceCount(instances)); + } catch (RuntimeException notInitialized) { + return applicationAbsent(observation, ListenerState.ERROR); + } + } catch (RuntimeException notInitialized) { + return applicationAbsent(absent(State.INSPECTION_ERROR, kind, DirectoryKind.MISSING), ListenerState.ERROR); + } + } + + private static Count instanceCount(Map> instances) { + if (instances == null) return UNKNOWN; + try { + int total = 0; + int visited = 0; + for (Collection values : instances.values()) { + if (++visited > 10_000) return new Count(CountState.OVERFLOW, 0); + if (values == null) return UNKNOWN; + int size = values.size(); + if (size < 0) return new Count(CountState.ERROR, 0); + if (size > 10_000 - total) return new Count(CountState.OVERFLOW, 0); + total += size; + } + return new Count(CountState.MEASURED, total); + } catch (RuntimeException notInitialized) { + return new Count(CountState.ERROR, 0); + } + } + + private static Count count(Supplier> getter) { + try { + Collection values = getter.get(); + if (values == null) return UNKNOWN; + int size = values.size(); + if (size < 0) return new Count(CountState.ERROR, 0); + return size > 10_000 ? new Count(CountState.OVERFLOW, 0) : new Count(CountState.MEASURED, size); + } catch (RuntimeException notInitialized) { + return new Count(CountState.ERROR, 0); + } + } + + private static Destroyed destroyed(DynamicDirectory directory) { + try { + return directory.isDestroyed() ? Destroyed.TRUE : Destroyed.FALSE; + } catch (RuntimeException notInitialized) { + return Destroyed.ERROR; + } + } + public static boolean available(ReferenceBeanManager manager, Set requiredInterfaces) { if (manager == null || requiredInterfaces.isEmpty()) return false; Set available = new HashSet<>(); diff --git a/services/platform/rpc-resilience/src/test/java/com/ulticode/rpc/resilience/DubboDiscoveryReadinessTest.java b/services/platform/rpc-resilience/src/test/java/com/ulticode/rpc/resilience/DubboDiscoveryReadinessTest.java new file mode 100644 index 0000000000..7407dce635 --- /dev/null +++ b/services/platform/rpc-resilience/src/test/java/com/ulticode/rpc/resilience/DubboDiscoveryReadinessTest.java @@ -0,0 +1,234 @@ +package com.ulticode.rpc.resilience; + +import java.util.AbstractList; +import java.util.AbstractMap; +import java.util.AbstractSet; +import java.util.Iterator; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; +import org.apache.dubbo.common.URL; +import org.apache.dubbo.config.ReferenceConfig; +import org.apache.dubbo.config.spring.ReferenceBean; +import org.apache.dubbo.config.spring.reference.ReferenceBeanManager; +import org.apache.dubbo.registry.client.ServiceDiscoveryRegistryDirectory; +import org.apache.dubbo.registry.client.ServiceInstance; +import org.apache.dubbo.registry.client.event.listener.ServiceInstancesChangedListener; +import org.apache.dubbo.registry.client.migration.MigrationInvoker; +import org.apache.dubbo.registry.client.migration.ServiceDiscoveryMigrationInvoker; +import org.apache.dubbo.rpc.Invocation; +import org.apache.dubbo.rpc.Invoker; +import org.apache.dubbo.rpc.Result; +import org.apache.dubbo.rpc.cluster.ClusterInvoker; +import org.apache.dubbo.rpc.cluster.Directory; +import org.junit.jupiter.api.Test; + +import static com.ulticode.rpc.resilience.DubboDiscoveryReadiness.*; +import static org.assertj.core.api.Assertions.assertThat; + +class DubboDiscoveryReadinessTest { + private static final String INTERFACE = Runnable.class.getName(); + private static final URL URL_VALUE = URL.valueOf("consumer://localhost/" + INTERFACE); + + @Test + void missingManagerReferenceConfigAndInvokerRemainDistinct() { + assertThat(inspectApplication(null, INTERFACE).directory().state()).isEqualTo(State.MANAGER_MISSING); + assertThat(inspectApplication(new ReferenceBeanManager(), INTERFACE).directory().state()) + .isEqualTo(State.REFERENCE_MISSING); + ReferenceBean reference = reference(null); + ReferenceBeanManager manager = manager(reference); + assertThat(inspectApplication(manager, INTERFACE).directory().state()).isEqualTo(State.CONFIG_MISSING); + assertThat(inspectApplication(manager((Invoker) null), INTERFACE).directory().state()) + .isEqualTo(State.INVOKER_MISSING); + } + + @Test + void bothMigrationTypesUseOnlyApplicationChildEvenWhenSelectedDirectoryWouldFail() { + for (boolean discoveryOnly : List.of(false, true)) { + MigrationInvoker migration = migration(discoveryOnly); + assertThat(inspectApplication(manager(migration), INTERFACE).directory().state()) + .isEqualTo(State.INVOKER_MISSING); + migration.setServiceDiscoveryInvoker(cluster(new TestDirectory(null))); + var result = inspectApplication(manager(migration), INTERFACE); + assertThat(result.directory().directoryKind()).isEqualTo(DirectoryKind.SERVICE_DISCOVERY); + assertThat(result.directory().invokerKind()).isEqualTo(discoveryOnly + ? InvokerKind.SERVICE_DISCOVERY_MIGRATION : InvokerKind.MIGRATION); + assertThat(result.listenerState()).isEqualTo(ListenerState.MISSING); + } + } + + @Test + void existingListenerReportsEmptyMapEmptyListAndInstancesWithoutReadingNamesOrObjects() { + for (Map> data : List.of( + Map.>of(), Map.of("secret-host", List.of()), + Map.of("secret-host", sized(2), "secret-token", sized(3)))) { + var result = observe(listener(Set.of("private-provider"), data)); + assertThat(result.listenerState()).isEqualTo(ListenerState.PRESENT); + assertThat(result.mappedApplications().token()).isEqualTo("1"); + assertThat(result.observedApplications().token()).isEqualTo(Integer.toString(data.size())); + assertThat(result.instances().token()).isEqualTo(data.size() == 2 ? "5" : "0"); + assertThat(result.toString()).doesNotContain("secret", "private-provider", "consumer://"); + } + var empty = observe(listener(Set.of(), Map.of())); + assertThat(empty.mappedApplications().token()).isEqualTo("0"); + } + + @Test + void countsAreBoundedAndUnknownOrExceptionsAreSanitized() { + assertThat(observe(listener(Set.of("x"), Map.of("x", sized(10_001)))).instances().token()) + .isEqualTo("OVERFLOW"); + assertThat(observe(listener(Set.of("x"), Map.of("x", sized(6_000), "y", sized(6_000)))) + .instances().token()).isEqualTo("OVERFLOW"); + assertThat(observe(listener(Set.of("x"), Map.of("x", sized(-1)))).instances().token()).isEqualTo("ERROR"); + assertThat(observe(listener(Set.of("x"), null)).instances().token()).isEqualTo("UNKNOWN"); + ServiceInstancesChangedListener failing = new ServiceInstancesChangedListener(Set.of(), null) { + @Override public Map> getAllInstances() { + throw new IllegalStateException("secret-token private-url"); + } + }; + var error = observe(failing); + assertThat(error.listenerState()).isEqualTo(ListenerState.ERROR); + assertThat(error.directory().state()).isEqualTo(State.INVOKER_PRESENT); + assertThat(error.toString()).doesNotContain("secret", "private-url"); + } + + @Test + void applicationAndInstanceCountsBoundTraversalAndSanitizeCollectionFailures() { + Set oversizedNames = new AbstractSet<>() { + @Override public int size() { return 10_001; } + @Override public Iterator iterator() { throw new AssertionError("Names must not be read"); } + }; + assertThat(observe(listener(oversizedNames, Map.of())).mappedApplications().token()).isEqualTo("OVERFLOW"); + Map> oversizedMap = new AbstractMap<>() { + @Override public Set>> entrySet() { + return new AbstractSet<>() { + @Override public int size() { return 10_001; } + @Override public Iterator>> iterator() { + return new Iterator<>() { + int visited; + @Override public boolean hasNext() { return true; } + @Override public Entry> next() { + if (++visited > 10_001) throw new AssertionError("Unbounded traversal"); + return Map.entry("private-name", List.of()); + } + }; + } + }; + } + }; + var overflow = observe(listener(Set.of(), oversizedMap)); + assertThat(overflow.observedApplications().token()).isEqualTo("OVERFLOW"); + assertThat(overflow.instances().token()).isEqualTo("OVERFLOW"); + List failingList = new AbstractList<>() { + @Override public int size() { throw new IllegalStateException("private-url"); } + @Override public ServiceInstance get(int index) { throw new AssertionError("Instance access forbidden"); } + }; + var failure = observe(listener(Set.of(), Map.of("private-name", failingList))); + assertThat(failure.instances().token()).isEqualTo("ERROR"); + assertThat(failure.toString()).doesNotContain("private"); + } + + @Test + void listenerGetterFailurePreservesDirectoryCounts() { + TestDirectory directory = new TestDirectory(null) { + @Override public ServiceInstancesChangedListener getServiceListener() { + throw new IllegalStateException("private-url"); + } + }; + var result = inspectApplication(manager(cluster(directory)), INTERFACE); + assertThat(result.directory().all().token()).isEqualTo("0"); + assertThat(result.listenerState()).isEqualTo(ListenerState.ERROR); + assertThat(result.toString()).doesNotContain("private"); + } + + @Test + void getterFailuresAndUnsupportedInvokerDoNotEscape() { + ClusterInvoker unsupported = cluster(null); + assertThat(inspectApplication(manager(unsupported), INTERFACE).directory().directoryKind()) + .isEqualTo(DirectoryKind.MISSING); + ReferenceBeanManager failing = new ReferenceBeanManager() { + @Override public Collection getReferences() { throw new IllegalStateException("secret"); } + }; + assertThat(inspectApplication(failing, INTERFACE).directory().state()).isEqualTo(State.INSPECTION_ERROR); + } + + private static ApplicationObservation observe(ServiceInstancesChangedListener listener) { + return inspectApplication(manager(cluster(new TestDirectory(listener))), INTERFACE); + } + + private static ServiceInstancesChangedListener listener(Set names, Map> data) { + return new ServiceInstancesChangedListener(names, null) { + @Override public Map> getAllInstances() { return data; } + }; + } + + private static List sized(int size) { + return new AbstractList<>() { + @Override public int size() { return size; } + @Override public ServiceInstance get(int index) { throw new AssertionError("Must not inspect instances"); } + }; + } + + private static ReferenceBean reference(ReferenceConfig config) { + return new ReferenceBean<>() { + @Override public String getServiceInterface() { return INTERFACE; } + @Override public ReferenceConfig getReferenceConfig() { return config; } + @Override public Object getObject() { throw new AssertionError("Must not initialize proxy"); } + }; + } + + private static ReferenceBeanManager manager(Invoker invoker) { + return manager(reference(new ReferenceConfig<>() { + @Override public Invoker getInvoker() { return invoker; } + })); + } + + private static ReferenceBeanManager manager(ReferenceBean reference) { + return new ReferenceBeanManager() { + @Override public Collection getReferences() { return List.of(reference); } + }; + } + + private static MigrationInvoker migration(boolean discoveryOnly) { + if (discoveryOnly) return new ServiceDiscoveryMigrationInvoker<>(null, null, null, Object.class, URL_VALUE, URL_VALUE) { + @Override public Directory getDirectory() { throw new AssertionError("Selected directory forbidden"); } + @Override public boolean isAvailable() { throw new AssertionError("Probe forbidden"); } + @Override public URL getUrl() { throw new AssertionError("URL forbidden"); } + @Override public Result invoke(Invocation invocation) { throw new AssertionError("RPC forbidden"); } + }; + return new MigrationInvoker<>(null, null, null, Object.class, URL_VALUE, URL_VALUE) { + @Override public Directory getDirectory() { throw new AssertionError("Selected directory forbidden"); } + @Override public boolean isAvailable() { throw new AssertionError("Probe forbidden"); } + @Override public URL getUrl() { throw new AssertionError("URL forbidden"); } + @Override public Result invoke(Invocation invocation) { throw new AssertionError("RPC forbidden"); } + }; + } + + private static ClusterInvoker cluster(Directory directory) { + return new ClusterInvoker<>() { + @Override public Directory getDirectory() { return directory; } + @Override public Class getInterface() { return Object.class; } + @Override public boolean isDestroyed() { return false; } + @Override public void destroy() { throw new AssertionError("Destroy forbidden"); } + @Override public URL getRegistryUrl() { throw new AssertionError("Registry URL forbidden"); } + @Override public URL getUrl() { throw new AssertionError("URL forbidden"); } + @Override public boolean isAvailable() { throw new AssertionError("Probe forbidden"); } + @Override public Result invoke(Invocation invocation) { throw new AssertionError("RPC forbidden"); } + }; + } + + private static class TestDirectory extends ServiceDiscoveryRegistryDirectory { + private final ServiceInstancesChangedListener listener; + private boolean armed; + TestDirectory(ServiceInstancesChangedListener listener) { super(Object.class, URL_VALUE); this.listener = listener; this.armed = true; } + @Override public URL getUrl() { + if (armed) throw new AssertionError("Directory URL forbidden"); + return super.getUrl(); + } + @Override public ServiceInstancesChangedListener getServiceListener() { return listener; } + @Override public List> getAllInvokers() { return List.of(); } + @Override public org.apache.dubbo.rpc.cluster.router.state.BitList> getValidInvokers() { return null; } + @Override public boolean isAvailable() { throw new AssertionError("Probe forbidden"); } + } +} diff --git a/services/platform/rpc-resilience/src/test/java/com/ulticode/rpc/resilience/NacosRuntimeDependencyTest.java b/services/platform/rpc-resilience/src/test/java/com/ulticode/rpc/resilience/NacosRuntimeDependencyTest.java new file mode 100644 index 0000000000..155e280d28 --- /dev/null +++ b/services/platform/rpc-resilience/src/test/java/com/ulticode/rpc/resilience/NacosRuntimeDependencyTest.java @@ -0,0 +1,71 @@ +package com.ulticode.rpc.resilience; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.alibaba.nacos.api.grpc.auto.Metadata; +import com.alibaba.nacos.api.grpc.auto.Payload; +import com.alibaba.nacos.api.grpc.auto.RequestGrpc; +import com.alibaba.nacos.client.naming.NacosNamingService; +import com.google.protobuf.Any; +import com.google.protobuf.ByteString; +import io.grpc.ManagedChannel; +import io.grpc.Server; +import io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder; +import io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder; +import io.grpc.stub.StreamObserver; +import java.net.InetSocketAddress; +import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.Test; + +class NacosRuntimeDependencyTest { + + @Test + void excludesTheSdkWithUnmanagedShadedDependencies() throws Exception { + assertThat(NacosNamingService.class.getProtectionDomain().getCodeSource().getLocation().toString()) + .endsWith("-pure.jar"); + assertThat(getClass().getClassLoader().getResources( + "com/alibaba/nacos/api/NacosFactory.class").asIterator()) + .toIterable().hasSize(1); + assertThat(getClass().getClassLoader().getResource( + "com/alibaba/nacos/shaded/com/google/common/collect/ImmutableList.class")) + .isNull(); + } + + @Test + void roundTripsNacosMessagesWithTheManagedProtobufRuntime() throws Exception { + Payload payload = payload(); + assertThat(Payload.parseFrom(payload.toByteArray())).isEqualTo(payload); + } + + @Test + void exchangesNacosMessagesOverTheManagedGrpcTransport() throws Exception { + Server server = NettyServerBuilder.forAddress(new InetSocketAddress("127.0.0.1", 0)) + .addService(new RequestGrpc.RequestImplBase() { + @Override + public void request(Payload request, StreamObserver response) { + response.onNext(request); + response.onCompleted(); + } + }).build().start(); + ManagedChannel channel = null; + try { + channel = NettyChannelBuilder.forAddress("127.0.0.1", server.getPort()) + .usePlaintext().build(); + assertThat(RequestGrpc.newBlockingStub(channel).withDeadlineAfter(5, TimeUnit.SECONDS) + .request(payload())).isEqualTo(payload()); + } finally { + if (channel != null) { + channel.shutdownNow().awaitTermination(5, TimeUnit.SECONDS); + } + server.shutdownNow().awaitTermination(5, TimeUnit.SECONDS); + } + } + + private static Payload payload() { + return Payload.newBuilder() + .setMetadata(Metadata.newBuilder().setType("ServerCheckRequest") + .putHeaders("requestId", "dependency-regression")) + .setBody(Any.newBuilder().setValue(ByteString.copyFromUtf8("{}"))) + .build(); + } +} diff --git a/services/pom.xml b/services/pom.xml index 6bc8bd362c..7c276570dc 100644 --- a/services/pom.xml +++ b/services/pom.xml @@ -52,18 +52,20 @@ - 1.0.5 - 1.0.5 - 1.0.5 - 1.0.5 - 1.0.5 - 1.0.5 - 1.0.5 + 1.0.6 + 1.0.6 + 1.0.6 + 1.0.6 + 1.0.6 + 1.0.6 + 1.0.6 17 2.21.7 3.1.7 11.0.25 + 33.7.2-jre + 2.5.1 UTF-8 3.5.17 0.13.0 @@ -225,6 +227,23 @@ okhttp ${okhttp.version} + + + org.apache.dubbo + dubbo-registry-nacos + ${dubbo.version} + + + com.alibaba.nacos + nacos-client + + + + + com.google.guava + guava + ${guava.version} + org.testcontainers testcontainers-bom diff --git a/services/search/pom.xml b/services/search/pom.xml index d65046dd84..f20034628f 100644 --- a/services/search/pom.xml +++ b/services/search/pom.xml @@ -14,7 +14,7 @@ - 1.0.5 + 1.0.6 backend-search diff --git a/services/submission/pom.xml b/services/submission/pom.xml index 7bff749c82..1272cc6fa8 100644 --- a/services/submission/pom.xml +++ b/services/submission/pom.xml @@ -15,7 +15,7 @@ - 1.0.5 + 1.0.6 backend-submission diff --git a/services/submission/src/main/java/com/ulticode/submission/config/SubmissionDiscoveryStartupGate.java b/services/submission/src/main/java/com/ulticode/submission/config/SubmissionDiscoveryStartupGate.java index 98f014eeb4..2e6633914b 100644 --- a/services/submission/src/main/java/com/ulticode/submission/config/SubmissionDiscoveryStartupGate.java +++ b/services/submission/src/main/java/com/ulticode/submission/config/SubmissionDiscoveryStartupGate.java @@ -34,12 +34,34 @@ public void run(ApplicationArguments args) throws InterruptedException { } void await(Duration timeout) throws InterruptedException { - long deadline = System.nanoTime() + timeout.toNanos(); + long started = System.nanoTime(); + long deadline = started + timeout.toNanos(); while (!ready()) { if (System.nanoTime() >= deadline) { + observe("deadline", started); throw new IllegalStateException("Submission projection providers were not discovered before readiness deadline"); } Thread.sleep(250); } + observe("ready", started); + } + + private void observe(String outcome, long started) { + // Separate observations may race with discovery; they never determine the outcome. + try { + long elapsed = Math.min(86_400_000L, Math.max(0L, (System.nanoTime() - started) / 1_000_000L)); + observeInterface(outcome, elapsed, "problem_facts", ProblemFactsPort.class.getName()); + observeInterface(outcome, elapsed, "user_read", SubmissionUserReadPort.class.getName()); + } catch (RuntimeException ignored) { + // Diagnostics must not change startup success/failure, including logging failures. + } + } + + private void observeInterface(String outcome, long elapsed, String alias, String interfaceName) { + var observation = DubboDiscoveryReadiness.inspect(references, interfaceName); + log.info("SubmissionDiscoveryObservation outcome={} elapsed_ms={} interface={} state={} invoker_kind={} directory_kind={} destroyed={} all={} valid={} reconnect={} disabled={}", + outcome, elapsed, alias, observation.state(), observation.invokerKind(), observation.directoryKind(), + observation.destroyed(), observation.all().token(), observation.valid().token(), + observation.reconnect().token(), observation.disabled().token()); } } diff --git a/services/submission/src/test/java/com/ulticode/submission/config/SubmissionDiscoveryStartupGateTest.java b/services/submission/src/test/java/com/ulticode/submission/config/SubmissionDiscoveryStartupGateTest.java index 8a728a7157..ac5369baf3 100644 --- a/services/submission/src/test/java/com/ulticode/submission/config/SubmissionDiscoveryStartupGateTest.java +++ b/services/submission/src/test/java/com/ulticode/submission/config/SubmissionDiscoveryStartupGateTest.java @@ -2,13 +2,26 @@ import com.ulticode.app.api.service.ProblemFactsPort; import com.ulticode.app.api.service.SubmissionUserReadPort; +import com.ulticode.rpc.resilience.DubboDiscoveryReadiness; +import com.ulticode.rpc.resilience.DubboDiscoveryReadiness.State; import java.time.Duration; import java.util.List; +import java.util.Set; +import org.apache.dubbo.registry.client.ServiceDiscoveryRegistryDirectory; +import org.apache.dubbo.registry.client.migration.MigrationInvoker; +import org.apache.dubbo.registry.client.migration.ServiceDiscoveryMigrationInvoker; +import org.apache.dubbo.registry.integration.DynamicDirectory; +import org.apache.dubbo.rpc.cluster.ClusterInvoker; +import org.apache.dubbo.rpc.cluster.Directory; +import org.apache.dubbo.rpc.cluster.router.state.BitList; import org.apache.dubbo.config.ReferenceConfig; import org.apache.dubbo.config.spring.ReferenceBean; import org.apache.dubbo.config.spring.reference.ReferenceBeanManager; import org.apache.dubbo.rpc.Invoker; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.boot.test.system.CapturedOutput; +import org.springframework.boot.test.system.OutputCaptureExtension; import org.springframework.beans.factory.ObjectProvider; import static org.assertj.core.api.Assertions.assertThat; @@ -61,4 +74,175 @@ void bothProjectionProvidersMustBeAvailableAndCanRecover() throws Exception { verify(problem, never()).getObject(); verify(users, never()).getObject(); } + + @SuppressWarnings({"rawtypes", "unchecked"}) + private static ReferenceBeanManager diagnosticManager(Invoker invoker) { + var manager = mock(ReferenceBeanManager.class); + ReferenceBean bean = mock(ReferenceBean.class); + ReferenceConfig config = mock(ReferenceConfig.class); + when(bean.getServiceInterface()).thenReturn(ProblemFactsPort.class.getName()); + when(bean.getReferenceConfig()).thenReturn(config); + when(config.getInvoker()).thenReturn(invoker); + when(manager.getReferences()).thenReturn(List.of(bean)); + return manager; + } + + private static DubboDiscoveryReadiness.Observation inspect(ReferenceBeanManager manager) { + var result = DubboDiscoveryReadiness.inspect(manager, ProblemFactsPort.class.getName()); + for (var reference : manager.getReferences()) verify(reference, never()).getObject(); + return result; + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + private static void forbidProbe(Invoker invoker) { + doThrow(new AssertionError("diagnostics must not probe availability")).when(invoker).isAvailable(); + doThrow(new AssertionError("diagnostics must not read URLs")).when(invoker).getUrl(); + doThrow(new AssertionError("diagnostics must not invoke RPC")).when(invoker).invoke(any()); + } + + @SuppressWarnings("rawtypes") + private static void verifyNoProbe(Invoker invoker) { + verify(invoker, never()).isAvailable(); + verify(invoker, never()).getUrl(); + verify(invoker, never()).invoke(any()); + } + + @Test + void diagnosticMissingAndErrorStatesDoNotBecomeAvailabilityClaims() { + String type = ProblemFactsPort.class.getName(); + assertThat(DubboDiscoveryReadiness.inspect(null, type).state()).isEqualTo(State.MANAGER_MISSING); + var manager = mock(ReferenceBeanManager.class); + when(manager.getReferences()).thenReturn(List.of()); + assertThat(inspect(manager).state()).isEqualTo(State.REFERENCE_MISSING); + manager = diagnosticManager(null); + var reference = manager.getReferences().iterator().next(); + assertThat(inspect(manager).state()).isEqualTo(State.INVOKER_MISSING); + when(reference.getReferenceConfig()).thenReturn(null); + assertThat(inspect(manager).state()).isEqualTo(State.CONFIG_MISSING); + when(reference.getReferenceConfig()).thenThrow(new IllegalStateException("credential-canary")); + assertThat(inspect(manager).state()).isEqualTo(State.INSPECTION_ERROR); + } + + @Test + @SuppressWarnings({"rawtypes", "unchecked"}) + void unknownImplementationsAreUnsupportedWithoutInvokingTheirGetters() { + Invoker other = mock(Invoker.class); + forbidProbe(other); + var observation = inspect(diagnosticManager(other)); + assertThat(observation.state()).isEqualTo(State.UNSUPPORTED); + assertThat(observation.invokerKind()).isEqualTo(DubboDiscoveryReadiness.InvokerKind.OTHER); + assertThat(observation.all().token()).isEqualTo("UNKNOWN"); + verifyNoProbe(other); + + ClusterInvoker cluster = mock(ClusterInvoker.class); + Directory unknown = mock(Directory.class); + forbidProbe(cluster); + doThrow(new AssertionError("unknown directory must not be probed")).when(unknown).isAvailable(); + when(cluster.getDirectory()).thenReturn(unknown); + observation = inspect(diagnosticManager(cluster)); + assertThat(observation.state()).isEqualTo(State.UNSUPPORTED); + assertThat(observation.directoryKind()).isEqualTo(DubboDiscoveryReadiness.DirectoryKind.OTHER); + verifyNoInteractions(unknown); + verifyNoProbe(cluster); + + when(cluster.getDirectory()).thenReturn(null); + observation = inspect(diagnosticManager(cluster)); + assertThat(observation.state()).isEqualTo(State.INVOKER_PRESENT); + assertThat(observation.directoryKind()).isEqualTo(DubboDiscoveryReadiness.DirectoryKind.MISSING); + when(cluster.getDirectory()).thenThrow(new IllegalStateException("credential-canary")); + assertThat(inspect(diagnosticManager(cluster)).state()).isEqualTo(State.INSPECTION_ERROR); + verifyNoProbe(cluster); + } + + @Test + @SuppressWarnings({"rawtypes", "unchecked"}) + void knownDirectoriesExposeOnlyNonAtomicBoundedCountsWithoutProbingChildren() { + ClusterInvoker[] invokers = {mock(ClusterInvoker.class), mock(MigrationInvoker.class), + mock(ServiceDiscoveryMigrationInvoker.class)}; + var kinds = new DubboDiscoveryReadiness.InvokerKind[] {DubboDiscoveryReadiness.InvokerKind.CLUSTER, + DubboDiscoveryReadiness.InvokerKind.MIGRATION, + DubboDiscoveryReadiness.InvokerKind.SERVICE_DISCOVERY_MIGRATION}; + for (int i = 0; i < invokers.length; i++) { + ClusterInvoker invoker = invokers[i]; + DynamicDirectory directory = i == 2 ? mock(ServiceDiscoveryRegistryDirectory.class) : mock(DynamicDirectory.class); + Invoker child = mock(Invoker.class); + forbidProbe(invoker); + forbidProbe(child); + doThrow(new AssertionError("directory availability is mutating")).when(directory).isAvailable(); + doThrow(new AssertionError("directory URL must remain private")).when(directory).getUrl(); + doThrow(new AssertionError("must not route invocations")).when(directory).list(any()); + when(invoker.getDirectory()).thenReturn(directory); + when(directory.getAllInvokers()).thenReturn(List.of(child)); + var valid = new BitList(List.of()); + when(directory.getValidInvokers()).thenReturn(valid); + when(directory.getInvokersToReconnect()).thenReturn(List.of(child)); + when(directory.getDisabledInvokers()).thenReturn(Set.of(child)); + var observation = inspect(diagnosticManager(invoker)); + assertThat(observation.state()).isEqualTo(State.INVOKER_PRESENT); + assertThat(observation.invokerKind()).isEqualTo(kinds[i]); + assertThat(observation.directoryKind()).isEqualTo(i == 2 + ? DubboDiscoveryReadiness.DirectoryKind.SERVICE_DISCOVERY : DubboDiscoveryReadiness.DirectoryKind.DYNAMIC); + assertThat(observation.destroyed()).isEqualTo(DubboDiscoveryReadiness.Destroyed.FALSE); + assertThat(observation.all().token()).isEqualTo("1"); + assertThat(observation.valid().token()).isEqualTo("0"); + assertThat(observation.reconnect().token()).isEqualTo("1"); + assertThat(observation.disabled().token()).isEqualTo("1"); + when(directory.getAllInvokers()).thenReturn(List.of()); + when(directory.isDestroyed()).thenReturn(true); + assertThat(inspect(diagnosticManager(invoker)).all().token()).isEqualTo("0"); + assertThat(inspect(diagnosticManager(invoker)).destroyed()).isEqualTo(DubboDiscoveryReadiness.Destroyed.TRUE); + verifyNoProbe(invoker); + verifyNoProbe(child); + verify(directory, never()).isAvailable(); + verify(directory, never()).getUrl(); + verify(directory, never()).list(any()); + } + } + + @Test + @SuppressWarnings({"rawtypes", "unchecked"}) + void countFailuresNullsAndOverflowAreNotReportedAsZero() { + ClusterInvoker invoker = mock(ClusterInvoker.class); + DynamicDirectory directory = mock(DynamicDirectory.class); + forbidProbe(invoker); + doThrow(new AssertionError("must not probe")).when(directory).isAvailable(); + when(invoker.getDirectory()).thenReturn(directory); + List oversized = mock(List.class); + when(oversized.size()).thenReturn(10_001); + when(directory.getAllInvokers()).thenReturn(oversized); + when(directory.getValidInvokers()).thenReturn(null); + when(directory.getInvokersToReconnect()).thenThrow(new IllegalStateException("credential-canary")); + when(directory.getDisabledInvokers()).thenReturn(Set.of()); + when(directory.isDestroyed()).thenThrow(new IllegalStateException("credential-canary")); + var observation = inspect(diagnosticManager(invoker)); + assertThat(observation.all().token()).isEqualTo("OVERFLOW"); + assertThat(observation.valid().token()).isEqualTo("UNKNOWN"); + assertThat(observation.reconnect().token()).isEqualTo("ERROR"); + assertThat(observation.disabled().token()).isEqualTo("0"); + assertThat(observation.destroyed()).isEqualTo(DubboDiscoveryReadiness.Destroyed.ERROR); + when(oversized.size()).thenReturn(10_000); + assertThat(inspect(diagnosticManager(invoker)).all().token()).isEqualTo("10000"); + when(oversized.size()).thenReturn(-1); + assertThat(inspect(diagnosticManager(invoker)).all().token()).isEqualTo("ERROR"); + verify(oversized, never()).iterator(); + verify(directory, never()).isAvailable(); + verify(directory, never()).getUrl(); + verify(directory, never()).list(any()); + verifyNoProbe(invoker); + } + + @Test + @ExtendWith(OutputCaptureExtension.class) + void diagnosticInspectionFailureDoesNotChangeSuccessfulGateDecision(CapturedOutput output) throws Exception { + var manager = mock(ReferenceBeanManager.class); + var problem = reference(ProblemFactsPort.class, true); + var users = reference(SubmissionUserReadPort.class, true); + when(manager.getReferences()).thenReturn(List.of(problem, users)) + .thenThrow(new IllegalStateException("credential-canary")); + gate(manager).await(Duration.ZERO); + assertThat(output.getAll()).contains("SubmissionDiscoveryObservation outcome=ready elapsed_ms=") + .contains("interface=problem_facts state=INSPECTION_ERROR") + .contains("interface=user_read state=INSPECTION_ERROR") + .doesNotContain("credential-canary"); + } }