From 75e73b39b5b8a2a0662b2231a992f0a8e116c2af Mon Sep 17 00:00:00 2001 From: Viktor Buzin Date: Sat, 10 Oct 2026 18:09:40 +0300 Subject: [PATCH 01/13] fix github pages --- .github/workflows/dockerhub-metadata.yml | 36 ++++- .github/workflows/pages.yml | 182 +++++------------------ CHANGELOG.md | 32 ++-- README-RU.md | 26 ++-- README.md | 26 ++-- dockerhub/delibera-webui.md | 18 +-- docs/ReleaseProcess.md | 62 +++----- site/index.html | Bin 0 -> 19746 bytes 8 files changed, 128 insertions(+), 254 deletions(-) create mode 100644 site/index.html diff --git a/.github/workflows/dockerhub-metadata.yml b/.github/workflows/dockerhub-metadata.yml index b315a2f..b229572 100644 --- a/.github/workflows/dockerhub-metadata.yml +++ b/.github/workflows/dockerhub-metadata.yml @@ -54,6 +54,20 @@ jobs: fi echo "token=$token" >> "$GITHUB_OUTPUT" + # Decode the JWT and refuse to run with an expired/stale token: a 403 five minutes + # later is much harder to diagnose than a clear failure here. Hub JWTs live ~5 min. + claims="$(jq -rn --arg t "$token" ' + ($t | split(".")[1] | gsub("-";"+") | gsub("_";"/") + | . + ("=" * ((4 - length % 4) % 4))) as $p + | ($p | @base64d | fromjson)')" + exp="$(jq -r '.exp // 0' <<< "$claims")" + now="$(date +%s)" + if [ "$exp" -le "$now" ]; then + echo "::error::Docker Hub token is already expired (exp=$exp, now=$now) — re-run the job." >&2 + exit 1 + fi + echo " token valid for $(( exp - now )) more seconds" + - name: Push descriptions and categories env: JWT: ${{ steps.auth.outputs.token }} @@ -66,14 +80,22 @@ jobs: echo "── techbuzzz/$repo" full="$(jq -Rs . < "dockerhub/$repo.md")" - jq -n --arg d "$short" --arg f "$full" \ + desc_resp="$(jq -n --arg d "$short" --arg f "$full" \ '{description:$d, full_description:$f}' \ - | curl -fsS -X PATCH \ - -H "Authorization: JWT ${JWT}" \ + | curl -sS -w $'\n%{http_code}' -X PATCH \ + -H "Authorization: Bearer ${JWT}" \ -H 'Content-Type: application/json' \ --data-binary @- \ - "https://hub.docker.com/v2/repositories/techbuzzz/$repo/" > /dev/null - echo " description + overview synced" + "https://hub.docker.com/v2/repositories/techbuzzz/$repo/")" + desc_code="${desc_resp##*$'\n'}" + if [ "$desc_code" = "200" ] || [ "$desc_code" = "201" ]; then + echo " description + overview synced" + else + echo "::error::description PATCH rejected for $repo (HTTP $desc_code)" >&2 + echo " Docker Hub said: $(printf '%s' "${desc_resp%$'\n'*}" | head -c 600)" >&2 + echo " If this is a 403, DOCKERHUB_TOKEN needs Read/Write/Delete scope on the techbuzzz namespace." >&2 + exit 1 + fi # Categories come from a fixed Docker Hub taxonomy keyed by slug, not by the # display name shown in the UI, so the slugs below are the part most likely to @@ -82,7 +104,7 @@ jobs: # body, so print it instead of discarding it. cats_resp="$(jq -nc --argjson c "$categories" '{categories:$c}' \ | curl -sS -w $'\n%{http_code}' -X PATCH \ - -H "Authorization: JWT ${JWT}" \ + -H "Authorization: Bearer ${JWT}" \ -H 'Content-Type: application/json' \ --data-binary @- \ "https://hub.docker.com/v2/repositories/techbuzzz/$repo/")" @@ -104,7 +126,7 @@ jobs: run: | set -euo pipefail for repo in delibera-server delibera-webui; do - body="$(curl -fsS -H "Authorization: JWT ${JWT}" \ + body="$(curl -fsS -H "Authorization: Bearer ${JWT}" \ "https://hub.docker.com/v2/repositories/techbuzzz/$repo/")" desc="$(jq -r '.description // ""' <<< "$body")" len="$(jq -r '(.full_description // "") | length' <<< "$body")" diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index dbcf5ea..26b8d9c 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -1,17 +1,17 @@ -name: Deploy Nuxt site to Pages +name: pages -# Publishes the real Delibera Web UI to GitHub Pages as a static build. +# Publishes site/index.html to GitHub Pages on every merge to main. # -# This is a STATIC export of the same app that ships in the container. The important -# consequence, which is not obvious from the workflow: `nuxt generate` produces no Nitro server, -# so the BFF route `/api/delibera/**` does not exist in the deployed site. The browser then needs -# an absolute API origin, supplied at build time by NUXT_PUBLIC_DELIBERA_API_BASE and resolved in -# app/composables/useDebates.ts. That server must allow this origin via CORS — Delibera.Server -# registers no CORS policy by default, because in its normal container deployment the browser -# only ever talks to the same-origin proxy. +# The site is hand-written HTML, not a generated one: a static landing page does not justify a +# docs toolchain (MkDocs/Docusaurus) in a repo that has no Python. The one thing that must stay +# in sync is the version string, so it is injected here from the csproj that actually drives +# `dotnet pack` — meaning the page can never advertise a version that was not published. # -# Without that env var the site still deploys and still renders, but every request 404s. That is -# a legitimate state for a UI preview; it is not a working install. +# The Nuxt Web UI is NOT deployed here. It ships in the container image +# (techbuzzz/delibera-webui) and is meant to run next to Delibera.Server, which provides the +# same-origin BFF route /api/delibera/**. A static export has no Nitro server and therefore no +# BFF, so the UI would need a separate CORS-enabled API origin — a deliberate owner decision, +# not something this workflow should configure silently. on: push: @@ -23,160 +23,60 @@ permissions: pages: write id-token: write -# Allow only one concurrent deployment, skipping runs queued between the in-progress and the -# latest queued. Do NOT cancel in-progress runs — let a production deploy finish. +# A new push supersedes an in-flight deploy; without this the second run would fail on the +# concurrency lock and leave the site one deploy behind. concurrency: group: pages - cancel-in-progress: false + cancel-in-progress: true jobs: - build: + deploy: + name: Build and deploy Pages runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} steps: - - name: Checkout - uses: actions/checkout@v4 + - uses: actions/checkout@v4 - - name: Detect package manager - id: detect-package-manager + - name: Resolve version from the packable project + id: version run: | set -euo pipefail - if [ -f "${{ github.workspace }}/src/Delibera.WebUI/pnpm-lock.yaml" ]; then - echo "manager=pnpm" >> "$GITHUB_OUTPUT" - echo "lockfile=pnpm-lock.yaml" >> "$GITHUB_OUTPUT" - echo "command=install" >> "$GITHUB_OUTPUT" - elif [ -f "${{ github.workspace }}/src/Delibera.WebUI/yarn.lock" ]; then - echo "manager=yarn" >> "$GITHUB_OUTPUT" - echo "lockfile=yarn.lock" >> "$GITHUB_OUTPUT" - echo "command=install" >> "$GITHUB_OUTPUT" - elif [ -f "${{ github.workspace }}/src/Delibera.WebUI/package-lock.json" ]; then - echo "manager=npm" >> "$GITHUB_OUTPUT" - echo "lockfile=package-lock.json" >> "$GITHUB_OUTPUT" - echo "command=ci" >> "$GITHUB_OUTPUT" - else - echo "::error::no lockfile found in src/Delibera.WebUI" >&2 + # Read the version from the project that publish-nuget.yml packs. Do NOT read it from + # a tag or from CHANGELOG.md: those can disagree with what is on nuget.org, and a + # landing page claiming an unpublished version is worse than a stale one. + version="$(sed -n 's/.*\([^<]*\)<\/Version>.*/\1/p' src/Delibera.Core/Delibera.Core.csproj | head -1)" + if [ -z "$version" ]; then + echo "Could not resolve from src/Delibera.Core/Delibera.Core.csproj" >&2 exit 1 fi - - - name: Setup Node - uses: actions/setup-node@v4 - with: - node-version: '22' - cache: ${{ steps.detect-package-manager.outputs.manager }} - # Must follow the manager the detect step chose. Hardcoding the npm lockfile works - # today because package-lock.json is the only one in the tree — but if this repo ever - # adopts pnpm, a pnpm cache keyed on a file that does not exist never invalidates, and - # a stale node_modules is restored forever under a green build. - cache-dependency-path: src/Delibera.WebUI/${{ steps.detect-package-manager.outputs.lockfile }} + echo "Resolved Delibera version: $version" + echo "value=$version" >> "$GITHUB_OUTPUT" - name: Setup Pages id: pages uses: actions/configure-pages@v5 - # No `static_site_generator: nuxt` here, and that is deliberate. That input makes the - # action REWRITE the generator config file instead of exporting anything, and it only - # understands .js/.cjs/.mjs — our nuxt.config.ts is TypeScript, so it emits a warning and - # injects nothing. The keys it would inject (`router.base`, `target`) are Nuxt 3 names - # that Nuxt 4 has moved or removed anyway. The base path is taken from this step's - # `base_path` output and passed to the build as NUXT_APP_BASE_URL instead. - - - name: Install dependencies - working-directory: src/Delibera.WebUI - run: ${{ steps.detect-package-manager.outputs.manager }} ${{ steps.detect-package-manager.outputs.command }} - - - name: Typecheck - working-directory: src/Delibera.WebUI - run: ${{ steps.detect-package-manager.outputs.manager }} run typecheck - - - name: Static HTML export with Nuxt - working-directory: src/Delibera.WebUI - env: - # Empty by default: the site deploys as a UI preview whose API calls 404. - # Set it as a repository variable to point the static build at a live server. - NUXT_PUBLIC_DELIBERA_API_BASE: ${{ vars.NUXT_PUBLIC_DELIBERA_API_BASE }} - # The site's own base path (e.g. /Delibera). Nuxt reads NUXT_APP_BASE_URL into - # app.baseURL, so the export keeps its documents at the artifact root while asset - # URLs are prefixed with the base — which is exactly what Pages expects. - NUXT_APP_BASE_URL: ${{ steps.pages.outputs.base_path }} - run: ${{ steps.detect-package-manager.outputs.manager }} run generate - - - name: Fail if the export produced a real site - run: | - set -euo pipefail - # `.output/public`, NOT `dist` — Nuxt 4 moved the static output there. GitHub's own - # sample workflow still uploads ./dist because it predates Nuxt 4, and copying it - # verbatim uploads nothing at all. - # - # The second assertion matters more: without an explicit nitro.prerender.routes list - # the export contains only 200.html + 404.html and NO document for the real pages, - # which deploys as a blank site that still reports a green build. - out=src/Delibera.WebUI/.output/public - if [ ! -f "$out/index.html" ]; then - echo "::error::$out/index.html is missing — the site would deploy blank" >&2 - ls -la "$out" || true - exit 1 - fi - echo "prerendered: $(cd "$out" && find . -name '*.html' | sort | tr '\n' ' ')" - echo "total files: $(find "$out" -type f | wc -l)" - - name: Fail if the configured API base did not reach the bundle - env: - API_BASE: ${{ vars.NUXT_PUBLIC_DELIBERA_API_BASE }} + - name: Assemble site run: | set -euo pipefail - # Nuxt substitutes `import.meta.env.NUXT_PUBLIC_*` only for keys declared under - # runtimeConfig.public. Undeclare or rename `deliberaApiBase` and nothing fails: the - # build goes green and the deployed site keeps calling its own origin, which on - # Pages is a 404. A green pipeline is exactly the signal that cannot catch this, - # so check the artefact instead of the exit code. - if [ -z "$API_BASE" ]; then - echo "no API base configured — deploying a UI preview whose requests 404" - exit 0 - fi - out=src/Delibera.WebUI/.output/public - if ! grep -rqF "$API_BASE" "$out"; then - echo "::error::$API_BASE is configured but appears nowhere in the export" >&2 - echo "Check that deliberaApiBase is still declared under runtimeConfig.public." >&2 + mkdir -p _site/img + cp site/index.html _site/index.html + # The banner is the same asset the README uses — one file, two consumers. + cp img/delibera-horizontal-1920x480.png _site/img/ + sed -i "s/{{VERSION}}/${{ steps.version.outputs.value }}/g" _site/index.html + if grep -q '{{VERSION}}' _site/index.html; then + echo "Unsubstituted {{VERSION}} placeholder left in the built site" >&2 exit 1 fi - echo "API base $API_BASE is baked into the export" - - - name: Fail if the Pages base path did not reach the export - run: | - set -euo pipefail - # configure-pages injects the site's base path into nuxt.config before the build, so - # a project site (techbuzzz.github.io/Delibera) emits /Delibera/_nuxt/... asset URLs - # while the files themselves stay at the root of the artifact. If the injection ever - # stops happening, the HTML keeps root-relative URLs: the deploy succeeds, and the - # site renders unstyled over a client bundle that 404s. - base="${{ steps.pages.outputs.base_path }}" - if [ -z "$base" ]; then - echo "no base path reported — an owner/org Pages site, nothing to check" - exit 0 - fi - html=src/Delibera.WebUI/.output/public/index.html - if grep -q "href=\"$base/_nuxt/" "$html"; then - echo "base path $base is applied to the export's asset URLs" - exit 0 - fi - echo "::error::expected asset URLs under $base/_nuxt/ in $html" >&2 - grep -o 'href="[^"]*_nuxt/[^"]*"' "$html" | head -3 >&2 || true - exit 1 + ls -la _site _site/img - name: Upload artifact uses: actions/upload-pages-artifact@v3 with: - path: src/Delibera.WebUI/.output/public + path: _site - deploy: - environment: - name: github-pages - url: ${{ steps.deployment.outputs.page_url }} - runs-on: ubuntu-latest - needs: build - steps: - name: Deploy to GitHub Pages id: deployment - # v5 is current (5.0.1, Sep 2026) and is what GitHub's own sample pairs with - # upload-pages-artifact@v3. It stays on upload-artifact@v4 internals, so v3 artifacts - # are still the supported input — no reason to move the upload major and re-test a - # tarball format nobody here can reproduce locally. - uses: actions/deploy-pages@v5 \ No newline at end of file + uses: actions/deploy-pages@v5 diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c7ec3c..40695c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed — GitHub Pages deploys the project website, not the Web UI + +`pages.yml` previously built and deployed the Nuxt Web UI as a static export. That was wrong: the +Web UI is meant to ship in the container image (`techbuzzz/delibera-webui`) and run next to +`Delibera.Server`, which provides the same-origin BFF route `/api/delibera/**`. A static export has +no Nitro server and therefore no BFF, so the deployed site could only reach an API via a separate +CORS-enabled origin — a deliberate owner decision, not something the workflow should configure +silently. + +Pages now publishes the hand-written landing page from `site/index.html` — header, install +instructions, documentation links, NuGet/Docker badges. The version string is injected from +`Delibera.Core.csproj` at build time, so the page cannot advertise an unpublished version. + ### Added — opt-in CORS for static browser clients `Delibera:Server:Cors:AllowedOrigins` registers a named CORS policy listing the browser origins @@ -18,12 +31,11 @@ page from *reading* a response — a cross-origin `fetch` still fires the reques cannot see the answer. Turning this on globally would have added a browser-side CSRF vector to an API that is unauthenticated, unthrottled, and spends real credits per debate. -Set it only for a deliberately static client — the GitHub Pages build of the Web UI, which has no -Nitro server and therefore no BFF route. In compose: +Set it only for a deliberately static client that calls the API cross-origin. In compose: ```yaml environment: - DELIBERA_CORS_ORIGIN: https://techbuzzz.github.io + DELIBERA_CORS_ORIGIN: https://example.com ``` Origins are matched exactly; there is no wildcard switch. `AllowCredentials` together with `*` @@ -33,20 +45,6 @@ pipeline over HTTP — that a default host emits no `Access-Control-Allow-Origin configured origin gets its own origin echoed back rather than a wildcard, that an unlisted origin gets nothing, that the preflight is answered, and that `X-Correlation-Id` is exposed. -### Fixed — the static Web UI build ignored its configured API origin - -`NUXT_PUBLIC_DELIBERA_API_BASE` did not reach the bundle, so a GitHub Pages build configured with -it still called its own origin and every request 404'd. `pages.yml` was green throughout: Nuxt -substitutes `import.meta.env.NUXT_PUBLIC_*` only for names declared under `runtimeConfig.public`, -and an undeclared one is silently left as-is rather than rejected. `deliberaApiBase` is now -declared (empty, so the container's same-origin behaviour is unchanged), and `pages.yml` asserts -that a configured origin actually appears in the export — a dead site is no longer able to report -a successful deployment. - -Verified: a build with the variable set carries it into 4 places in the export; a build without it -carries it into none; the new gate passes when the origin is present, fails naming the missing -config key when it is not, and is a no-op in preview mode. Web UI tests 34/34. - ## [10.5.2] - 2026-10-09 **Contains behaviour changes.** Two are breaking for existing deployments: published ports now diff --git a/README-RU.md b/README-RU.md index 57b96e4..f69ae94 100644 --- a/README-RU.md +++ b/README-RU.md @@ -15,7 +15,7 @@ [![Docker Hub](https://img.shields.io/badge/Docker%20Hub-2496ED?logo=docker&logoColor=fff)](https://hub.docker.com/r/techbuzzz/delibera-server) [![Pulls: server](https://img.shields.io/docker/pulls/techbuzzz/delibera-server?logo=docker&logoColor=2496ED)](https://hub.docker.com/r/techbuzzz/delibera-server) [![Pulls: webui](https://img.shields.io/docker/pulls/techbuzzz/delibera-webui?logo=docker&logoColor=2496ED)](https://hub.docker.com/r/techbuzzz/delibera-webui) -[![Web UI](https://img.shields.io/badge/Web%20UI-GitHub%20Pages-3E8AFF?logo=githubpages)](https://techbuzzz.github.io/Delibera/) +[![Website](https://img.shields.io/badge/Website-GitHub%20Pages-3E8AFF?logo=githubpages)](https://techbuzzz.github.io/Delibera/) [![CI](https://github.com/techbuzzz/Delibera/actions/workflows/publish-nuget.yml/badge.svg)](https://github.com/techbuzzz/Delibera/actions/workflows/publish-nuget.yml) [![Docker CI](https://github.com/techbuzzz/Delibera/actions/workflows/publish-docker.yml/badge.svg)](https://github.com/techbuzzz/Delibera/actions/workflows/publish-docker.yml) [![License: MIT](https://img.shields.io/badge/License-MIT-10B981.svg)](LICENSE) @@ -269,23 +269,17 @@ docker compose -f docker-compose.hub.yml up -d > Все публикуемые порты слушают `127.0.0.1`. Поставьте аутентифицирующий обратный прокси, прежде чем > открывать этот стек в сеть, которой не управляете. -#### 🌐 Веб-интерфейс на GitHub Pages +#### 🌐 Сайт проекта на GitHub Pages -Тот же интерфейс статически публикуется на при каждом -мердже в `main`. +Лендинг проекта публикуется на при каждом мердже в `main`. +Там ссылки на документацию, заметки о релизах, пакеты NuGet, образы Docker и исходный код. -У статической сборки **нет сервера**, поэтому same-origin BFF-маршрута в ней не существует — нужен -абсолютный origin API, и тот сервер должен разрешать origin Pages через CORS: - -```yaml -environment: - DELIBERA_CORS_ORIGIN: https://techbuzzz.github.io -``` - -CORS **выключен по умолчанию** и остаётся выключенным, пока не перечислены origins. Это намеренно: -именно отсутствие заголовка не даёт случайной веб-странице прочитать ответ этого API, а контейнерному -Web UI он не нужен вовсе. Комбинация «wildcard + AllowCredentials» отклоняется на старте с сообщением, -называющим конкретный ключ конфигурации. +Сам веб-интерфейс **там не публикуется** — он поставляется в образе +[`techbuzzz/delibera-webui`](https://hub.docker.com/r/techbuzzz/delibera-webui) и предназначен для +работы рядом с `Delibera.Server`, который предоставляет same-origin BFF-маршрут +`/api/delibera/**`, от которого зависит UI. Статическая сборка не имеет Nitro-сервера и, значит, +BFF, поэтому ей потребовался бы отдельный API-origin с разрешённым CORS — это осознанное решение +владельца, а не то, что workflow Pages настраивает молча. #### 📥 Из исходников diff --git a/README.md b/README.md index 5f72b1f..ade4cb3 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ [![Docker Hub](https://img.shields.io/badge/Docker%20Hub-2496ED?logo=docker&logoColor=fff)](https://hub.docker.com/r/techbuzzz/delibera-server) [![Pulls: server](https://img.shields.io/docker/pulls/techbuzzz/delibera-server?logo=docker&logoColor=2496ED)](https://hub.docker.com/r/techbuzzz/delibera-server) [![Pulls: webui](https://img.shields.io/docker/pulls/techbuzzz/delibera-webui?logo=docker&logoColor=2496ED)](https://hub.docker.com/r/techbuzzz/delibera-webui) -[![Web UI](https://img.shields.io/badge/Web%20UI-GitHub%20Pages-3E8AFF?logo=githubpages)](https://techbuzzz.github.io/Delibera/) +[![Website](https://img.shields.io/badge/Website-GitHub%20Pages-3E8AFF?logo=githubpages)](https://techbuzzz.github.io/Delibera/) [![CI](https://github.com/techbuzzz/Delibera/actions/workflows/publish-nuget.yml/badge.svg)](https://github.com/techbuzzz/Delibera/actions/workflows/publish-nuget.yml) [![Docker CI](https://github.com/techbuzzz/Delibera/actions/workflows/publish-docker.yml/badge.svg)](https://github.com/techbuzzz/Delibera/actions/workflows/publish-docker.yml) [![License: MIT](https://img.shields.io/badge/License-MIT-10B981.svg)](LICENSE) @@ -268,22 +268,18 @@ docker compose -f docker-compose.hub.yml up -d > published port binds to `127.0.0.1`. Put an authenticating reverse proxy in front before exposing > this stack to any network you do not control. -#### 🌐 Web UI on GitHub Pages +#### 🌐 Project website on GitHub Pages -The same UI is deployed statically to on every merge to -`main`. +The project landing page is published at on every merge +to `main`. It links to the documentation, release notes, NuGet packages, Docker images and the +source repository. -A static export has **no server**, so the UI's same-origin BFF route does not exist there — it needs -an absolute API origin, and that server must allow the Pages origin via CORS: - -```yaml -environment: - DELIBERA_CORS_ORIGIN: https://techbuzzz.github.io -``` - -CORS is **off by default** and stays off unless you list origins. That is deliberate: the missing -header is what stops a random web page from reading this API, and the Web UI in the container does -not need it. `Wildcard + AllowCredentials` is rejected at startup with a message naming the key. +The Web UI itself is **not** deployed there — it ships in the +[`techbuzzz/delibera-webui`](https://hub.docker.com/r/techbuzzz/delibera-webui) container image and +is meant to run next to `Delibera.Server`, which provides the same-origin BFF route +`/api/delibera/**` the UI depends on. A static export has no Nitro server and therefore no BFF, +so it would need a separate CORS-enabled API origin — a deliberate owner decision, not something +the Pages workflow configures silently. #### 📥 From source diff --git a/dockerhub/delibera-webui.md b/dockerhub/delibera-webui.md index 8ef70e7..1f3c75d 100644 --- a/dockerhub/delibera-webui.md +++ b/dockerhub/delibera-webui.md @@ -43,20 +43,12 @@ authenticating reverse proxy in front before exposing this stack to any network --- -## Static build (GitHub Pages) +## GitHub Pages -The same app is published as a static site at - on every merge to `main`. - -A static export has **no Nitro server**, so the `/api/delibera/**` BFF route does not exist there. -Set `NUXT_PUBLIC_DELIBERA_API_BASE` to a reachable server origin to give the static build an API — -and that server must allow the Pages origin via CORS, which `Delibera.Server` does not do by -default. Without it the page deploys and renders, but its requests 404; that is a UI preview, not -an install. - -Two build details worth knowing if you fork it: Nuxt 4 emits to `.output/public`, not `dist`, and -the prerender routes must be listed explicitly in `nuxt.config.ts` — otherwise the export contains -only the `200.html`/`404.html` SPA fallbacks and deploys as a blank page. +The project landing page lives at . The Web UI itself is +**not** deployed there — it ships in this image and is meant to run next to +[`delibera-server`](https://hub.docker.com/r/techbuzzz/delibera-server), which provides the +same-origin BFF route `/api/delibera/**`. --- diff --git a/docs/ReleaseProcess.md b/docs/ReleaseProcess.md index 0ca06fa..2e26243 100644 --- a/docs/ReleaseProcess.md +++ b/docs/ReleaseProcess.md @@ -175,57 +175,29 @@ curl -s -o /dev/null -w '%{http_code}\n' https://techbuzzz.github.io/Delibera/ A 404 there, served by GitHub.com itself, means nothing has ever been deployed to this Pages site. Only the Source dropdown in Settings says whether it is configured to accept deploys. -`pages.yml` deploys the Web UI as a **static** export on every merge to `main`. Two consequences: +`pages.yml` deploys the **hand-written landing page** from `site/index.html` on every merge to +`main`. The version string is injected from `Delibera.Core.csproj` at build time, so the page +cannot advertise an unpublished version, and the build fails loudly on a leftover `{{VERSION}}` +placeholder. -- A static export has **no Nitro server**, so the `/api/delibera/**` BFF route does not exist - there. The browser needs an absolute origin via `NUXT_PUBLIC_DELIBERA_API_BASE`, and that - origin must allow the Pages origin via CORS — which `Delibera.Server` does not do by default. - Unset, the site deploys and its requests 404: a UI preview, not an install. -- Nuxt 4 emits to `.output/public`, **not** `dist`. GitHub's own sample workflow still uploads - `./dist` because it predates Nuxt 4. +The Web UI is **not** deployed to Pages. It ships in the `techbuzzz/delibera-webui` container image +and is meant to run next to `Delibera.Server`, which provides the same-origin BFF route +`/api/delibera/**`. A static export has no Nitro server and therefore no BFF, so the UI would need +a separate CORS-enabled API origin — a deliberate owner decision, not something the Pages workflow +should configure silently. ### Reproduce the Pages build locally -`pages.yml` runs steps that PR CI does not: `nuxt generate`, and two assertions about the export. -That gap is worth closing by hand before every Pages-related change, because none of it can fail -a pull request: +`pages.yml` assembles `_site/` from `site/index.html` and `img/delibera-horizontal-1920x480.png`, +then substitutes `{{VERSION}}` from the csproj. That is a few shell commands: ```bash -cd src/Delibera.WebUI -npm ci -npm run generate # 6 routes -> .output/public -test -f .output/public/index.html # the gate in pages.yml -test -f .output/public/debates/new/index.html # prerender.routes actually took - -# the origin must be BAKED IN, not merely configured -NUXT_PUBLIC_DELIBERA_API_BASE=https://example.test npm run generate -grep -rqF 'https://example.test' .output/public # 0 hits = the whole feature is dead -``` - -That last `grep` is the check that matters. `NUXT_PUBLIC_*` is substituted only for names declared -under `runtimeConfig.public`; an undeclared one is dropped silently, the build stays green, and the -deployed site quietly calls its own origin. It has already happened once here. - -Note `npm run build` (not `generate`) before `npm test` — the SSE suite loads -`.output/server/index.mjs`, which a static export does not produce, and reports a suite failure -that looks like a broken test rather than a missing artefact. - -### Verify base-path runs from PowerShell, not Git Bash - -Git Bash rewrites an environment value that looks like a POSIX path when it exports it to a child -process. `NUXT_APP_BASE_URL=/Delibera npm run generate` reaches the build as -`C:/Program Files/Git/Delibera`. - -That malformed base produces exactly the output a real Nuxt bug would: `.output/public/index` -and `.output/public/debates/new` containing `Redirecting...` instead of documents, no `index.html` -anywhere, and a green build. I spent a cycle "fixing" nuxt.config.ts on the strength of that. - -Set these variables from PowerShell, where the value passes through untouched, before believing -anything about a base-path build: - -```powershell -$env:NUXT_APP_BASE_URL = '/Delibera' -npm run generate +mkdir -p _site/img +cp site/index.html _site/index.html +cp img/delibera-horizontal-1920x480.png _site/img/ +VERSION=$(sed -n 's/.*\([^<]*\)<\/Version>.*/\1/p' src/Delibera.Core/Delibera.Core.csproj | head -1) +sed -i "s/{{VERSION}}/$VERSION/g" _site/index.html +grep -q '{{VERSION}}' _site/index.html && echo "Unsubstituted placeholder" && exit 1 ``` --- diff --git a/site/index.html b/site/index.html new file mode 100644 index 0000000000000000000000000000000000000000..8e804e0a3723d9de4fef592a51cd501131ff9d32 GIT binary patch literal 19746 zcmds<$#UFB9mkt1RlLLCl)-__Xt5o~i7aruB(B7XEjgq(QIbZogtei?vg682@D?0+ z0g6LzTsiOpaN(L0Ckps}`a7-e|1$qr8p{!`DQjl>-^*`bd;H)3%!MC^+u`$YD?AFn z4iCbe&qxVP`n;}h zLyfa)MeqIM$vaxPp%ojNx1kd@^>kme_{3Q=1uV=sh~x93&8j|agk`<&X?8FCE&L_? zOYe_#-i~m29O<*BPn#OO8Sd#Xt5&pXU;lu1UsRlmC#QW)*>q?yYRBL;Y3g!7dQFJLh(SC5Rr8Jwe~oX$Sg#s51}rcTal^k8uH-k^)c% zcrqonBJPa$b!OBVbp|c!fCa}m2`5J)EVtHD=D4T zu4zY}R^{_*lpZ9X9SvVoDyQSD>pBu6s>ryXSRmJ`1EO+Dd@ zfjZ17@mtf5NjtGA=);sWW1!DhwZ5-2cJ;!F!qeyahu_?aa{6JEqiw-|(UQK~TLLdOXj{*!^0Xv*AdWnq~x%u;C^ZHk@_@3Y6Tv>Bz(97pvg zC6Xn9yGbh7M1SJ3z9byZlzP>qPgVBDQL5C2l5gy9+u4N8@6)*14-}ILI;yYWhNFPYhm;Jt0OB5ggse~Y<;rEl2LG?y+Def*wy+{_b&(0LOCWo zxx{DcSK%khBhr7Ne^skVFMrhjYDp+RB>J)6}{krt0Zfi=rF)OW_5e<@7adO>S zLsNS(D23O8Hc$@h0XN7blBe!#$LW#IGCxydpjAu4K1qS$8gI+X$fL{l;aR68JKb26 z^l7K>H9?)}Tf&|4YeQVps2``P`MB~F>t4wPuT45Ki1ywY z8Tl5{g@uJXz;2&{wZIp`mGl|kZI8|w=`%APYn}0W?^u6-8{>R@=bH9(G)$zEvyr-9 zl~|OtBF9Qrp`-&=DdSOUOI_ZZyh_v{hH=)7UpFF3iJy@1KaBTo=$HfC> za(UP3^rKz6ONl|6Paa>EJ2W$@>xwh3 zAN$v)Hki+xo4s(RNtH|QRU)f8500W%BrjWqifAKMSJR1Q&iIz3X*NRYitA}@P&CVTD2i=)^4}h_hQ>9KAa!2jY-;oEpV>xfk z-Ge#JqX&Tnb8UEk)T?q7oH}R88_j99Gq!#Cg_SthBU9R`+JV7#kwsFKaRt!*EUNiL ztIr~Jr)5QVBS+~RF~i-m+qK=AkdVvGpQ69zb)}Zf34%HWs<~PHbm(zQSe>7qmg}cP zRj#PH(}bi^@pt53?WdI#03R}~-ziR`tC#A5jMCwwD?yZ)tHbUhxckR!uk6M1=%rGX z;XHJWh%WWKIAt!Lz+PzbyeLWUbwe|;4coB`S?c<{)?^dZ@^ocy32nPHBYPbzNhJ zvLd{u8olnRxGUZjNk@98bSim=B^lfn^f}LPjoZ(m`BH=CZG33Alo{83Vbw;Trp2t| z?im9?P96%R$$mb78sO0 zS13#W68qH7-=-gN7sitM3$5G{jysZ~a^IgEkJ~Pztv6y;N&n@Jq^3@l+y_Rh6HLUP z<$33mB3MIoVk5i`dgRm2h8kIiknYTvi!o04Sa>cg&rW@MczH6c-97H&zt1=u>`Kdq zW%8{b>LjaUG@u^)fbEiKfMvK5KCSFf#p=H7_M;*v`P*^E0nsjnD}2yQxlx+@-C zSM};zP1n+F@_^|EbXZ{9)~fAki^`w_F+)42@I+_jW1KNB3s$@@QN)zI%AIgi(s*C* z1C0+OTz?F||G_`QpM&53B$)0*nBhDYK6#v8c<16bl}mqHs71M2|Ez8l$Gl&9VgFPsPoQC zH_$pFd;E`2X1oe-GZ#qJVWjTSNWzHgvfspm5Gg;=Uv3K#c_OLQhYl3G5`UJKjkpv! z@?6Kqz+_K^tXWok!_8ir8-qmC-08sE=XhoiaY>P56&tF_^97{FnlaPZ-Qs!eK+J{j zo!4Jtce?e&y}pl7$I!P2fivAXEaQq49dh$5MeB4~h^C3op~#-7m2c;nFWjC)`oVKG zLU>wO7ULeKSb_#vZYs8YAPKkqau&x~oJy9%`In^8Xt;R}1k>Nn}$FFJ8Y{`p?` zQ14`uzp3R@H^$^xiTBnQav!oG-x}pFbilfB-?PkZ;9ChX9A|ZcnA=rrT2;>ZF08nZ zhz;b15IlTYscFxTQuVIgW20}m4uEY%6RmC7Nc4g`MD8E)6ps7eR?>-NXPHL^$$m%? z_Ho##J(L=R?$D#cPID4+>)ODl;^KXcP~Tt|xIsn+J*qI5ngbW_6jqw7qHfExtSoES zbh0ZHr46PQeN87iy5d$sl^4`29%z50#Cd%?jW~m9DR|f#BFR{D==3al6Xw+Mz*bwo z&~mbNEH*2X#8Vgd-ca4VvK6QvirD%iu(a42=qrfh=H6N|OZ*d0|yU-Q`I$}iE zSlbgVGc?3c8`4;Cv?Z!*L&mUK)8d(Pi)yXz`;wPiqQXr{$Vbs*ey;aPq`?(rtPxT{ zeP}W%xGVZQ9=Bz~mO1h!`YP?4^Iz_bO-Fe_9-#1Hl$vZGv7F1=1Ib9gaKRH}K@2yR z)wYis;G4kXY!em~-8oanw@XF0I$kL~o_JL$S z-1Q^YAS)v#rjyVmdpv8aH`jH!z6am2GWjN#z2Mn+HruP*Nx_==hB$D%+5YRHbQiup zQKm?(fShy^iOu`b2}(QG`I~Hadhhm9cLf!3eoq|{d#o^MCL?7uiY;+Gu{Dx|Y9$C*Tq%P0DND|t3@boXl5#S~rYVwjm%{lO(Ma-c zDPw!CwWslQ=hq^nTA6iY5=nqd%^IBdos9qYq@DayG+SwSOI|=Ol(}5WNx}=5N=fi< zl&c!xT%VYfI}j-gNtL#gm3QI5vwD1UetB*hxVAw=X+LKQbDO{!T)s~-iB#Mdhw`3O zOsT6>Y28IxQ_*oEl@jWJ+{)oLg>%;rwI3L)OG1cSs26g3U@M+PL{R$wr2jy$r+LT+ zGZ@Eiq#DssJK+7@%fM!Ay-|_CBV{Iw`rZDbB)7ezck=4Qm(m^6|5aq8pO31P_-m@3 z*n6ahoCAC9D28Xx$L65f?z@&jTv&E**tg7LavC&8sBd*1v&XV zvJ74WB`jo5zB$-cRhh_tREe@3I!IpfyjebBI04S9eZwyCzY&ABfj!J}o2X z_8HlWbwS3nSM709pU3%v>vjW4DmmAbH2X9+mrlneXX5qBVfx=)QnW_w)jK%6iky5%2E$aTkTU&iQ}gNZFOm z+JPq_2I;!9c{*KK97Uu=Ba_kA_oOV@{*5M@gs0<106v>|D96)fpF3Aj=)R;bnB#B% zo`bHOt7Rw8pxf}OkMVuH=PZBaP0wcQT(EhefA+&pXEX*%2c zOvExo>BO#JaIg3dmGBF%7bc* Date: Sat, 10 Oct 2026 18:58:38 +0300 Subject: [PATCH 02/13] site: publish nested docs, restoring the work log and scope pages The first version of the site only walked `docs/*.md`, which silently dropped `docs/TASKS/` and `docs/scope/` - 9 committed, public documents. The previous landing page linked `docs/TASKS/README.md` ("Work log - open items by severity, including what is deliberately not fixed"), so losing them was a coverage regression, not a curation decision. The docs route is now a catch-all and the slug mirrors the directory layout: | File | Page | | ----------------------------- | ----------------------------------- | | docs/QuickStart.md | /docs/quickstart/ | | docs/TASKS/README.md | /docs/tasks/ | | docs/TASKS/W1-correctness.md | /docs/tasks/w1-correctness/ | | docs/scope/I-00-project-vision.md | /docs/scope/i-00-project-vision/ | A directory's README.md collapses to the directory itself, which is why the work log stays reachable at /docs/tasks/. The index groups by source directory - keying on the slug filed the work-log index under "Guides", because its slug has no path separator. Two description bugs fixed while re-reading the cards: - a description took only the first *line* of a paragraph, so a wrapped sentence was cut mid-clause ("...referenced Microsoft.Extensions.AI 10.8.0 and"). It now joins the whole paragraph and trims on a word boundary; - stripping `#` everywhere turned "C# 15" into "C 15". Only leading/trailing markers are heading syntax now; a `#` inside a word is part of it. docs/habr/ stays excluded: it is a draft for another platform carrying editorial notes about which headline to use, the same category as the gitignored .articles/. 36 pages, up from 27. typecheck, build and verify all green. --- .github/workflows/pages.yml | 84 +- CHANGELOG.md | 92 +- README-RU.md | 7 +- README.md | 8 +- docs/ReleaseProcess.md | 34 +- site/.gitignore | 20 + site/README.md | 120 +++ site/app/blog/[slug]/page.tsx | 75 ++ site/app/blog/page.tsx | 60 ++ site/app/changelog/page.tsx | 107 ++ site/app/docs/[...slug]/page.tsx | 110 ++ site/app/docs/page.tsx | 111 ++ site/app/globals.css | 729 ++++++++++++++ site/app/layout.tsx | 84 ++ site/app/measurements/page.tsx | 54 + site/app/not-found.tsx | 45 + site/app/page.tsx | 206 ++++ site/app/robots.ts | 23 + site/app/sitemap.ts | 33 + site/app/templates/page.tsx | 94 ++ site/components/layout.tsx | 102 ++ site/components/markdown.tsx | 54 + site/index.html | Bin 19746 -> 0 bytes site/lib/content.ts | 542 ++++++++++ site/lib/markdown.ts | 294 ++++++ site/lib/paths.ts | 40 + site/lib/routes.ts | 87 ++ site/lib/site.ts | 62 ++ site/next.config.ts | 30 + site/package-lock.json | 1623 ++++++++++++++++++++++++++++++ site/package.json | 34 + site/scripts/serve-export.ps1 | 51 + site/scripts/sync-assets.mjs | 24 + site/scripts/verify-export.mjs | 225 +++++ site/tsconfig.json | 44 + 35 files changed, 5246 insertions(+), 62 deletions(-) create mode 100644 site/.gitignore create mode 100644 site/README.md create mode 100644 site/app/blog/[slug]/page.tsx create mode 100644 site/app/blog/page.tsx create mode 100644 site/app/changelog/page.tsx create mode 100644 site/app/docs/[...slug]/page.tsx create mode 100644 site/app/docs/page.tsx create mode 100644 site/app/globals.css create mode 100644 site/app/layout.tsx create mode 100644 site/app/measurements/page.tsx create mode 100644 site/app/not-found.tsx create mode 100644 site/app/page.tsx create mode 100644 site/app/robots.ts create mode 100644 site/app/sitemap.ts create mode 100644 site/app/templates/page.tsx create mode 100644 site/components/layout.tsx create mode 100644 site/components/markdown.tsx delete mode 100644 site/index.html create mode 100644 site/lib/content.ts create mode 100644 site/lib/markdown.ts create mode 100644 site/lib/paths.ts create mode 100644 site/lib/routes.ts create mode 100644 site/lib/site.ts create mode 100644 site/next.config.ts create mode 100644 site/package-lock.json create mode 100644 site/package.json create mode 100644 site/scripts/serve-export.ps1 create mode 100644 site/scripts/sync-assets.mjs create mode 100644 site/scripts/verify-export.mjs create mode 100644 site/tsconfig.json diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index 26b8d9c..4169f14 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -1,17 +1,17 @@ name: pages -# Publishes site/index.html to GitHub Pages on every merge to main. +# Builds the Next.js site in site/ and publishes it to GitHub Pages on every merge to main. # -# The site is hand-written HTML, not a generated one: a static landing page does not justify a -# docs toolchain (MkDocs/Docusaurus) in a repo that has no Python. The one thing that must stay -# in sync is the version string, so it is injected here from the csproj that actually drives -# `dotnet pack` — meaning the page can never advertise a version that was not published. +# The site is a *view* over files that already live in this repository: the guides and +# release notes in docs/, the changelog, the version in Delibera.Core.csproj, and the debate +# templates in DebateTemplate.cs. Nothing is copied into a second location, so a page cannot +# advertise a version, a feature or a measurement the code does not have. # # The Nuxt Web UI is NOT deployed here. It ships in the container image # (techbuzzz/delibera-webui) and is meant to run next to Delibera.Server, which provides the -# same-origin BFF route /api/delibera/**. A static export has no Nitro server and therefore no -# BFF, so the UI would need a separate CORS-enabled API origin — a deliberate owner decision, -# not something this workflow should configure silently. +# same-origin BFF route /api/delibera/**. A static export has no Nitro server and therefore +# no BFF, so the UI would need a separate CORS-enabled API origin — a deliberate owner +# decision, not something this workflow should configure silently. on: push: @@ -30,53 +30,53 @@ concurrency: cancel-in-progress: true jobs: - deploy: - name: Build and deploy Pages + build: + name: Build runs-on: ubuntu-latest - environment: - name: github-pages - url: ${{ steps.deployment.outputs.page_url }} steps: - uses: actions/checkout@v4 - - name: Resolve version from the packable project - id: version - run: | - set -euo pipefail - # Read the version from the project that publish-nuget.yml packs. Do NOT read it from - # a tag or from CHANGELOG.md: those can disagree with what is on nuget.org, and a - # landing page claiming an unpublished version is worse than a stale one. - version="$(sed -n 's/.*\([^<]*\)<\/Version>.*/\1/p' src/Delibera.Core/Delibera.Core.csproj | head -1)" - if [ -z "$version" ]; then - echo "Could not resolve from src/Delibera.Core/Delibera.Core.csproj" >&2 - exit 1 - fi - echo "Resolved Delibera version: $version" - echo "value=$version" >> "$GITHUB_OUTPUT" + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + cache-dependency-path: site/package-lock.json + + - name: Install dependencies + run: npm ci + working-directory: site - name: Setup Pages id: pages uses: actions/configure-pages@v5 - - name: Assemble site - run: | - set -euo pipefail - mkdir -p _site/img - cp site/index.html _site/index.html - # The banner is the same asset the README uses — one file, two consumers. - cp img/delibera-horizontal-1920x480.png _site/img/ - sed -i "s/{{VERSION}}/${{ steps.version.outputs.value }}/g" _site/index.html - if grep -q '{{VERSION}}' _site/index.html; then - echo "Unsubstituted {{VERSION}} placeholder left in the built site" >&2 - exit 1 - fi - ls -la _site _site/img + - name: Build + run: npm run build + working-directory: site + env: + # configure-pages computes the prefix for this repo's Pages URL (/Delibera) and + # Next.js needs it at build time — it is baked into every emitted href. + PAGES_BASE_PATH: ${{ steps.pages.outputs.base_path }} + + - name: Verify the export + run: node scripts/verify-export.mjs + working-directory: site + env: + PAGES_BASE_PATH: ${{ steps.pages.outputs.base_path }} - name: Upload artifact uses: actions/upload-pages-artifact@v3 with: - path: _site + path: site/out + deploy: + name: Deploy + runs-on: ubuntu-latest + needs: build + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: - name: Deploy to GitHub Pages id: deployment - uses: actions/deploy-pages@v5 + uses: actions/deploy-pages@v4 \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 40695c3..5b42ec1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,52 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed — C# 15 where it compiles, and the two features that need .NET 11 + +`LangVersion=preview` was already set across the solution, but what that actually buys depended +on which SDK was installed: 10.0.112 ships Roslyn 5.0 and knows no C# 15 at all, while 10.0.401 +ships Roslyn 5.9. A `global.json` now pins `10.0.401` (`rollForward: latestFeature`) so a machine +without it cannot silently fail to compile. `Delibera.Redis` gained the `LangVersion=preview` the +other six source projects already had. + +Union types and closed hierarchies compile as keywords but are rejected on `net10.0` — they need +`System.Runtime.CompilerServices.IUnion` and `IsClosedTypeAttribute`, which ship with **.NET 11**. +They are deferred to the `net11.0` retarget along with the runtime work it unlocks. The full +feature gate is recorded in `docs/NET10-Upgrade.md`. + +What did ship is collection expression arguments on the paths that build a collection whose final +size is known: voting score tables, chunk lists, sentence-selection and deduplication buffers, and +the Knowledge Keeper's multi-corpus merge. Borda counting also stops materialising a sorted copy of +each ballot, and the semantic compressor no longer materialises a sorted copy of every sentence it +is about to iterate over. + +### Fixed — three per-request costs on the server + +- `ValidationFilter` called `typeof(IValidator<>).MakeGenericType(arg.GetType())` for every + argument of every request. Building a closed generic type allocates a runtime type; the mapping + is now cached per argument type. +- `RedisDebateOrchestrator` opened its own `ConnectionMultiplexer`, bypassing the singleton the + host registers — a second TCP pool, handshake and reconnect timers for the life of the process, + for a class whose own documentation claimed it shared the connection with `RedisDebateCache`. + It now reuses the injected multiplexer when there is one, and still opens its own when + constructed outside DI. +- The SSE writer emitted each frame as six writes plus a flush, and re-encoded the event name from + string to UTF-8 on every event. A frame is now assembled once in the response pipe and flushed + once, with event names as `u8` literals. Per-frame flushing is unchanged deliberately: a + buffering proxy once passed every functional test here while delivering rounds 150–200 seconds + late. + +### Added — regression tests for both + +`Validator_Type_Is_Resolved_Once_Per_Contract_Not_Per_Request` fails if the cached validator type +disappears. `Round_Is_Flushed_While_The_Debate_Is_Still_Running` withholds the terminal event and +requires the round to reach the wire within three seconds; it was verified to fail when the +per-frame flush is removed. + +`docs/performance-measurements.md` records the before/after measurement, including the result that +the framework-overhead benchmark shows **no change** — those paths are not exercised by that +workload, and the server-side figures were not measured at all in this pass. + ### Changed — GitHub Pages deploys the project website, not the Web UI `pages.yml` previously built and deployed the Nuxt Web UI as a static export. That was wrong: the @@ -16,9 +62,49 @@ no Nitro server and therefore no BFF, so the deployed site could only reach an A CORS-enabled origin — a deliberate owner decision, not something the workflow should configure silently. -Pages now publishes the hand-written landing page from `site/index.html` — header, install -instructions, documentation links, NuGet/Docker badges. The version string is injected from -`Delibera.Core.csproj` at build time, so the page cannot advertise an unpublished version. +Pages now publishes a real project site rather than a single hand-written page. + +**The site is a view over this repository, not a copy of it.** `site/` is a Next.js app +configured for `output: 'export'` (following +[nextjs/deploy-github-pages](https://github.com/nextjs/deploy-github-pages)). Every page is +rendered at build time from content that already lives here: + +| Page | Source | +| --- | --- | +| `/` | version from `Delibera.Core.csproj`, release cards from `docs/WhatsNew-*.md` | +| `/docs//` | every Markdown file under `docs/`, at any depth | +| `/blog//` | `docs/WhatsNew-*.md` | +| `/measurements/` | `docs/performance-measurements.md` | +| `/templates/` | `src/Delibera.Core/Templates/DebateTemplate.cs` | +| `/changelog/` | `CHANGELOG.md`, split per release | + +The docs route is a catch-all, so nested files keep their directory in the URL and a +directory's `README.md` collapses to the directory itself — `docs/TASKS/README.md` is +`/docs/tasks/`, the URL the previous landing page already linked to. The work log in +`docs/TASKS/` and the planning notes in `docs/scope/` are published for the same reason: +they are committed and public, and dropping them would be a coverage regression dressed up +as a curation decision. + +Consequences worth stating, because they are the point: + +- **The version cannot be wrong.** It is read from the csproj that `publish-nuget.yml` actually + packs — not from a tag (which can be pushed before the package reaches nuget.org) and not from + this changelog (which is edited by hand). If it cannot be resolved, the build fails. +- **The templates page cannot be stale.** Roles, strategy, chairman stance and round count are + extracted from `DebateTemplate.cs` at build time. If the parse finds nothing, the page says so + rather than showing a list that no longer matches the package. +- **Adding a Markdown file adds a page.** No route list to update, and every existing cross-link + to that file starts resolving. Repository-relative links (`../CHANGELOG.md`, + `docs/performance-measurements.md#3-context-compression`) are rewritten to site routes; targets + the site does not serve become absolute GitHub links. +- **A broken cross-link fails the deploy.** `scripts/verify-export.mjs` walks the export and fails + on an internal href with no file behind it, a missing `/Delibera` prefix, or a `#fragment` + pointing at a renamed heading. All three pass a green `next build` and 404 in production — + GitHub Pages is case-sensitive while every local check is not. + +Code blocks are highlighted with Shiki using GitHub's own TextMate grammars, in both light and dark +themes, following `prefers-color-scheme`. The site ships no CSS framework and no client-side +JavaScript beyond the Next.js runtime. ### Added — opt-in CORS for static browser clients diff --git a/README-RU.md b/README-RU.md index f69ae94..331396a 100644 --- a/README-RU.md +++ b/README-RU.md @@ -271,8 +271,11 @@ docker compose -f docker-compose.hub.yml up -d #### 🌐 Сайт проекта на GitHub Pages -Лендинг проекта публикуется на при каждом мердже в `main`. -Там ссылки на документацию, заметки о релизах, пакеты NuGet, образы Docker и исходный код. +Сайт проекта публикуется на при каждом мердже в `main`. +Он рендерит собственный Markdown этого репозитория — руководства из `docs/`, заметки о релизах, +отчёт о замерах, changelog и шаблоны дебатов, разобранные из `Delibera.Core.cs`, — поэтому +страница не может показывать версию, функцию или число, которых нет в коде. Подробности: +[`site/README.md`](site/README.md). Сам веб-интерфейс **там не публикуется** — он поставляется в образе [`techbuzzz/delibera-webui`](https://hub.docker.com/r/techbuzzz/delibera-webui) и предназначен для diff --git a/README.md b/README.md index ade4cb3..a64986e 100644 --- a/README.md +++ b/README.md @@ -270,9 +270,11 @@ docker compose -f docker-compose.hub.yml up -d #### 🌐 Project website on GitHub Pages -The project landing page is published at on every merge -to `main`. It links to the documentation, release notes, NuGet packages, Docker images and the -source repository. +The project site is published at on every merge to +`main`. It renders this repository's own Markdown — the guides in `docs/`, the release notes, the +measured-behaviour report, the changelog, and the debate templates parsed from +`Delibera.Core.cs` — so a page cannot advertise a version, a feature or a number the code does +not have. See [`site/README.md`](site/README.md). The Web UI itself is **not** deployed there — it ships in the [`techbuzzz/delibera-webui`](https://hub.docker.com/r/techbuzzz/delibera-webui) container image and diff --git a/docs/ReleaseProcess.md b/docs/ReleaseProcess.md index 2e26243..20c0829 100644 --- a/docs/ReleaseProcess.md +++ b/docs/ReleaseProcess.md @@ -175,10 +175,16 @@ curl -s -o /dev/null -w '%{http_code}\n' https://techbuzzz.github.io/Delibera/ A 404 there, served by GitHub.com itself, means nothing has ever been deployed to this Pages site. Only the Source dropdown in Settings says whether it is configured to accept deploys. -`pages.yml` deploys the **hand-written landing page** from `site/index.html` on every merge to -`main`. The version string is injected from `Delibera.Core.csproj` at build time, so the page -cannot advertise an unpublished version, and the build fails loudly on a leftover `{{VERSION}}` -placeholder. +`pages.yml` builds the **Next.js site** in `site/` and publishes the static export in `site/out` +on every merge to `main`. The site is a view over this repository: the guides and release notes in +`docs/`, `CHANGELOG.md`, and the debate templates in `DebateTemplate.cs`. Nothing is copied into a +second location, so a page cannot advertise a version, a feature or a measurement the code does not +have. + +The version is read from `Delibera.Core.csproj` at build time — the project `publish-nuget.yml` +actually packs. A tag can be pushed before the package reaches nuget.org and the changelog is edited +by hand, so neither is a trustworthy source. If the version cannot be resolved the build fails +rather than publishing a placeholder. The Web UI is **not** deployed to Pages. It ships in the `techbuzzz/delibera-webui` container image and is meant to run next to `Delibera.Server`, which provides the same-origin BFF route @@ -188,18 +194,22 @@ should configure silently. ### Reproduce the Pages build locally -`pages.yml` assembles `_site/` from `site/index.html` and `img/delibera-horizontal-1920x480.png`, -then substitutes `{{VERSION}}` from the csproj. That is a few shell commands: +The workflow runs `npm ci`, then `next build` with `PAGES_BASE_PATH` set to the base path +`actions/configure-pages` computes for this repo (`/Delibera`). Set the same variable locally or the +export will differ from production: ```bash -mkdir -p _site/img -cp site/index.html _site/index.html -cp img/delibera-horizontal-1920x480.png _site/img/ -VERSION=$(sed -n 's/.*\([^<]*\)<\/Version>.*/\1/p' src/Delibera.Core/Delibera.Core.csproj | head -1) -sed -i "s/{{VERSION}}/$VERSION/g" _site/index.html -grep -q '{{VERSION}}' _site/index.html && echo "Unsubstituted placeholder" && exit 1 +cd site +npm ci +PAGES_BASE_PATH=/Delibera npm run build # writes out/ +npm run verify # every internal link, asset and anchor resolves ``` +`npm run verify` is not decoration. A green `next build` proves the pages compiled, not that they +link to each other, and three failure modes only appear on the live site: an internal href with no +emitted file behind it, a missing `/Delibera` prefix, and a `#fragment` pointing at a renamed +heading. `verify` fails the deploy on all three. + --- ## Changing a workflow diff --git a/site/.gitignore b/site/.gitignore new file mode 100644 index 0000000..4a7178f --- /dev/null +++ b/site/.gitignore @@ -0,0 +1,20 @@ +# dependencies +node_modules/ + +# next.js build output +.next/ +out/ + +# Brand images copied from ../img by scripts/sync-assets.mjs - never committed twice. +public/img/ + +# typescript +next-env.d.ts +*.tsbuildinfo + +# local env files +.env*.local + +# OS +.DS_Store +Thumbs.db \ No newline at end of file diff --git a/site/README.md b/site/README.md new file mode 100644 index 0000000..e84ff68 --- /dev/null +++ b/site/README.md @@ -0,0 +1,120 @@ +# Delibera website + +The GitHub Pages site: + +A [Next.js](https://nextjs.org) app configured for `output: 'export'`, following +[nextjs/deploy-github-pages](https://github.com/nextjs/deploy-github-pages). It is a *view* +over files that already live in this repository — there is no CMS and no second copy of any +content. + +## What it publishes + +| Page | Source | +| --- | --- | +| `/` | Version from `Delibera.Core.csproj`, release cards from `docs/WhatsNew-*.md` | +| `/docs//` | Every Markdown file under `docs/`, at any depth | +| `/blog//` | `docs/WhatsNew-*.md` | +| `/measurements/` | `docs/performance-measurements.md` | +| `/templates/` | `src/Delibera.Core/Templates/DebateTemplate.cs` | +| `/changelog/` | `CHANGELOG.md`, split per release | + +Content is read at build time. Adding a Markdown file to `docs/` adds a page — there is no +route list to update. + +### Nested documents + +The docs route is a catch-all, so the directory layout is mirrored in the URL: + +| File | Page | +| --- | --- | +| `docs/QuickStart.md` | `/docs/quickstart/` | +| `docs/TASKS/README.md` | `/docs/tasks/` | +| `docs/TASKS/W1-correctness.md` | `/docs/tasks/w1-correctness/` | +| `docs/scope/I-00-project-vision.md` | `/docs/scope/i-00-project-vision/` | + +A directory's `README.md` collapses to the directory itself, which is why the work log is +reachable at `/docs/tasks/` — the URL the previous landing page already pointed at. + +Excluded, with the reason in `listDocPages()`: release notes (they are under `/blog/`), +`performance-measurements.md` (under `/measurements/`), `v10.2.6.md` (a duplicate of its +release note), and `docs/habr/` — a draft being written for Habr that still carries editorial +notes about which headline to use. Same category as `.articles/`, which is gitignored. + +### Why the version comes from the csproj + +`src/Delibera.Core/Delibera.Core.csproj` is the input to `dotnet pack` in +`publish-nuget.yml`. A git tag can be pushed before the package reaches nuget.org, and +CHANGELOG.md is edited by hand; either can disagree with what consumers can actually install. +The csproj cannot. If the version cannot be resolved, the build **fails** rather than +publishing a page that advertises an unpublished version. + +### Why the templates page parses C# + +The roles, strategy, chairman stance and round count on `/templates/` are extracted from +`DebateTemplate.cs` at build time. A hand-written list would be a second place to update on +every template change — and this project documents cases where a claim drifted from what the +code actually did. If the parse finds no templates, the page says so instead of showing a +stale list. + +## Commands + +```bash +npm ci +npm run dev # http://localhost:3000 +npm run build # static export to out/ +npm run verify # check every link/asset/anchor in out/ +npm run typecheck +``` + +`npm run build` writes to `out/`, which is what `actions/upload-pages-artifact` publishes. + +## Deployment + +`.github/workflows/pages.yml` runs on every merge to `main`: + +1. `npm ci` in `site/` +2. `actions/configure-pages` computes the base path (`/Delibera`) and passes it as + `PAGES_BASE_PATH` +3. `npm run build` +4. `npm run verify` — fails the deploy on a broken internal link +5. upload `site/out` and deploy + +### Why `npm run verify` exists + +A green `next build` proves the pages compiled, not that they link to each other. These +failures only show up on the live site: + +- an internal href that no emitted file matches; +- a missing `/Delibera` prefix (Next adds `basePath` to `` and its own chunks, but + **not** to a raw `` or a metadata URL — see `lib/paths.ts`); +- a `#fragment` pointing at a heading that was renamed; +- a route still written in the old mixed case. GitHub Pages is case-sensitive; Windows and + local dev servers are not, so this passes locally and 404s in production. + +## Layout + +``` +app/ routes: /, /docs, /blog, /measurements, /templates, /changelog +components/ layout shell, markdown body, table of contents +lib/content.ts reads docs/, CHANGELOG.md, the csproj, DebateTemplate.cs +lib/markdown.ts marked + Shiki, heading anchors, repository-link rewriting +lib/routes.ts repo path -> site route (single source of truth) +lib/paths.ts basePath helpers for raw and metadata URLs +scripts/ asset sync, export verification +``` + +## Adding a page + +Drop a Markdown file into `docs/`. It appears in `/docs//`, in the sitemap, and every +existing cross-link to it starts resolving. + +Release notes are special-cased by filename: `docs/WhatsNew-v10.5.3.md` publishes to +`/blog/10.5.3/`. A `-RU` suffix gets its own URL rather than overwriting the English page. + +## Not deployed here + +The Nuxt Web UI (`src/Delibera.WebUI`) ships as the container image +`techbuzzz/delibera-webui` and runs next to `Delibera.Server`, which provides the +same-origin BFF route `/api/delibera/**`. A static export has no Nitro server and therefore +no BFF — the UI would need a separate CORS-enabled API origin, which is an owner decision +rather than something a deploy workflow should configure silently. \ No newline at end of file diff --git a/site/app/blog/[slug]/page.tsx b/site/app/blog/[slug]/page.tsx new file mode 100644 index 0000000..d9db6e1 --- /dev/null +++ b/site/app/blog/[slug]/page.tsx @@ -0,0 +1,75 @@ +import { notFound } from 'next/navigation' +import type { Metadata } from 'next' +import { getVersion, listReleaseNotes, loadDoc } from '@/lib/content' +import { buildRouteIndex, releaseRoute } from '@/lib/routes' +import { renderMarkdown } from '@/lib/markdown' +import { Breadcrumbs, PageHead, PageShell } from '@/components/layout' +import { DocLayout } from '@/components/markdown' +import { githubBlob } from '@/lib/site' + +export const dynamicParams = false + +export async function generateStaticParams() { + const notes = await listReleaseNotes() + return notes.map((note) => ({ slug: note.slug })) +} + +export async function generateMetadata({ + params, +}: { + params: Promise<{ slug: string }> +}): Promise { + const { slug } = await params + const note = (await listReleaseNotes()).find((candidate) => candidate.slug === slug) + + if (!note) return { title: 'Not found' } + + return { + title: note.title, + description: note.description, + alternates: { canonical: releaseRoute(note.slug) }, + } +} + +export default async function ReleaseNotePage({ params }: { params: Promise<{ slug: string }> }) { + const { slug } = await params + const version = await getVersion() + + const note = (await listReleaseNotes()).find((candidate) => candidate.slug === slug) + if (!note) notFound() + + const { markdown } = await loadDoc(note.source) + const [routes] = await Promise.all([buildRouteIndex()]) + const { html, headings } = await renderMarkdown(markdown, { + source: note.source, + routes, + stripPreamble: true, + }) + + return ( + + {/* No visible description: the release note opens with that same sentence. The + extracted description is still used for the page metadata above. */} + + + + + +
+

+ Source:{' '} + + {note.source} + {' '} + on GitHub. +

+
+
+ ) +} \ No newline at end of file diff --git a/site/app/blog/page.tsx b/site/app/blog/page.tsx new file mode 100644 index 0000000..c6853aa --- /dev/null +++ b/site/app/blog/page.tsx @@ -0,0 +1,60 @@ +import Link from 'next/link' +import type { Metadata } from 'next' +import { getVersion, getReleases, byVersionDescending, isReleased, listReleaseNotes } from '@/lib/content' +import { PageShell, PageHead } from '@/components/layout' +import { releaseRoute, normalisedReleaseSlug } from '@/lib/routes' + +export const metadata: Metadata = { + title: 'Release notes', + description: 'What changed in every Delibera release, newest first.', +} + +/** + * Release-note index. + * + * Reads `docs/WhatsNew-*.md` directly rather than the changelog: these are the long-form + * notes written for each release, and their descriptions come from the documents themselves. + * The changelog has its own page. + */ +export default async function BlogIndexPage() { + const version = await getVersion() + const notes = await listReleaseNotes() + const releases = (await getReleases()).sort(byVersionDescending) + + const latestReleased = releases.find(isReleased) + + return ( + + + +
+
+ {latestReleased ? ( +

+ Latest published release: v{latestReleased.version} + {latestReleased.date ? ` (${latestReleased.date})` : null}. +

+ ) : null} + +
+ {notes.map((note) => ( +
+

+ {note.title} +

+ {note.description ?

{note.description}

: null} +
+ v{normalisedReleaseSlug(note.slug)} + {note.lang === 'ru' ? RU : null} +
+
+ ))} +
+
+
+
+ ) +} \ No newline at end of file diff --git a/site/app/changelog/page.tsx b/site/app/changelog/page.tsx new file mode 100644 index 0000000..53c4209 --- /dev/null +++ b/site/app/changelog/page.tsx @@ -0,0 +1,107 @@ +import type { Metadata } from 'next' +import Link from 'next/link' +import { + getVersion, + getReleases, + byVersionDescending, + isReleased, + listReleaseNotes, +} from '@/lib/content' +import { PageShell, PageHead } from '@/components/layout' +import { buildRouteIndex, releaseRoute, normalisedReleaseSlug, CHANGELOG_ROUTE } from '@/lib/routes' +import { renderMarkdown } from '@/lib/markdown' +import { githubBlob } from '@/lib/site' + +export const metadata: Metadata = { + title: 'Changelog', + description: 'Complete change history for Delibera, from 10.1.0 onwards.', +} + +/** + * The changelog, split per release so each version gets an anchor instead of one + * 23 KB wall of text. + * + * Read straight from `CHANGELOG.md` and rendered through the same pipeline as the guides, so + * a `## [10.5.2]` section here is byte-identical to the one in the repository file. + */ +export default async function ChangelogPage() { + const version = await getVersion() + const releases = (await getReleases()).sort(byVersionDescending) + const [routes] = await Promise.all([buildRouteIndex()]) + + const rendered = await Promise.all( + releases.map(async (release) => { + const { html } = await renderMarkdown(release.body, { + source: 'CHANGELOG.md', + routes, + }) + + return { release, html } + }), + ) + + const published = rendered.filter(({ release }) => isReleased(release)).length + + // Not every released version has a long-form note: the changelog goes back to 10.1.0, + // while `docs/WhatsNew-*.md` starts at 10.2.6. Link only where a note actually exists — + // otherwise the card is a 404 on a page that exists to be the complete record. + const notedVersions = new Set( + (await listReleaseNotes()).map((note) => normalisedReleaseSlug(note.slug)), + ) + + return ( + + + +
+
+ {rendered.map(({ release, html }) => ( +
+

+ v{release.version} + {release.date ? ( + + {' '} + — {release.date} + + ) : null} +

+ {notedVersions.has(release.version) ? ( +

+ {/* `Link`, not `a`: only the router-aware component gets `basePath` + applied automatically. A raw anchor would point at /blog/... and 404 + on the deployed site under /Delibera. */} + + Read the long-form notes for v{release.version} + +

+ ) : null} +
+
+ ))} +
+ + +
+
+ ) +} \ No newline at end of file diff --git a/site/app/docs/[...slug]/page.tsx b/site/app/docs/[...slug]/page.tsx new file mode 100644 index 0000000..2efac67 --- /dev/null +++ b/site/app/docs/[...slug]/page.tsx @@ -0,0 +1,110 @@ +import { notFound } from 'next/navigation' +import type { Metadata } from 'next' +import { getVersion, listDocPages, loadDoc } from '@/lib/content' +import { buildRouteIndex } from '@/lib/routes' +import { renderMarkdown } from '@/lib/markdown' +import { Breadcrumbs, PageHead, PageShell } from '@/components/layout' +import { DocLayout } from '@/components/markdown' +import { githubBlob } from '@/lib/site' + +/** + * One page per Markdown file under `docs/`, at any depth. + * + * The catch-all `[...slug]` is what lets `docs/TASKS/W1-correctness.md` publish to + * `/docs/tasks/w1-correctness/` and `docs/TASKS/README.md` publish to `/docs/tasks/`. + * A single-segment `[slug]` route cannot express that, and the work log was previously + * reachable from the landing page. + * + * `dynamicParams = false` is deliberate: under `output: 'export'` an unlisted parameter would + * have no server to render it on request, and Next.js fails the build rather than emitting a + * page that 404s. The document list is therefore the contract — a new file under `docs/` must + * be published to appear on the site, which is exactly the guarantee that stops a link from + * silently pointing at something that was never exported. + */ +export const dynamicParams = false + +export async function generateStaticParams() { + const pages = await listDocPages() + return pages.map((page) => ({ slug: page.slug.split('/') })) +} + +export async function generateMetadata({ + params, +}: { + params: Promise<{ slug: string[] }> +}): Promise { + const { slug } = await params + const wanted = slug.join('/') + const page = (await listDocPages()).find((candidate) => candidate.slug === wanted) + + if (!page) return { title: 'Not found' } + + return { + title: page.title, + description: page.description, + alternates: { canonical: `/docs/${page.slug}/` }, + } +} + +export default async function DocPage({ params }: { params: Promise<{ slug: string[] }> }) { + const { slug } = await params + const wanted = slug.join('/') + const version = await getVersion() + + const pages = await listDocPages() + const page = pages.find((candidate) => candidate.slug === wanted) + + if (!page) notFound() + + const { markdown } = await loadDoc(page.source) + const [routes] = await Promise.all([buildRouteIndex()]) + const { html, headings } = await renderMarkdown(markdown, { + source: page.source, + routes, + stripPreamble: true, + }) + + // Breadcrumbs mirror the directory: `/docs/tasks/w1-correctness/` yields + // Home / Docs / Work log / W1 — Correctness. + // + // The group label is plain text, not a link to `/docs//`. Only `docs/TASKS/` has an + // index file; `docs/scope/` has none, so a link there would be a 404 on a breadcrumb. + const segments = page.slug.split('/') + const crumbs: Array<{ href?: string; label: string }> = [ + { href: '/', label: 'Home' }, + { href: '/docs/', label: 'Docs' }, + ...(segments.length > 1 ? [{ label: sectionLabel(segments[0] ?? '') }] : []), + { label: page.title }, + ] + + return ( + + {/* No visible description: the document opens with that same sentence. The extracted + description is still used for the page metadata above. */} + + + + + +
+

+ Source:{' '} + + {page.source} + {' '} + on GitHub — edit it there and this page updates on the next deploy. +

+
+
+ ) +} + +/** Directory segment -> the heading the docs index uses for that group. */ +function sectionLabel(segment: string): string { + const labels: Record = { + tasks: 'Work log', + scope: 'Scope', + } + + return labels[segment] ?? segment +} \ No newline at end of file diff --git a/site/app/docs/page.tsx b/site/app/docs/page.tsx new file mode 100644 index 0000000..d35763c --- /dev/null +++ b/site/app/docs/page.tsx @@ -0,0 +1,111 @@ +import Link from 'next/link' +import type { Metadata } from 'next' +import { getVersion, listDocPages } from '@/lib/content' +import { PageShell, PageHead } from '@/components/layout' +import { docRoute } from '@/lib/routes' + +export const metadata: Metadata = { + title: 'Documentation', + description: + 'Guides for Delibera: quick start, server hosting, distributed debates, caching, the work log and the release history.', +} + +/** Documents are grouped by the directory they live in; top-level files form their own group. */ +interface Group { + key: string + title: string + hint: string + pages: Awaited> +} + +const GROUP_META: Record = { + guides: { + title: 'Guides', + hint: 'How to use Delibera: setup, hosting, distribution, caching and providers.', + }, + tasks: { + title: 'Work log', + hint: 'Open items by severity, including what is deliberately not fixed. Updated as work lands.', + }, + scope: { + title: 'Scope', + hint: 'Planning notes behind the stabilisation effort — hypotheses, and the breaking-change inventory.', + }, +} + +export default async function DocsIndexPage() { + const version = await getVersion() + const pages = await listDocPages() + + const groups = new Map() + for (const page of pages) { + // Group by the *directory* the file lives in, not by its slug. `docs/TASKS/README.md` + // collapses to the slug `tasks` (a directory with no path separator), and keying on the + // slug would file the work-log index under "Guides" alongside the user documentation. + const relative = page.source.replace(/^docs\//, '') + const inDirectory = relative.includes('/') + const key = inDirectory ? (relative.split('/')[0] ?? 'guides').toLowerCase() : 'guides' + + const group = groups.get(key) ?? { + key, + ...(GROUP_META[key] ?? { title: key, hint: '' }), + pages: [], + } + group.pages.push(page) + groups.set(key, group) + } + + // Guides first, then the supporting material, so the index reads top-down by importance. + const order = ['guides', 'tasks', 'scope'] + const ordered = [...groups.values()].sort( + (a, b) => order.indexOf(a.key) - order.indexOf(b.key), + ) + + return ( + + + +
+
+ {ordered.map((group, index) => { + const english = group.pages.filter((page) => page.lang === 'en') + const russian = group.pages.filter((page) => page.lang === 'ru') + + return ( +
+

{group.title}

+ {group.hint ?

{group.hint}

: null} + +
+ {english.map((page) => ( + +
{page.title}
+ {page.description ?
{page.description}
: null} + + ))} +
+ + {russian.length > 0 ? ( + <> +

Русский

+
+ {russian.map((page) => ( + +
{page.title}
+ {page.description ?
{page.description}
: null} + + ))} +
+ + ) : null} +
+ ) + })} +
+
+
+ ) +} \ No newline at end of file diff --git a/site/app/globals.css b/site/app/globals.css new file mode 100644 index 0000000..484c01f --- /dev/null +++ b/site/app/globals.css @@ -0,0 +1,729 @@ +/** + * Site stylesheet. + * + * Hand-written rather than Tailwind: this is a documentation site with six page templates + * and one accent colour, and a build-time-only site has no reason to carry a CSS toolchain. + * Both colour schemes come from `prefers-color-scheme`, so a reader never has to find a + * theme switch. + */ + +:root { + --bg: #ffffff; + --bg-soft: #f6f8fa; + --bg-inset: #eef1f4; + --border: #d0d7de; + --fg: #1f2328; + --fg-dim: #59636e; + --accent: #0969da; + --accent-soft: #ddf4ff; + --warn-bg: #fff8c5; + --warn-border: #d4a72c; + --warn-fg: #7d4e00; + --radius: 10px; + --mono: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, monospace; + --sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; + --wrap: 1080px; + --measure: 74ch; +} + +@media (prefers-color-scheme: dark) { + :root { + --bg: #0d1117; + --bg-soft: #161b22; + --bg-inset: #010409; + --border: #30363d; + --fg: #e6edf3; + --fg-dim: #9198a1; + --accent: #58a6ff; + --accent-soft: #121d2f; + --warn-bg: #272115; + --warn-border: #9e6a03; + --warn-fg: #e3b341; + } +} + +* { + box-sizing: border-box; +} + +html { + -webkit-text-size-adjust: 100%; + scroll-behavior: smooth; + scroll-padding-top: 88px; +} + +body { + margin: 0; + background: var(--bg); + color: var(--fg); + font: 16px/1.65 var(--sans); + overflow-wrap: break-word; +} + +a { + color: var(--accent); + text-decoration: none; +} + +a:hover { + text-decoration: underline; +} + +:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 2px; + border-radius: 4px; +} + +.skip-link { + position: absolute; + left: -9999px; + top: 0; + background: var(--accent); + color: #fff; + padding: 10px 16px; + z-index: 100; +} + +.skip-link:focus { + left: 8px; + top: 8px; +} + +.wrap { + max-width: var(--wrap); + margin: 0 auto; + padding: 0 24px; +} + +.wrap-narrow { + max-width: var(--measure); + margin: 0 auto; + padding: 0 24px; +} + +/* ---------------------------------------------------------------- site header */ + +.site-header { + position: sticky; + top: 0; + z-index: 20; + background: color-mix(in srgb, var(--bg) 88%, transparent); + backdrop-filter: blur(8px); + border-bottom: 1px solid var(--border); +} + +.site-header__inner { + display: flex; + align-items: center; + gap: 20px; + min-height: 60px; +} + +.site-header__brand { + display: flex; + align-items: center; + gap: 10px; + font-weight: 600; + color: var(--fg); + font-size: 1rem; +} + +.site-header__brand img { + height: 28px; + width: auto; +} + +.site-nav { + display: flex; + gap: 4px; + margin-left: auto; + flex-wrap: wrap; +} + +.site-nav a { + color: var(--fg-dim); + padding: 6px 10px; + border-radius: 6px; + font-size: 0.925rem; +} + +.site-nav a:hover { + color: var(--fg); + background: var(--bg-soft); + text-decoration: none; +} + +.site-nav a[aria-current='page'] { + color: var(--fg); + font-weight: 600; +} + +@media (max-width: 720px) { + .site-header__inner { + flex-direction: column; + align-items: flex-start; + gap: 8px; + padding-top: 12px; + padding-bottom: 12px; + } + + .site-nav { + margin-left: 0; + } +} + +/* ------------------------------------------------------------------- sections */ + +main { + min-height: 60vh; +} + +.hero { + padding: 64px 0 44px; + text-align: center; + border-bottom: 1px solid var(--border); +} + +.hero img { + width: 100%; + max-width: 560px; + height: auto; +} + +.hero h1 { + font-size: clamp(1.9rem, 4vw, 2.6rem); + margin: 0 0 8px; + letter-spacing: -0.02em; +} + +.hero p { + color: var(--fg-dim); + font-size: 1.05rem; + max-width: 720px; + margin: 0 auto; +} + +section.band { + padding: 48px 0; + border-bottom: 1px solid var(--border); +} + +section.band:last-of-type { + border-bottom: 0; +} + +h2.band__title { + font-size: 1.45rem; + margin: 0 0 4px; + letter-spacing: -0.01em; +} + +.band__hint { + color: var(--fg-dim); + margin: 0 0 22px; + font-size: 0.95rem; +} + +.page-head { + padding: 44px 0 8px; + border-bottom: 1px solid var(--border); + margin-bottom: 32px; +} + +.page-head h1 { + font-size: clamp(1.7rem, 3.4vw, 2.2rem); + margin: 0 0 6px; + letter-spacing: -0.02em; +} + +.page-head p { + color: var(--fg-dim); + margin: 0; + max-width: var(--measure); +} + +.breadcrumbs { + font-size: 0.85rem; + color: var(--fg-dim); + padding-top: 18px; +} + +.breadcrumbs a { + color: var(--fg-dim); +} + +.breadcrumbs a:hover { + color: var(--accent); +} + +/* ---------------------------------------------------------------------- cards */ + +.cards { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); + gap: 14px; +} + +.card { + display: block; + background: var(--bg-soft); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 16px 18px; + color: inherit; +} + +a.card:hover { + border-color: var(--accent); + text-decoration: none; +} + +.card .t { + font-weight: 600; + margin-bottom: 4px; +} + +.card .d { + color: var(--fg-dim); + font-size: 0.925rem; + line-height: 1.5; +} + +.card .meta { + display: block; + margin-top: 8px; + font-size: 0.8rem; + color: var(--fg-dim); + font-family: var(--mono); +} + +/* --------------------------------------------------------------------- badges */ + +.badges { + display: flex; + flex-wrap: wrap; + gap: 8px; + justify-content: center; + margin-top: 22px; +} + +.badges img { + height: 20px; +} + +.pill { + display: inline-block; + font-family: var(--mono); + font-size: 0.8rem; + padding: 2px 8px; + border-radius: 999px; + border: 1px solid var(--border); + background: var(--bg-soft); + color: var(--fg-dim); +} + +.pill--accent { + border-color: var(--accent); + color: var(--accent); + background: var(--accent-soft); +} + +/* ---------------------------------------------------------------------- prose */ + +.prose { + max-width: var(--measure); +} + +.prose > *:first-child { + margin-top: 0; +} + +.prose h1, +.prose h2, +.prose h3, +.prose h4 { + letter-spacing: -0.01em; + line-height: 1.3; + margin: 32px 0 10px; + scroll-margin-top: 88px; +} + +.prose h1 { + font-size: 1.9rem; +} + +.prose h2 { + font-size: 1.35rem; + padding-bottom: 6px; + border-bottom: 1px solid var(--border); +} + +.prose h3 { + font-size: 1.12rem; +} + +.prose h4 { + font-size: 1rem; + color: var(--fg-dim); +} + +.md-heading { + position: relative; +} + +.md-anchor { + margin-left: 8px; + color: var(--fg-dim); + opacity: 0; + text-decoration: none; + font-weight: 400; +} + +.md-heading:hover .md-anchor, +.md-anchor:focus-visible { + opacity: 1; +} + +.prose p { + margin: 0 0 16px; +} + +.prose ul, +.prose ol { + margin: 0 0 16px; + padding-left: 24px; +} + +.prose li { + margin-bottom: 6px; +} + +.prose li > p { + margin-bottom: 8px; +} + +.prose img { + max-width: 100%; + height: auto; +} + +.prose hr { + border: 0; + border-top: 1px solid var(--border); + margin: 32px 0; +} + +.prose blockquote { + margin: 0 0 16px; + padding: 2px 16px; + border-left: 3px solid var(--border); + color: var(--fg-dim); +} + +.prose :not(pre) > code { + font-family: var(--mono); + font-size: 0.875em; + background: var(--bg-soft); + border: 1px solid var(--border); + border-radius: 5px; + padding: 0.12em 0.36em; +} + +.prose table { + display: block; + width: max-content; + max-width: 100%; + overflow-x: auto; + border-collapse: collapse; + margin: 0 0 20px; + font-size: 0.94rem; +} + +.prose th, +.prose td { + border: 1px solid var(--border); + padding: 7px 13px; + text-align: left; + vertical-align: top; +} + +.prose th { + background: var(--bg-soft); + font-weight: 600; +} + +.prose tr:nth-child(2n) td { + background: color-mix(in srgb, var(--bg-soft) 55%, transparent); +} + +/* ---------------------------------------------------------------- code blocks */ + +.md-code { + margin: 0 0 20px; + border: 1px solid var(--border); + border-radius: var(--radius); + overflow: hidden; +} + +.md-code pre, +.prose pre { + margin: 0; + padding: 14px 16px; + overflow-x: auto; + font-family: var(--mono); + font-size: 0.83rem; + line-height: 1.55; + background: var(--bg-soft); +} + +.prose pre { + border: 1px solid var(--border); + border-radius: var(--radius); +} + +.md-code pre code { + background: none; + border: 0; + padding: 0; + font-size: inherit; +} + +.md-code .line { + display: inline-block; + width: 100%; +} + +/* Shiki dual themes: light is the default, dark overrides it under a dark preference. */ +@media (prefers-color-scheme: dark) { + .shiki, + .shiki span { + color: var(--shiki-dark) !important; + background-color: var(--shiki-dark-bg) !important; + font-style: var(--shiki-dark-font-style) !important; + font-weight: var(--shiki-dark-font-weight) !important; + text-decoration: var(--shiki-dark-text-decoration) !important; + } +} + +/* -------------------------------------------------------------------- callout */ + +.callout { + border: 1px solid var(--warn-border); + border-left-width: 4px; + border-radius: var(--radius); + background: var(--warn-bg); + color: var(--warn-fg); + padding: 14px 18px; + margin: 0 0 24px; + font-size: 0.95rem; +} + +.callout strong { + color: inherit; +} + +.callout > *:last-child { + margin-bottom: 0; +} + +/* --------------------------------------------------------------- table of contents */ + +.toc { + position: sticky; + top: 88px; + align-self: start; + max-height: calc(100vh - 110px); + overflow-y: auto; + font-size: 0.88rem; + border-left: 2px solid var(--border); + padding-left: 14px; +} + +.toc h2 { + font-size: 0.78rem; + text-transform: uppercase; + letter-spacing: 0.06em; + color: var(--fg-dim); + margin: 0 0 8px; +} + +.toc ol { + list-style: none; + margin: 0; + padding: 0; +} + +.toc li { + margin: 0 0 4px; +} + +.toc li[data-depth='3'] { + padding-left: 12px; +} + +.toc li[data-depth='4'] { + padding-left: 24px; +} + +.toc a { + color: var(--fg-dim); +} + +.toc a:hover { + color: var(--accent); +} + +.doc-layout { + display: grid; + grid-template-columns: minmax(0, 1fr) 240px; + gap: 44px; + align-items: start; + padding-bottom: 64px; +} + +@media (max-width: 1000px) { + .doc-layout { + grid-template-columns: minmax(0, 1fr); + gap: 28px; + } + + .toc { + position: static; + max-height: none; + border-left: 0; + border-top: 2px solid var(--border); + padding: 14px 0 0; + order: -1; + } +} + +/* ---------------------------------------------------------------- post listing */ + +.post-list { + display: grid; + gap: 14px; +} + +.post { + background: var(--bg-soft); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 18px 20px; +} + +.post h3 { + margin: 0 0 6px; + font-size: 1.12rem; +} + +.post h3 a { + color: var(--fg); +} + +.post h3 a:hover { + color: var(--accent); + text-decoration: none; +} + +.post p { + margin: 0 0 10px; + color: var(--fg-dim); + font-size: 0.94rem; +} + +.post-meta { + display: flex; + gap: 10px; + align-items: center; + font-size: 0.8rem; + color: var(--fg-dim); + font-family: var(--mono); +} + +/* -------------------------------------------------------------------- template */ + +.tpl { + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 18px 20px; + background: var(--bg-soft); +} + +.tpl h3 { + margin: 0 0 4px; + font-size: 1.08rem; + font-family: var(--mono); +} + +.tpl .role-list { + display: flex; + flex-wrap: wrap; + gap: 6px; + margin: 12px 0; +} + +.tpl dl { + display: grid; + grid-template-columns: max-content 1fr; + gap: 4px 14px; + margin: 0; + font-size: 0.9rem; +} + +.tpl dt { + color: var(--fg-dim); +} + +.tpl dd { + margin: 0; + font-family: var(--mono); +} + +.tpl p.summary { + color: var(--fg-dim); + font-size: 0.92rem; + margin: 10px 0 0; +} + +/* ---------------------------------------------------------------------- footer */ + +.site-footer { + border-top: 1px solid var(--border); + padding: 32px 0 48px; + color: var(--fg-dim); + font-size: 0.875rem; +} + +.site-footer__inner { + display: flex; + flex-wrap: wrap; + gap: 8px 20px; + align-items: center; + justify-content: space-between; +} + +.site-footer nav { + display: flex; + gap: 16px; + flex-wrap: wrap; +} + +.site-footer a { + color: var(--fg-dim); +} + +.site-footer a:hover { + color: var(--accent); +} + +@media (prefers-reduced-motion: reduce) { + html { + scroll-behavior: auto; + } + + * { + animation-duration: 0.01ms !important; + transition-duration: 0.01ms !important; + } +} \ No newline at end of file diff --git a/site/app/layout.tsx b/site/app/layout.tsx new file mode 100644 index 0000000..94a59f1 --- /dev/null +++ b/site/app/layout.tsx @@ -0,0 +1,84 @@ +import type { ReactNode } from 'react' +import { brandImage, siteOrigin } from '@/lib/paths' +import './globals.css' + +// Two different rules apply to the two kinds of URL below, which is the whole trap: +// +// - `openGraph` / `twitter` images are *joined* onto `metadataBase` path-wise by Next, so +// they must NOT already contain the base path — passing it yields `.../Delibera/Delibera/…`. +// - `icons` are emitted verbatim, so they MUST already contain it, or the favicon 404s on +// a Pages deploy where the site is served from a subdirectory. +// +// See scripts/verify-export.mjs, which fails the build on either mistake. +const BANNER = '/img/delibera-horizontal-1920x480.png' +const FAVICON = brandImage('delibera-favicon-1024x1024.png') + +/** + * Site-wide metadata. + * + * `metadataBase` is required for any relative URL (OpenGraph images, canonical links) to + * resolve against an absolute origin — Next.js otherwise emits a warning and drops the tag. + * It is derived from the deployment origin *including* the base path, so a canonical link + * emitted for `/docs/quickstart/` comes out as + * `https://techbuzzz.github.io/Delibera/docs/quickstart/` rather than dropping the prefix. + */ +export const metadata = { + metadataBase: new URL(siteOrigin('/')), + title: { + default: 'Delibera — Thoughtful AI Decisions', + template: '%s · Delibera', + }, + description: + 'Collective decision making through structured AI deliberation — multi-model councils with RAG, pgvector, Knowledge Keeper, MCP tools and live SSE streaming.', + applicationName: 'Delibera', + authors: [{ name: 'Delibera' }], + keywords: [ + 'AI', + 'LLM', + 'multi-agent', + 'deliberation', + '.NET', + 'C#', + 'RAG', + 'pgvector', + 'Qdrant', + 'MCP', + ], + openGraph: { + type: 'website', + siteName: 'Delibera', + title: 'Delibera — Thoughtful AI Decisions', + description: + 'Multi-model AI councils with a chairman, RAG knowledge keeper, MCP tools and live streaming.', + images: [{ url: BANNER, width: 1920, height: 480 }], + }, + twitter: { + card: 'summary_large_image', + title: 'Delibera — Thoughtful AI Decisions', + description: 'Multi-model AI councils with a chairman, RAG, MCP tools and live streaming.', + images: [BANNER], + }, + icons: { + icon: [{ url: FAVICON, type: 'image/png' }], + }, +} + +export const viewport = { + themeColor: [ + { media: '(prefers-color-scheme: light)', color: '#ffffff' }, + { media: '(prefers-color-scheme: dark)', color: '#0d1117' }, + ], +} + +export default function RootLayout({ children }: { children: ReactNode }) { + return ( + + + + Skip to content + + {children} + + + ) +} \ No newline at end of file diff --git a/site/app/measurements/page.tsx b/site/app/measurements/page.tsx new file mode 100644 index 0000000..628e236 --- /dev/null +++ b/site/app/measurements/page.tsx @@ -0,0 +1,54 @@ +import type { Metadata } from 'next' +import { getVersion, getMeasurements } from '@/lib/content' +import { buildRouteIndex, MEASUREMENTS_ROUTE } from '@/lib/routes' +import { renderMarkdown } from '@/lib/markdown' +import { Breadcrumbs, PageHead, PageShell } from '@/components/layout' +import { DocLayout } from '@/components/markdown' +import { githubBlob } from '@/lib/site' + +export const metadata: Metadata = { + title: 'Measured behaviour', + description: + 'Latency, throughput and token costs for Delibera, measured against a live provider with the harness that produced them.', +} + +/** + * The measured-behaviour report gets its own top-level page rather than living under /docs. + * + * The project treats "measured" as a claim with obligations attached — numbers are reported + * as observed, and a documented claim that measurement contradicted is flagged rather than + * quietly updated. Giving it a distinct URL keeps that contract visible. + */ +export default async function MeasurementsPage() { + const version = await getVersion() + const report = await getMeasurements() + + const [routes] = await Promise.all([buildRouteIndex()]) + const { html, headings } = await renderMarkdown(report.markdown, { + source: report.source, + routes, + stripPreamble: true, + }) + + return ( + + {/* No visible description: the report opens with that same sentence, and showing it + twice reads as a rendering bug. It still feeds the page metadata below. */} + + + + + +
+

+ Source:{' '} + + {report.source} + {' '} + on GitHub. Re-run the harnesses described at the bottom of the report to reproduce + these numbers. Route: {MEASUREMENTS_ROUTE} +

+
+
+ ) +} \ No newline at end of file diff --git a/site/app/not-found.tsx b/site/app/not-found.tsx new file mode 100644 index 0000000..04fa1e9 --- /dev/null +++ b/site/app/not-found.tsx @@ -0,0 +1,45 @@ +import Link from 'next/link' +import { getVersion } from '@/lib/content' +import { PageShell } from '@/components/layout' + +/** + * 404 page. + * + * Under `output: 'export'` Next.js emits this as a static `404.html`, which GitHub Pages + * serves for any unmatched path. It links back to the sections most likely to be what a + * visitor was reaching for — a renamed deep link is the common case. + */ +export default async function NotFoundPage() { + const version = await getVersion() + + return ( + +
+
+

404

+

+ That page is not here. It may have moved when the documentation moved from GitHub + onto this site. +

+

+ + Home + {' '} + + Docs + {' '} + + Releases + {' '} + + Measurements + {' '} + + Changelog + +

+
+
+
+ ) +} \ No newline at end of file diff --git a/site/app/page.tsx b/site/app/page.tsx new file mode 100644 index 0000000..9dae5f7 --- /dev/null +++ b/site/app/page.tsx @@ -0,0 +1,206 @@ +import Link from 'next/link' +import { getVersion, getLatestDocumentedVersion, listReleaseNotes, listDocPages } from '@/lib/content' +import { PageShell } from '@/components/layout' +import { + DOCKER_IMAGES, + NUGET_PACKAGES, + NUGET_URL, + REPO_URL, + RELEASES_URL, +} from '@/lib/site' +import { releaseRoute, docRoute } from '@/lib/routes' +import { brandImage } from '@/lib/paths' + +/** + * Landing page. + * + * Built from the same data the rest of the site uses: the version comes from the packable + * csproj, the release cards from `docs/WhatsNew-*.md`, the documentation cards from the + * documents themselves. Nothing here is a hand-maintained copy of the README, so the page + * cannot drift away from what the repository actually contains. + */ +export default async function HomePage() { + const version = await getVersion() + const latestDocumented = await getLatestDocumentedVersion() + const allNotes = await listReleaseNotes() + + // English notes only on the English home page. Taking the first three of a mixed list + // means a Russian translation of an old release can displace the actual latest three. + const notes = allNotes.filter((note) => note.lang === 'en').slice(0, 3) + + // Featured order is an editorial choice, but membership and links come from the real + // document list — a renamed or deleted guide drops off the front page on its own instead + // of leaving a card pointing at a 404. + const featuredOrder = ['quickstart', 'server', 'distributed-debates', 'caching'] + const guides = await listDocPages() + const featuredDocs = featuredOrder + .map((slug) => guides.find((page) => page.slug === slug)) + .filter((page): page is NonNullable => page !== undefined) + + return ( + +
+
+ {/* eslint-disable-next-line @next/next/no-img-element */} + Delibera +

Thoughtful AI Decisions

+

+ Collective decision making through structured AI deliberation — multiple models + reason through a question across rounds, critique each other’s answers, and a{' '} + Chairman weighs the arguments into a final verdict. RAG via + Qdrant or PostgreSQL/pgvector, MCP tools, live SSE streaming. +

+

+ v{version}{' '} + + Release notes + {' '} + .NET 10 + MIT +

+ +

+ English ·{' '} + Русский +

+
+
+ +
+
+

Install

+

+ Three NuGet packages and two Docker images, published on every release. +

+ +

NuGet

+
+            {NUGET_PACKAGES.map((p) => `dotnet add package ${p.id}`).join('\n')}
+          
+ +

Docker Hub — no build required

+
+            {`curl -O https://raw.githubusercontent.com/techbuzzz/Delibera/main/deploy/docker-compose.hub.yml
+export DELIBERA_VERSION=${version}   # omit to track: latest
+docker compose -f docker-compose.hub.yml up -d`}
+          
+ +
+

+ The API is unauthenticated and unthrottled, and a debate spends real LLM + credits. Every published port binds to 127.0.0.1. Put an + authenticating reverse proxy in front before exposing this stack to a network you + do not control. +

+
+
+
+ +
+
+

What’s new

+

+ Latest release: v{latestDocumented}. + Every number below was measured, not estimated — see{' '} + measured behaviour. +

+
+ {notes.map((note) => ( + +
{note.title}
+
{note.description}
+ + ))} +
+

+ All release notes · Changelog +

+
+
+ +
+
+

Documentation

+

Start here, then go deeper.

+
+ {featuredDocs.map((page) => ( + +
{page.title}
+
{page.description}
+ + ))} + +
Debate templates
+
Six built-in councils, parsed from the code that ships in the package.
+ + +
Measured behaviour
+
Latency, throughput and token costs with the harness that produced them.
+ +
+

+ All documentation +

+
+
+ +
+ +
+
+ ) +} \ No newline at end of file diff --git a/site/app/robots.ts b/site/app/robots.ts new file mode 100644 index 0000000..3e3497a --- /dev/null +++ b/site/app/robots.ts @@ -0,0 +1,23 @@ +import type { MetadataRoute } from 'next' +import { siteOrigin } from '@/lib/paths' + +// See app/sitemap.ts — required under `output: 'export'`. +export const dynamic = 'force-static' + +/** + * Deliberately permissive. + * + * The site's content is public documentation, and the previous behaviour was to let anyone + * reach it. The one thing this does *not* do is advertise an API surface — the Nuxt Web UI + * is deployed as a container next to `Delibera.Server`, never from this repository's Pages + * site, and nothing here links to a live instance. + * + * `Sitemap` is absolute because a crawler cannot resolve a relative one against an origin it + * has not been told about. + */ +export default function robots(): MetadataRoute.Robots { + return { + rules: [{ userAgent: '*', allow: '/' }], + sitemap: siteOrigin('/sitemap.xml'), + } +} \ No newline at end of file diff --git a/site/app/sitemap.ts b/site/app/sitemap.ts new file mode 100644 index 0000000..6dbbd78 --- /dev/null +++ b/site/app/sitemap.ts @@ -0,0 +1,33 @@ +import type { MetadataRoute } from 'next' +import { listDocPages, listReleaseNotes } from '@/lib/content' +import { docRoute, releaseRoute, MEASUREMENTS_ROUTE, TEMPLATES_ROUTE, CHANGELOG_ROUTE } from '@/lib/routes' +import { siteOrigin } from '@/lib/paths' + +// `output: 'export'` has no server to revalidate anything, so every route must declare +// itself static or the build fails while collecting page data. +export const dynamic = 'force-static' + +/** + * Sitemap for the static export. + * + * Next.js emits `sitemap.xml` during `next build` under `output: 'export'`. Entries are + * absolute: the sitemap protocol requires a full URL, and a bare `/docs/quickstart/` tells a + * crawler nothing about the origin. The deployment prefix has to be part of that URL too — + * `siteOrigin()` carries the `/Delibera` base path, so a route is never published under the + * wrong root. + * + * Pages are listed without `lastModified`: `git log` is not reliably available in the Pages + * checkout, and a timestamp derived from the checkout time would be uniformly wrong. + */ +export default async function sitemap(): Promise { + const docs = await listDocPages() + const notes = await listReleaseNotes() + + const staticRoutes = ['/', '/docs/', '/blog/', MEASUREMENTS_ROUTE, TEMPLATES_ROUTE, CHANGELOG_ROUTE] + + return [ + ...staticRoutes.map((route) => ({ url: siteOrigin(route) })), + ...docs.map((page) => ({ url: siteOrigin(docRoute(page.slug)) })), + ...notes.map((note) => ({ url: siteOrigin(releaseRoute(note.slug)) })), + ] +} \ No newline at end of file diff --git a/site/app/templates/page.tsx b/site/app/templates/page.tsx new file mode 100644 index 0000000..21e6821 --- /dev/null +++ b/site/app/templates/page.tsx @@ -0,0 +1,94 @@ +import type { Metadata } from 'next' +import { getVersion, getDebateTemplates } from '@/lib/content' +import { PageShell, PageHead } from '@/components/layout' +import { githubBlob } from '@/lib/site' + +export const metadata: Metadata = { + title: 'Debate templates', + description: + 'The built-in Delibera councils — roles, strategy and round count, parsed from DebateTemplate.cs.', +} + +/** + * Built-in debate templates, read out of `DebateTemplate.cs`. + * + * The roles, strategy, chairman stance and round count on this page are extracted from the + * source that ships inside `Delibera.Core`. A hand-written list would be a second place to + * update every time a template changes — and this project has a documented history of + * claims drifting from what the code actually does. + */ +export default async function TemplatesPage() { + const version = await getVersion() + const templates = await getDebateTemplates() + + return ( + + + +
+
+

+ Each template is a pre-configured CouncilBuilder: participants, strategy + and persona prompts are set for you, and every With* call still applies. +

+ + {templates.length === 0 ? ( +
+

+ No templates were found in src/Delibera.Core/Templates/DebateTemplate.cs. + The page says so rather than showing a stale list — see{' '} + the source. +

+
+ ) : ( +
+ {templates.map((template) => ( +
+

{template.name}

+
+ {template.members.map((member) => ( + + {member} + + ))} +
+
+
Strategy
+
{template.strategy}
+
Chairman
+
{template.chairman}
+
Rounds
+
{template.maxRounds}
+
+ {template.summary ?

{template.summary}

: null} +
+ ))} +
+ )} + +

+ Using one +

+
+            {`var executor = DebateTemplate.${templates[0]?.name ?? 'Custom'}
+    .WithQuestion("Should we migrate to event-driven architecture?")
+    .WithProvider(provider)
+    .WithMaxRounds(4)
+    .Build();
+
+var result = await executor.ExecuteAsync(cancellationToken);`}
+          
+ +

+ DebateTemplate.Custom() is the escape hatch — it returns a fresh{' '} + CouncilBuilder with no presets. Full API:{' '} + Quick Start. +

+
+
+
+ ) +} \ No newline at end of file diff --git a/site/components/layout.tsx b/site/components/layout.tsx new file mode 100644 index 0000000..23cd471 --- /dev/null +++ b/site/components/layout.tsx @@ -0,0 +1,102 @@ +import Link from 'next/link' +import type { ReactNode } from 'react' +import { REPO_URL } from '@/lib/site' +import { brandImage } from '@/lib/paths' + +const NAV = [ + { href: '/docs/', label: 'Docs' }, + { href: '/blog/', label: 'Releases' }, + { href: '/measurements/', label: 'Measurements' }, + { href: '/templates/', label: 'Templates' }, + { href: '/changelog/', label: 'Changelog' }, +] + +export function SiteHeader({ current }: { current?: string }) { + return ( +
+
+ + {/* eslint-disable-next-line @next/next/no-img-element */} + Delibera + + +
+
+ ) +} + +export function SiteFooter({ version }: { version: string }) { + return ( +
+
+
+ Delibera {version} · MIT · Repository ·{' '} + Issues +
+ +
+
+ ) +} + +export interface PageShellProps { + children: ReactNode + version: string + current?: string +} + +export function PageShell({ children, version, current }: PageShellProps) { + return ( + <> + +
{children}
+ + + ) +} + +export function PageHead({ title, description }: { title: string; description?: string }) { + return ( +
+
+

{title}

+ {description ?

{description}

: null} +
+
+ ) +} + +export function Breadcrumbs({ items }: { items: Array<{ href?: string; label: string }> }) { + return ( +
+ +
+ ) +} \ No newline at end of file diff --git a/site/components/markdown.tsx b/site/components/markdown.tsx new file mode 100644 index 0000000..41bbed5 --- /dev/null +++ b/site/components/markdown.tsx @@ -0,0 +1,54 @@ +import type { RenderResult } from '@/lib/markdown' + +/** + * Rendered Markdown document plus its table of contents. + * + * The HTML is inserted with `dangerouslySetInnerHTML` from repository-owned Markdown that + * this build already parsed. There is no runtime input path here: `output: 'export'` means + * these strings are baked into static files at build time. + */ +export function MarkdownBody({ html }: { html: string }) { + return
+} + +/** + * On-page table of contents. + * + * Built from the headings captured during rendering, so an entry can never point at an + * anchor that does not exist. Restricted to `h2` and `h3`: `h4` and deeper make the list a + * wall rather than a wayfinding aid, and `h1` is the document title, which the page header + * already shows. + */ +export function TableOfContents({ headings }: { headings: RenderResult['headings'] }) { + const items = headings.filter((heading) => heading.depth === 2 || heading.depth === 3) + + if (items.length < 2) return null + + return ( + + ) +} + +export function DocLayout({ + html, + headings, +}: { + html: string + headings: RenderResult['headings'] +}) { + return ( +
+ + +
+ ) +} \ No newline at end of file diff --git a/site/index.html b/site/index.html deleted file mode 100644 index 8e804e0a3723d9de4fef592a51cd501131ff9d32..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 19746 zcmds<$#UFB9mkt1RlLLCl)-__Xt5o~i7aruB(B7XEjgq(QIbZogtei?vg682@D?0+ z0g6LzTsiOpaN(L0Ckps}`a7-e|1$qr8p{!`DQjl>-^*`bd;H)3%!MC^+u`$YD?AFn z4iCbe&qxVP`n;}h zLyfa)MeqIM$vaxPp%ojNx1kd@^>kme_{3Q=1uV=sh~x93&8j|agk`<&X?8FCE&L_? zOYe_#-i~m29O<*BPn#OO8Sd#Xt5&pXU;lu1UsRlmC#QW)*>q?yYRBL;Y3g!7dQFJLh(SC5Rr8Jwe~oX$Sg#s51}rcTal^k8uH-k^)c% zcrqonBJPa$b!OBVbp|c!fCa}m2`5J)EVtHD=D4T zu4zY}R^{_*lpZ9X9SvVoDyQSD>pBu6s>ryXSRmJ`1EO+Dd@ zfjZ17@mtf5NjtGA=);sWW1!DhwZ5-2cJ;!F!qeyahu_?aa{6JEqiw-|(UQK~TLLdOXj{*!^0Xv*AdWnq~x%u;C^ZHk@_@3Y6Tv>Bz(97pvg zC6Xn9yGbh7M1SJ3z9byZlzP>qPgVBDQL5C2l5gy9+u4N8@6)*14-}ILI;yYWhNFPYhm;Jt0OB5ggse~Y<;rEl2LG?y+Def*wy+{_b&(0LOCWo zxx{DcSK%khBhr7Ne^skVFMrhjYDp+RB>J)6}{krt0Zfi=rF)OW_5e<@7adO>S zLsNS(D23O8Hc$@h0XN7blBe!#$LW#IGCxydpjAu4K1qS$8gI+X$fL{l;aR68JKb26 z^l7K>H9?)}Tf&|4YeQVps2``P`MB~F>t4wPuT45Ki1ywY z8Tl5{g@uJXz;2&{wZIp`mGl|kZI8|w=`%APYn}0W?^u6-8{>R@=bH9(G)$zEvyr-9 zl~|OtBF9Qrp`-&=DdSOUOI_ZZyh_v{hH=)7UpFF3iJy@1KaBTo=$HfC> za(UP3^rKz6ONl|6Paa>EJ2W$@>xwh3 zAN$v)Hki+xo4s(RNtH|QRU)f8500W%BrjWqifAKMSJR1Q&iIz3X*NRYitA}@P&CVTD2i=)^4}h_hQ>9KAa!2jY-;oEpV>xfk z-Ge#JqX&Tnb8UEk)T?q7oH}R88_j99Gq!#Cg_SthBU9R`+JV7#kwsFKaRt!*EUNiL ztIr~Jr)5QVBS+~RF~i-m+qK=AkdVvGpQ69zb)}Zf34%HWs<~PHbm(zQSe>7qmg}cP zRj#PH(}bi^@pt53?WdI#03R}~-ziR`tC#A5jMCwwD?yZ)tHbUhxckR!uk6M1=%rGX z;XHJWh%WWKIAt!Lz+PzbyeLWUbwe|;4coB`S?c<{)?^dZ@^ocy32nPHBYPbzNhJ zvLd{u8olnRxGUZjNk@98bSim=B^lfn^f}LPjoZ(m`BH=CZG33Alo{83Vbw;Trp2t| z?im9?P96%R$$mb78sO0 zS13#W68qH7-=-gN7sitM3$5G{jysZ~a^IgEkJ~Pztv6y;N&n@Jq^3@l+y_Rh6HLUP z<$33mB3MIoVk5i`dgRm2h8kIiknYTvi!o04Sa>cg&rW@MczH6c-97H&zt1=u>`Kdq zW%8{b>LjaUG@u^)fbEiKfMvK5KCSFf#p=H7_M;*v`P*^E0nsjnD}2yQxlx+@-C zSM};zP1n+F@_^|EbXZ{9)~fAki^`w_F+)42@I+_jW1KNB3s$@@QN)zI%AIgi(s*C* z1C0+OTz?F||G_`QpM&53B$)0*nBhDYK6#v8c<16bl}mqHs71M2|Ez8l$Gl&9VgFPsPoQC zH_$pFd;E`2X1oe-GZ#qJVWjTSNWzHgvfspm5Gg;=Uv3K#c_OLQhYl3G5`UJKjkpv! z@?6Kqz+_K^tXWok!_8ir8-qmC-08sE=XhoiaY>P56&tF_^97{FnlaPZ-Qs!eK+J{j zo!4Jtce?e&y}pl7$I!P2fivAXEaQq49dh$5MeB4~h^C3op~#-7m2c;nFWjC)`oVKG zLU>wO7ULeKSb_#vZYs8YAPKkqau&x~oJy9%`In^8Xt;R}1k>Nn}$FFJ8Y{`p?` zQ14`uzp3R@H^$^xiTBnQav!oG-x}pFbilfB-?PkZ;9ChX9A|ZcnA=rrT2;>ZF08nZ zhz;b15IlTYscFxTQuVIgW20}m4uEY%6RmC7Nc4g`MD8E)6ps7eR?>-NXPHL^$$m%? z_Ho##J(L=R?$D#cPID4+>)ODl;^KXcP~Tt|xIsn+J*qI5ngbW_6jqw7qHfExtSoES zbh0ZHr46PQeN87iy5d$sl^4`29%z50#Cd%?jW~m9DR|f#BFR{D==3al6Xw+Mz*bwo z&~mbNEH*2X#8Vgd-ca4VvK6QvirD%iu(a42=qrfh=H6N|OZ*d0|yU-Q`I$}iE zSlbgVGc?3c8`4;Cv?Z!*L&mUK)8d(Pi)yXz`;wPiqQXr{$Vbs*ey;aPq`?(rtPxT{ zeP}W%xGVZQ9=Bz~mO1h!`YP?4^Iz_bO-Fe_9-#1Hl$vZGv7F1=1Ib9gaKRH}K@2yR z)wYis;G4kXY!em~-8oanw@XF0I$kL~o_JL$S z-1Q^YAS)v#rjyVmdpv8aH`jH!z6am2GWjN#z2Mn+HruP*Nx_==hB$D%+5YRHbQiup zQKm?(fShy^iOu`b2}(QG`I~Hadhhm9cLf!3eoq|{d#o^MCL?7uiY;+Gu{Dx|Y9$C*Tq%P0DND|t3@boXl5#S~rYVwjm%{lO(Ma-c zDPw!CwWslQ=hq^nTA6iY5=nqd%^IBdos9qYq@DayG+SwSOI|=Ol(}5WNx}=5N=fi< zl&c!xT%VYfI}j-gNtL#gm3QI5vwD1UetB*hxVAw=X+LKQbDO{!T)s~-iB#Mdhw`3O zOsT6>Y28IxQ_*oEl@jWJ+{)oLg>%;rwI3L)OG1cSs26g3U@M+PL{R$wr2jy$r+LT+ zGZ@Eiq#DssJK+7@%fM!Ay-|_CBV{Iw`rZDbB)7ezck=4Qm(m^6|5aq8pO31P_-m@3 z*n6ahoCAC9D28Xx$L65f?z@&jTv&E**tg7LavC&8sBd*1v&XV zvJ74WB`jo5zB$-cRhh_tREe@3I!IpfyjebBI04S9eZwyCzY&ABfj!J}o2X z_8HlWbwS3nSM709pU3%v>vjW4DmmAbH2X9+mrlneXX5qBVfx=)QnW_w)jK%6iky5%2E$aTkTU&iQ}gNZFOm z+JPq_2I;!9c{*KK97Uu=Ba_kA_oOV@{*5M@gs0<106v>|D96)fpF3Aj=)R;bnB#B% zo`bHOt7Rw8pxf}OkMVuH=PZBaP0wcQT(EhefA+&pXEX*%2c zOvExo>BO#JaIg3dmGBF%7bc* { + return fs.readFile(path.join(REPO_ROOT, ...segments), 'utf8') +} + +async function exists(...segments: string[]): Promise { + try { + await fs.access(path.join(REPO_ROOT, ...segments)) + return true + } catch { + return false + } +} + +/** + * The version every page renders. + * + * Read from the project that `publish-nuget.yml` actually packs, not from a git tag and not + * from CHANGELOG.md. A tag can be pushed before the package reaches nuget.org, and the + * changelog is edited by hand; both can disagree with what consumers can install. The csproj + * cannot — it is the input to `dotnet pack`. + * + * Failing loudly is deliberate. A placeholder silently rendered as "v0.0.0" on the front page + * of a published site is a worse outcome than a failed deploy. + */ +export async function getVersion(): Promise { + const csproj = await readText('src', 'Delibera.Core', 'Delibera.Core.csproj') + const match = csproj.match(/([^<]+)<\/Version>/) + + if (!match?.[1]) { + throw new Error( + 'Could not resolve from src/Delibera.Core/Delibera.Core.csproj. ' + + 'The site refuses to publish an unverified version.', + ) + } + + return match[1] +} + +export interface Release { + /** `10.5.2`, or `unreleased` for the working-set section. */ + version: string + /** ISO date when the changelog records one; older entries only carry the year. */ + date: string | null + /** Raw Markdown body of the release section. */ + body: string +} + +/** + * Parse `## [10.5.2] - 2026-10-09` headings out of CHANGELOG.md. + * + * The changelog is the release ledger and the site reads it rather than duplicating it. Older + * entries are dated `2026` only, which is why `date` is nullable: rendering a fabricated + * day-of-month would be worse than showing the year alone. + */ +export async function getReleases(): Promise { + const changelog = await readText('CHANGELOG.md') + const header = /^## \[([^\]]+)\](?:\s+-\s+(\S+))?\s*$/gm + + const releases: Release[] = [] + const matches = [...changelog.matchAll(header)] + + for (const [index, match] of matches.entries()) { + const bodyStart = (match.index ?? 0) + match[0].length + const bodyEnd = matches[index + 1]?.index ?? changelog.length + + const version = (match[1] ?? '').trim() + const rawDate = match[2]?.trim() + + releases.push({ + version: version.toLowerCase() === 'unreleased' ? 'unreleased' : version, + date: rawDate && /^\d{4}-\d{2}-\d{2}$/.test(rawDate) ? rawDate : null, + body: changelog.slice(bodyStart, bodyEnd).trim(), + }) + } + + return releases +} + +/** True for `10.5.2`, false for `unreleased` and for `0.0.0`-style placeholders. */ +export function isReleased(release: Release): boolean { + return release.version !== 'unreleased' +} + +/** Semantic-version compare, newest first. `unreleased` always sorts to the top. */ +export function byVersionDescending(a: Release, b: Release): number { + if (a.version === 'unreleased') return -1 + if (b.version === 'unreleased') return 1 + + const pa = a.version.split('.').map(Number) + const pb = b.version.split('.').map(Number) + + for (let i = 0; i < 3; i += 1) { + const diff = (pb[i] ?? 0) - (pa[i] ?? 0) + if (diff !== 0) return diff + } + + return 0 +} + +/** + * The version whose release note the "what's new" cards link to. + * + * Resolved against what actually exists on disk rather than against `getVersion()`: a + * release may ship the binary before its note is written, and a card pointing at a + * not-yet-existing page is a 404 on the front page of the site. + */ +export async function getLatestDocumentedVersion(): Promise { + const version = await getVersion() + if (await exists('docs', `WhatsNew-v${version}.md`)) return version + + // Walk down the patch line until a note exists (10.5.2 -> 10.5.1 -> ...). + const parts = version.split('.').map(Number) + for (let patch = (parts[2] ?? 0) - 1; patch >= 0; patch -= 1) { + const candidate = [...parts.slice(0, 2), patch].join('.') + if (await exists('docs', `WhatsNew-v${candidate}.md`)) return candidate + } + + const documented = await listReleaseNotes() + const newest = documented[0] + + return (newest && versionFromFileName(newest.source)) ?? version +} + +export interface DocPage { + slug: string + /** Repository-relative POSIX path, e.g. `docs/QuickStart.md`. */ + source: string + title: string + /** One-paragraph summary pulled from the first paragraph after the title. */ + description: string + /** `ru` for files suffixed `-RU`, otherwise `en`. */ + lang: 'en' | 'ru' + /** Milliseconds since epoch, from the file's mtime. Zero when unknown (static checkout). */ + updatedAt: number +} + +/** Strip Markdown/HTML decoration so a heading reads as a title, not as markup. */ +function plainText(markdown: string): string { + return markdown + .replace(/<[^>]+>/g, ' ') + .replace(/!\[([^\]]*)\]\([^)]*\)/g, '$1') + .replace(/\[([^\]]*)\]\([^)]*\)/g, '$1') + .replace(/`([^`]*)`/g, '$1') + .split('\n') + .map((line) => + line + // Heading markers only where they are markup: a leading `##` or a closing `##`. + // A `#` in the middle of a word is part of it — `C# 15`, `F#`, `#1` — and stripping + // it turns the project's headline language name into "C 15". + .replace(/^\s*#{1,6}\s*/, '') + .replace(/\s*#+\s*$/, '') + .replace(/[*_~>]/g, ''), + ) + .join(' ') + .replace(/\s+/g, ' ') + .trim() +} + +/** + * Pull the document title and a one-line description out of Markdown. + * + * Three shapes have to be handled, all of them present in `docs/`: + * + * - `QuickStart.md` opens with an HTML banner and puts its `#` title on line 5, so + * "line one is the title" does not hold; + * - release notes and the roadmap open with a `>` status block — `**Release type:** minor…` + * — which is metadata about the document, not prose a card should show; + * - the work-log entries open with a `**Problem.** …` paragraph that wraps across several + * source lines, followed by a table. Taking the first line alone yields a card reading + * "Delibera.Core referenced Microsoft.Extensions.AI 10.8.0 and". + * + * So: the first ATX heading is the title, and the description is the first whole *paragraph* + * after it — skipping block quotes, HTML, tables, lists and headings, and joining the wrapped + * lines before trimming. + */ +export function extractHeadings(markdown: string): { title: string; description: string } { + const lines = markdown.split(/\r?\n/) + let title = '' + let titleIndex = -1 + + for (const [index, line] of lines.entries()) { + const match = line.match(/^#\s+(.+?)\s*#*\s*$/) + if (match?.[1]) { + title = plainText(match[1]) + titleIndex = index + break + } + } + + /** Line that opens a different block, so the current paragraph has ended. */ + const startsBlock = (text: string): boolean => + /^#{1,6}\s/.test(text) || + text.startsWith('>') || + text.startsWith('<') || + text.startsWith('|') || + text === '---' || + /^([-*+]\s|\d+\.\s)/.test(text) + + let description = '' + + for (let index = titleIndex + 1; index < lines.length; index += 1) { + const text = (lines[index] ?? '').trim() + + if (!text) continue + if (startsBlock(text)) continue + + // Collect the whole paragraph, so a wrapped sentence is not cut mid-clause. + const paragraph: string[] = [] + while (index < lines.length) { + const current = (lines[index] ?? '').trim() + if (!current || startsBlock(current)) break + paragraph.push(current) + index += 1 + } + + const plain = plainText(paragraph.join(' ')) + + // A line that is essentially only a link (plus an emoji or a short flag) carries no + // description. That is the banner in `QuickStart.md`: a language link between the `#` + // title and the real first sentence. + const withoutLinks = paragraph + .join(' ') + .replace(/!?\[[^\]]*\]\([^)]*\)/g, '') + .replace(/[*_~`>#]/g, '') + .trim() + + if (!plain) continue + if (withoutLinks.replace(/[^\p{L}]/gu, '').length < 12) continue + + description = truncate(plain, 200) + break + } + + return { title, description } +} + +/** Trim to a word boundary with an ellipsis, so a card never ends mid-word. */ +function truncate(text: string, limit: number): string { + if (text.length <= limit) return text + + const cut = text.slice(0, limit) + const lastSpace = cut.lastIndexOf(' ') + return `${(lastSpace > limit * 0.6 ? cut.slice(0, lastSpace) : cut).replace(/[,;:.]$/, '')}…` +} + +async function mtime(...segments: string[]): Promise { + try { + const stat = await fs.stat(path.join(REPO_ROOT, ...segments)) + return stat.mtimeMs + } catch { + return 0 + } +} + +/** + * URL segment for a document, derived from its file name. + * + * Lower-cased on purpose: GitHub Pages serves from a case-sensitive origin, so a folder + * emitted as `QuickStart/` is unreachable from a link written as `/docs/quickstart/`. + * Windows checkouts and local dev servers are case-insensitive, which is exactly why this + * bug survives local testing and 404s only in production. + * + * `WhatsNew-v10.5.2.md` -> `10.5.2`; `-RU` is kept so translations get their own URL rather + * than overwriting the English page at build time. + */ +function slugFor(fileName: string): string { + return fileName + .replace(/\.md$/i, '') + .replace(/^WhatsNew-v/i, '') + .toLowerCase() +} + +/** `WhatsNew-v10.5.2.md` -> `10.5.2`. Used to build the release-note index. */ +export function versionFromFileName(fileName: string): string | null { + const match = fileName.match(/^WhatsNew-v(\d+(?:\.\d+)*)/i) + return match?.[1] ?? null +} + +/** + * URL slug for a Markdown file under `docs/`. + * + * The slug mirrors the directory layout, so a link written as `docs/TASKS/README.md` and one + * written as `/docs/tasks/` resolve to the same page. A directory's `README.md` collapses to + * the directory itself — `docs/TASKS/README.md` becomes `/docs/tasks/`, which is the + * conventional index URL and what the previous landing page linked to by name. + * + * Lower-cased throughout, including directory names: GitHub Pages serves from a + * case-sensitive origin, so an emitted `TASKS/` folder is unreachable from a link written + * as `/docs/tasks/`. Windows checkouts and local dev servers are case-insensitive, which is + * exactly why that bug survives local testing and 404s only in production. + */ +export function slugForSource(source: string): string { + const relative = source.replace(/^docs\//, '').replace(/\.md$/i, '') + const segments = relative.split('/').filter(Boolean) + + const last = segments[segments.length - 1] ?? '' + if (last.toLowerCase() === 'readme') segments.pop() + + return segments + .map((segment, index) => { + // Only the file-name segment carries the `WhatsNew-v` prefix; a directory that happens + // to be named `WhatsNew-v…` is not a release note. + const stripped = + index === segments.length - 1 ? segment.replace(/^WhatsNew-v/i, '') : segment + return stripped.toLowerCase() + }) + .join('/') +} + +async function describeDoc(source: string): Promise { + const markdown = await fs.readFile(path.join(REPO_ROOT, source), 'utf8') + const { title, description } = extractHeadings(markdown) + + return { + source, + slug: slugForSource(source), + title, + description, + lang: /-RU\.md$/i.test(source) ? 'ru' : 'en', + updatedAt: await mtime(source), + } +} + +/** + * Every Markdown file under `docs/`, at any depth, minus the documents that get their own + * section. + * + * Depth matters: the work log in `docs/TASKS/` and the planning notes in `docs/scope/` are + * committed, public, and were linked from the previous landing page — leaving them out would + * be a coverage regression dressed up as a curation decision. + * + * Excluded on purpose: + * - `WhatsNew-*.md` — published under `/blog/`; + * - `performance-measurements.md` — published under `/measurements/`; + * - `v10.2.6.md` — the prose export of a release note whose canonical form is + * `WhatsNew-v10.2.6.md`, so publishing both duplicates the whole article; + * - `docs/habr/` — a draft being written for Habr, carrying editorial notes about which + * headline to use. Same category as `.articles/`, which is gitignored for the same + * reason: working material for another platform, not documentation. + */ +export async function listDocPages(): Promise { + const files = await collectMarkdown(path.join(REPO_ROOT, 'docs'), 'docs') + + const published = files + .filter((source) => !/^docs\/WhatsNew-/i.test(source)) + .filter((source) => source !== 'docs/performance-measurements.md') + .filter((source) => !/^docs\/v\d/i.test(source)) + .filter((source) => !source.startsWith('docs/habr/')) + .sort((a, b) => a.localeCompare(b)) + + return Promise.all(published.map(describeDoc)) +} + +/** Recursively collect `.md` paths under `dir`, returned as forward-slash repo-relative paths. */ +async function collectMarkdown(dir: string, prefix: string): Promise { + const entries = await fs.readdir(dir, { withFileTypes: true }) + const found: string[] = [] + + for (const entry of entries) { + const name = entry.name + if (name.startsWith('.')) continue + + if (entry.isDirectory()) { + found.push(...(await collectMarkdown(path.join(dir, name), `${prefix}/${name}`))) + continue + } + + if (entry.isFile() && name.endsWith('.md')) { + found.push(`${prefix}/${name}`) + } + } + + return found +} + +/** Release notes, newest first. */ +export async function listReleaseNotes(): Promise { + const entries = await fs.readdir(path.join(REPO_ROOT, 'docs'), { withFileTypes: true }) + const files = entries + .filter((entry) => entry.isFile() && /^WhatsNew-v\d/i.test(entry.name)) + .map((entry) => `docs/${entry.name}`) + + const pages = await Promise.all(files.map(describeDoc)) + + // Newest first. `compareVersionStrings` already orders descending, so the operands go in + // as-is — passing them swapped silently produces an ascending list, which on the home + // page reads as "the newest release is 10.2.6". + return pages.sort((a, b) => + compareVersionStrings( + versionFromFileName(path.basename(a.source)), + versionFromFileName(path.basename(b.source)), + ), + ) +} + +/** `null` sorts last, so an unversioned draft never outranks a shipped release. */ +function compareVersionStrings(a: string | null, b: string | null): number { + if (a === null && b === null) return 0 + if (a === null) return 1 + if (b === null) return -1 + + const pa = a.split('.').map(Number) + const pb = b.split('.').map(Number) + + for (let i = 0; i < 3; i += 1) { + const diff = (pb[i] ?? 0) - (pa[i] ?? 0) + if (diff !== 0) return diff + } + + return 0 +} + +export interface DocSource { + source: string + markdown: string +} + +/** Load one document by its repository-relative path. */ +export async function loadDoc(source: string): Promise { + return { source, markdown: await readText(source) } +} + +export async function getDocPage(source: string): Promise { + return describeDoc(source) +} + +/** + * `docs/performance-measurements.md` is the measured-behaviour report. It is a first-class + * section of the site rather than an ordinary guide because every claim on it is a measured + * number, and the project treats that distinction as part of its contract. + */ +export async function getMeasurements(): Promise { + const source = 'docs/performance-measurements.md' + const { markdown } = await loadDoc(source) + const { title, description } = extractHeadings(markdown) + + return { source, markdown, title, description } +} + +export interface DebateTemplateInfo { + /** Property name on `DebateTemplate`, e.g. `ArchitectureReview`. */ + name: string + /** Display name derived from the property, e.g. `Architecture review`. */ + title: string + /** Member roles in the order they are added. */ + members: string[] + /** Debate strategy class, e.g. `CritiqueDebate`. */ + strategy: string + /** `Standard` or `Strict` — the chairman's stance. */ + chairman: string + maxRounds: number + /** The persona/role prompt for the first member, trimmed. */ + summary: string +} + +const MEMBER_RE = /\.AddMember\(\s*"[a-z0-9-]+"\s*,\s*provider\s*,\s*"([^"]+)"/g +const PERSONA_MEMBER_RE = /\.AddMember\(\s*"[a-z0-9-]+"\s*,\s*provider\s*,\s*"([^"]+)"\s*,\s*Persona\.(\w+)\s*\)/g +const STRATEGY_RE = /\.WithStrategy\(new (\w+)\(\)\)/ +const CHAIRMAN_RE = /\.SetChairman\(Chairman\.Create(\w+)\("chairman"/ +const MAX_ROUNDS_RE = /\.WithMaxRounds\((\d+)\)/ +const CLASS_RE = /public sealed class (\w+)Template : DebateTemplateBase/g + +/** + * Read the built-in debate templates out of `DebateTemplate.cs`. + * + * Parsed rather than hand-written: a list on a website that disagrees with the code is the + * exact failure this project documents elsewhere (see the measured-behaviour page, where a + * documented claim was corrected to what the harness actually produced). The roles, strategy + * and round count all come from the source that ships in the NuGet package. + * + * If `DebateTemplate.cs` ever moves or is refactored away from this shape, the parse yields + * nothing and the templates page says so — instead of shipping a stale list. + */ +export async function getDebateTemplates(): Promise { + const source = await readText('src', 'Delibera.Core', 'Templates', 'DebateTemplate.cs') + + const starts = [...source.matchAll(CLASS_RE)] + if (starts.length === 0) return [] + + return starts.map((match, index) => { + const className = match[1] ?? '' + const bodyStart = match.index ?? 0 + const bodyEnd = starts[index + 1]?.index ?? source.length + const body = source.slice(bodyStart, bodyEnd) + + const members: string[] = [] + for (const member of body.matchAll(MEMBER_RE)) { + if (member[1]) members.push(member[1]) + } + + const strategy = body.match(STRATEGY_RE)?.[1] ?? 'StandardDebate' + const chairman = body.match(CHAIRMAN_RE)?.[1] ?? 'Standard' + const maxRounds = Number(body.match(MAX_ROUNDS_RE)?.[1] ?? 4) + + // Prefer the first inline persona; fall back to naming the named Persona constants so + // the card still says something for templates that reference shared personas. + const inline = body.match(/\.AddMember\([^,]+,\s*provider\s*,\s*"[^"]+"\s*,\s*\n?\s*"""\n\s*([^\n]+)/) + + // The type predicate is what narrows `(string | undefined)[]` to `string[]`; + // `.filter(Boolean)` leaves the union in place and `Intl.ListFormat` rejects it. + const personas = [ + ...new Set([...body.matchAll(PERSONA_MEMBER_RE)].map((m) => m[2]).filter((name): name is string => Boolean(name))), + ] + + const summary = + inline?.[1]?.trim() ?? + (personas.length > 0 ? `Uses the shared ${new Intl.ListFormat('en').format(personas)} personas.` : '') + + return { + name: className, + title: humanize(className), + members, + strategy, + chairman, + maxRounds, + summary, + } + }) +} + +/** `ArchitectureReview` -> `Architecture review`. */ +function humanize(name: string): string { + const spaced = name.replace(/([a-z0-9])([A-Z])/g, '$1 $2').toLowerCase() + return spaced.charAt(0).toUpperCase() + spaced.slice(1) +} \ No newline at end of file diff --git a/site/lib/markdown.ts b/site/lib/markdown.ts new file mode 100644 index 0000000..434df79 --- /dev/null +++ b/site/lib/markdown.ts @@ -0,0 +1,294 @@ +import { Marked } from 'marked' +import GithubSlugger from 'github-slugger' +import { createHighlighter, type Highlighter } from 'shiki' +import { githubBlob, githubTree } from './site' +import { withBasePath } from './paths' + +/** + * Markdown -> HTML for build-time rendering. + * + * Three things happen here that a bare `marked.parse()` would not do: + * + * 1. Syntax highlighting via Shiki, using the same TextMate grammars GitHub uses. The + * highlighter is created once per build and shared across every page. + * 2. Stable heading anchors, generated with `github-slugger` so the `#anchor` fragments + * written in the Markdown keep resolving after the docs move from GitHub to this site. + * 3. Link rewriting. The Markdown in `docs/` links to its neighbours by repository path + * (`../CHANGELOG.md`, `QuickStart.md`, `docs/performance-measurements.md#3-context-compression`). + * Those targets do not exist on a static host, so each one is resolved to either a site + * route or an absolute GitHub URL. + * + * The Markdown is repository content and is trusted, but `raw` HTML is still passed through + * rather than escaped because the docs legitimately embed `
` banners and + * badge images. It is first-party content in a repository we build, not user input. + */ + +/** Languages the docs actually use. Loading all of Shiki's grammars would triple build time. */ +const LANGS = [ + 'csharp', + 'bash', + 'shell', + 'json', + 'yaml', + 'xml', + 'dockerfile', + 'docker', + 'sql', + 'ini', + 'console', + 'powershell', + 'diff', + 'typescript', + 'markdown', + 'text', +] as const + +let highlighterPromise: Promise | null = null + +function getHighlighter(): Promise { + highlighterPromise ??= createHighlighter({ + themes: ['github-light', 'github-dark'], + langs: [...LANGS], + }) + + return highlighterPromise +} + +/** + * Repository-relative paths that have a page on this site, mapped to the route that serves + * them. Filled in by `createMarkdown()` from the caller's content index, so adding a document + * automatically makes every existing cross-link to it resolve to a real page. + */ +export type RouteIndex = Map + +function normaliseRepoPath(input: string): string { + return input.replace(/^\.\//, '').replace(/^\/+/, '') +} + +export interface MarkdownOptions { + /** `docs/QuickStart.md` — the document being rendered, used to resolve relative links. */ + source: string + /** repo path -> site route, for every document published on the site. */ + routes: RouteIndex + /** + * Drop the document's own banner and first `#` heading. + * + * Set by every page that renders a `PageHead` above the body: `QuickStart.md` opens with a + * centred logo, a tagline, a link to the translation and then `# Delibera — Quick Start`. + * Rendered as-is on a page that already has a title bar, that logo and headline appear + * twice, and the banner's tagline becomes a top-level entry in the table of contents. + */ + stripPreamble?: boolean +} + +/** + * Remove the leading banner block and the first H1 from a document. + * + * Both are optional and position-sensitive: only a `
` at the very start of the file is + * treated as a banner, and only the *first* H1 is dropped, so headings further down keep + * their level and the in-page table of contents is unaffected. + */ +export function stripDocumentPreamble(markdown: string): string { + let text = markdown.replace(/^\s+/, '') + + text = text.replace(/^]*>[\s\S]*?<\/div>\s*/i, '') + + // The first H1 is the document title, already shown by the page header. + text = text.replace(/^#\s+.+?#*\s*(\r?\n)+/, '') + + return text +} + +/** + * Rewrite one link target. + * + * Order matters: absolute URLs and bare anchors are returned untouched, then we try to + * resolve the path against the repository, and only fall back to GitHub when the target is + * not something this site serves. + */ +function rewriteHref(href: string, source: string, routes: RouteIndex): string { + if (!href || href.startsWith('#') || /^[a-z][a-z0-9+.-]*:/i.test(href) || href.startsWith('//')) { + return href + } + + // Slice at the `#` rather than splitting on it: the fragment has to be carried over + // *with* its marker. Dropping it turns `QuickStart.md#cost-limits` into + // `/docs/quickstart/cost-limits`, which looks like a path and 404s. + const hashIndex = href.indexOf('#') + const rawPath = hashIndex === -1 ? href : href.slice(0, hashIndex) + const hash = hashIndex === -1 ? '' : href.slice(hashIndex) + + if (!rawPath) return href + + // Resolve relative to the document's own directory, then normalise `..` segments. + const sourceDir = source.includes('/') ? source.slice(0, source.lastIndexOf('/')) : '' + const joined = rawPath.startsWith('/') + ? normaliseRepoPath(rawPath) + : normaliseRepoPath(`${sourceDir}/${rawPath}`) + + const resolved = normalise(joined) + if (!resolved) return href + + // Images and other binaries are served from the site's own `public/` directory. + if (/\.(png|jpe?g|gif|svg|webp|avif)$/i.test(resolved)) { + if (resolved.startsWith('img/')) return withBasePath(`/img/${resolved.slice('img/'.length)}${hash}`) + return githubBlob(resolved) + } + + const route = routes.get(resolved) + if (route) return `${withBasePath(route)}${hash}` + + if (resolved.endsWith('.md')) { + return githubBlob(resolved) + } + + if (resolved.endsWith('/')) return githubTree(resolved.slice(0, -1)) + + // A source file, a compose file, a directory — none of these are rendered here. + return /\.[a-z0-9]+$/i.test(resolved) ? githubBlob(resolved) : githubTree(resolved) +} + +function normalise(p: string): string { + const out: string[] = [] + + for (const segment of p.split('/')) { + if (segment === '' || segment === '.') continue + if (segment === '..') { + out.pop() + continue + } + out.push(segment) + } + + return out.join('/') +} + +function escapeAttribute(value: string): string { + return value + .replace(/&/g, '&') + .replace(/"/g, '"') + .replace(//g, '>') +} + +export interface RenderResult { + html: string + /** Ordered table of contents built from the rendered headings. */ + headings: Array<{ depth: number; text: string; id: string }> +} + +/** Render one Markdown document to HTML. */ +export async function renderMarkdown(markdown: string, options: MarkdownOptions): Promise { + const highlighter = await getHighlighter() + const slugger = new GithubSlugger() + const headings: RenderResult['headings'] = [] + + // Shiki is async and `marked`'s renderer must be synchronous, so highlighting happens in + // the `walkTokens` hook — which does accept a promise — and the result is handed to the + // renderer through this map. Marked awaits the hook before parsing, so every entry is + // present by the time `code` runs. + const highlighted = new WeakMap() + + const marked = new Marked({ + gfm: true, + async: true, + + walkTokens: async (token) => { + if (token.type !== 'code') return + + const language = (token.lang ?? '').split(/\s+/)[0]?.toLowerCase() ?? '' + if (!language || !highlighter.getLoadedLanguages().includes(language)) return + + highlighted.set( + token, + highlighter.codeToHtml(token.text, { + lang: language, + themes: { light: 'github-light', dark: 'github-dark' }, + defaultColor: 'light', + }), + ) + }, + + renderer: { + heading({ tokens, depth }) { + const text = this.parser.parseInline(tokens) + const plain = stripTags(text) + const id = slugger.slug(plain) + + headings.push({ depth, text: plain, id }) + + return `${text}#\n` + }, + + link({ href, title, tokens }) { + const text = this.parser.parseInline(tokens) + const target = rewriteHref(href, options.source, options.routes) + const titleAttr = title ? ` title="${escapeAttribute(title)}"` : '' + + // Absolute links leave the site; open them in a new tab and drop the referrer. + const isExternal = /^https?:\/\//i.test(target) + const externalAttrs = isExternal ? ' target="_blank" rel="noopener noreferrer"' : '' + + return `${text}` + }, + + image({ href, title, text }) { + const target = rewriteHref(href, options.source, options.routes) + const titleAttr = title ? ` title="${escapeAttribute(title)}"` : '' + return `${escapeAttribute(text)}` + }, + + /** + * Raw HTML blocks are passed through by `marked` verbatim, which is what lets the + * docs keep their `
` banners and badge images. The side effect is + * that an `` inside raw HTML never reaches the `image` + * renderer, so its relative path would survive into the page and resolve against + * `/docs/quickstart/` instead of the site root. + * + * Rewriting the `src`/`href` attributes of inline HTML fixes that without parsing + * the markup into a DOM. + */ + html(token) { + return rewriteInlineHtml(token.text, options.source, options.routes) + }, + + code(token) { + const preHighlighted = highlighted.get(token) + if (preHighlighted) return `
${preHighlighted}
\n` + + // No grammar loaded for this fence: emit escaped plain text rather than guessing a + // language, which would highlight every shell transcript as if it were C#. + return `
${escapeHtml(token.text)}
\n` + }, + }, + }) + + const html = await marked.parse(options.stripPreamble ? stripDocumentPreamble(markdown) : markdown) + + return { html, headings } +} + +function stripTags(html: string): string { + return html.replace(/<[^>]+>/g, '').replace(/&/g, '&').trim() +} + +function escapeHtml(value: string): string { + return value + .replace(/&/g, '&') + .replace(//g, '>') +} + +/** + * Rewrite `src=` / `href=` inside a raw HTML block so relative repository paths resolve. + * + * Only two attributes are touched, and only when their value looks like a relative path — + * an external `https://` image or a `#anchor` is left exactly as written. + */ +function rewriteInlineHtml(html: string, source: string, routes: RouteIndex): string { + return html.replace(/\b(src|href)="([^"]+)"/g, (match, attribute: string, value: string) => { + if (!value || value.startsWith('#') || /^(https?:|data:|mailto:|\/\/)/i.test(value)) return match + + return `${attribute}="${escapeAttribute(rewriteHref(value, source, routes))}"` + }) +} \ No newline at end of file diff --git a/site/lib/paths.ts b/site/lib/paths.ts new file mode 100644 index 0000000..d73e382 --- /dev/null +++ b/site/lib/paths.ts @@ -0,0 +1,40 @@ +/** + * Base path helpers. + * + * `next.config.ts` sets `basePath` from `PAGES_BASE_PATH`, which `actions/configure-pages` + * fills in as `/Delibera` on a Pages deploy and which is empty locally. Next.js applies that + * prefix automatically to ``, `next/image` and its own chunk URLs — but **not** to a + * plain `` or to a string literal inside the metadata object, because + * those bypass the router entirely. + * + * That asymmetry is the whole reason this module exists: the hand-written markup in + * `app/page.tsx` and `app/layout.tsx` uses raw `` tags, and every one of them needs the + * prefix added by hand or the deployed site renders broken images. + * + * Keep the fallback in sync with `basePath` in next.config.ts — both read the same variable. + */ +const BASE_PATH = process.env.PAGES_BASE_PATH ?? '' + +/** Prefix a root-relative site path with the deployment base path. */ +export function withBasePath(path: string): string { + if (!path.startsWith('/')) return path + return `${BASE_PATH}${path}` +} + +/** Public path of a repository brand image, ready for use in `src`/`href`. */ +export function brandImage(fileName: string): string { + return withBasePath(`/img/${fileName}`) +} + +/** + * Absolute origin the site is published at. + * + * Used for anything a crawler consumes — `sitemap.xml` entries and the `robots.txt` + * `Sitemap:` line — where a relative URL is either invalid or unresolvable. Constructed from + * the deployment origin plus `basePath`, so it follows the Pages configuration instead of + * hardcoding `/Delibera` in a second place. + */ +export function siteOrigin(path: string): string { + const origin = process.env.SITE_ORIGIN ?? 'https://techbuzzz.github.io' + return `${origin}${withBasePath(path)}` +} \ No newline at end of file diff --git a/site/lib/routes.ts b/site/lib/routes.ts new file mode 100644 index 0000000..a51e454 --- /dev/null +++ b/site/lib/routes.ts @@ -0,0 +1,87 @@ +import { listDocPages, listReleaseNotes } from './content' +import type { RouteIndex } from './markdown' + +/** + * The single mapping from "a file in this repository" to "a page on this site". + * + * Markdown in `docs/` links between documents by repository path. Rendering those links as + * written would produce `/docs/QuickStart.md`, which 404s on a static host. Both the link + * rewriter and the pages themselves derive their routes from this one function, so a + * document can never be published at one URL and linked from another. + */ + +/** Route for a guide in `docs/`, e.g. `docs/QuickStart.md` -> `/docs/quickstart/`. */ +export function docRoute(slug: string): string { + return `/docs/${slug}/` +} + +/** + * Route for a release note. + * + * Keyed on the document slug, which is the version with any `-RU` suffix preserved: + * `WhatsNew-v10.5.2.md` -> `/blog/10.5.2/`, `WhatsNew-v10.2.6-RU.md` -> + * `/blog/10.2.6-ru/`. Keying on the bare version instead would collapse the Russian + * translations onto the English pages, and one of the two would silently overwrite the + * other at export time. + */ +export function releaseRoute(slug: string): string { + return `/blog/${slug}/` +} + +/** Normalised slug for linking a version number to whichever translation exists. */ +export function normalisedReleaseSlug(slug: string): string { + return slug.replace(/-ru$/i, '') +} + +/** + * Trailing slashes are part of every route string. + * + * `next.config.ts` sets `trailingSlash: true`, so `out/blog/10.5.2/index.html` is what the + * Pages CDN serves. A link written as `/blog/10.5.2` would still redirect, but a file copied + * out of the export and opened from disk would not. + */ +export const MEASUREMENTS_ROUTE = '/measurements/' +export const CHANGELOG_ROUTE = '/changelog/' +export const TEMPLATES_ROUTE = '/templates/' + +/** + * Build the repo-path -> route index used when rewriting Markdown links. + * + * Release notes are registered under both their source path and their bare version, because + * documents reference them either way (`WhatsNew-v10.5.2.md` from the changelog, `v10.5.2` + * in prose). Whichever form appears in the source resolves to the same page. + */ +export async function buildRouteIndex(): Promise { + const routes: RouteIndex = new Map() + + for (const page of await listDocPages()) { + routes.set(page.source, docRoute(page.slug)) + } + + for (const note of await listReleaseNotes()) { + const route = releaseRoute(note.slug) + routes.set(note.source, route) + + // `WhatsNew-v10.5.2.md` is also written as `v10.5.2` in prose (README, changelog), so + // register that spelling too. Translations register under their own slug as well, which + // is why the source path — not the bare version — is the primary key above. + routes.set(`v${normalisedReleaseSlug(note.slug)}`, route) + } + + routes.set('docs/performance-measurements.md', MEASUREMENTS_ROUTE) + routes.set('performance-measurements.md', MEASUREMENTS_ROUTE) + routes.set('CHANGELOG.md', CHANGELOG_ROUTE) + + return routes +} + +/** Every static path the export must emit, for `generateStaticParams`. */ +export async function listStaticDocPaths(): Promise { + const guides = await listDocPages() + const notes = await listReleaseNotes() + + return [ + ...guides.map((page) => docRoute(page.slug)), + ...notes.map((note) => releaseRoute(note.slug)), + ] +} \ No newline at end of file diff --git a/site/lib/site.ts b/site/lib/site.ts new file mode 100644 index 0000000..122681d --- /dev/null +++ b/site/lib/site.ts @@ -0,0 +1,62 @@ +/** + * Repository-wide constants for the site. + * + * Every external URL is derived from `REPO_OWNER`/`REPO_NAME` instead of being written + * out per link: the site is deployed from a fork-friendly workflow, and a hardcoded + * `techbuzzz/...` in 20 places is 20 places to fix when the repo moves. + */ +export const REPO_OWNER = 'techbuzzz' +export const REPO_NAME = 'Delibera' + +/** Public origin of the deployed site. Used for canonical URLs and OpenGraph tags. */ +export const SITE_URL = `https://${REPO_OWNER}.github.io` + +/** Markdown documentation and release notes live here. */ +export const DOCS_DIR = 'docs' + +/** + * The NuGet packages that ship from this repository. Kept as data rather than markup so + * the install block and the package grid cannot drift apart. + */ +export const NUGET_PACKAGES = [ + { + id: 'Delibera.Core', + description: 'The framework — councils, chairman, RAG, compression, CLI, gRPC.', + }, + { + id: 'Delibera.Server', + description: 'ASP.NET Core 10 Minimal API host: REST, SSE streaming, MCP.', + }, + { + id: 'Delibera.Redis', + description: 'Distributed debates plus a shared result cache over Redis.', + }, +] as const + +/** Container images published on every `v*` tag. */ +export const DOCKER_IMAGES = [ + { + id: 'delibera-server', + hub: 'https://hub.docker.com/r/techbuzzz/delibera-server', + description: 'The API host, with the Web UI BFF alongside it.', + }, + { + id: 'delibera-webui', + hub: 'https://hub.docker.com/r/techbuzzz/delibera-webui', + description: 'Nuxt 4 browser front end for the debate workflow.', + }, +] as const + +/** A link to a file as GitHub will render it, anchored to a branch or a tag. */ +export function githubBlob(repoPath: string, ref = 'main'): string { + return `https://github.com/${REPO_OWNER}/${REPO_NAME}/blob/${ref}/${repoPath.replace(/^\/+/, '')}` +} + +export function githubTree(repoPath: string, ref = 'main'): string { + return `https://github.com/${REPO_OWNER}/${REPO_NAME}/tree/${ref}/${repoPath.replace(/^\/+/, '')}` +} + +export const REPO_URL = `https://github.com/${REPO_OWNER}/${REPO_NAME}` +export const ISSUES_URL = `${REPO_URL}/issues` +export const RELEASES_URL = `${REPO_URL}/releases/latest` +export const NUGET_URL = (id: string) => `https://www.nuget.org/packages/${id}` \ No newline at end of file diff --git a/site/next.config.ts b/site/next.config.ts new file mode 100644 index 0000000..81672d5 --- /dev/null +++ b/site/next.config.ts @@ -0,0 +1,30 @@ +import type { NextConfig } from 'next' + +// GitHub Pages serves this site from https://techbuzzz.github.io/Delibera/, so every +// route and asset is prefixed with `/Delibera`. `actions/configure-pages` computes that +// prefix and hands it to the build as PAGES_BASE_PATH (see .github/workflows/pages.yml); +// locally the variable is absent and the site runs at `/`, which is what `npm run dev` +// wants. Keep the fallback in sync with lib/paths.ts - both read the same variable. +const basePath = process.env.PAGES_BASE_PATH ?? '' + +const nextConfig: NextConfig = { + // A Pages deploy is a static artifact: there is no Node server to render on demand. + output: 'export', + + basePath, + + // Pages serves `/blog/v10.5.2/` (a directory) rather than `/blog/v10.5.2`. Without + // this the export emits extensionless files and a visitor following a shared deep link + // gets a 404 from the CDN. + trailingSlash: true, + + // The image optimizer needs a running server; under `output: export` every image must + // be served as-is. + images: { unoptimized: true }, + + // Markdown is rendered at build time from the repository's own files, so nothing is + // fetched at runtime. No images, fonts or scripts come from a third-party origin either. + reactStrictMode: true, +} + +export default nextConfig \ No newline at end of file diff --git a/site/package-lock.json b/site/package-lock.json new file mode 100644 index 0000000..93d2eac --- /dev/null +++ b/site/package-lock.json @@ -0,0 +1,1623 @@ +{ + "name": "delibera-site", + "version": "10.5.2", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "delibera-site", + "version": "10.5.2", + "license": "MIT", + "dependencies": { + "github-slugger": "^2.0.0", + "marked": "^15.0.7", + "next": "16.4.0", + "react": "^19.3.0", + "react-dom": "^19.3.0", + "shiki": "^3.0.0" + }, + "devDependencies": { + "@types/node": "^22.15.0", + "@types/react": "^19.1.0", + "@types/react-dom": "^19.1.0", + "typescript": "^5.8.3" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz", + "integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.5.tgz", + "integrity": "sha512-QRUlFQ0WxvdWyqqG/WtI3iupfD5rBzmCHXSdPsY91sAtVtTo7Q4cb6zOccZ3gqEqkr0f1As1ehLqmEpDsRf+lg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.3.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.5.tgz", + "integrity": "sha512-+BR255RhDlpygUpOc/Jdt1nT6DQ3XG/ERo5wbcdOf5Q320dKtPCKPLR1LJs9VGXRaMa8l1uUa0tkCNOXiAxZUw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.3.4" + } + }, + "node_modules/@img/sharp-freebsd-wasm32": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.5.tgz", + "integrity": "sha512-Y/z91nEZ4uIBX5X3nfTovjU9lHNKFYbL2lpHCLVNmXQK03VIZvXBBt0KxbPGp2SdGSF+2mQU4e+hQaWOt86iAw==", + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "dependencies": { + "@img/sharp-wasm32": "0.35.5" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.4.tgz", + "integrity": "sha512-5R89nBYiRdUlSWJxPhO+GVtaXzXSxKnRu/xqMn3KTA3L9EB9Oy/P+Nn2f2vlhPuUdy/Zusb2DarbyTpGCfEDuw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.4.tgz", + "integrity": "sha512-iR2OKH80yi0U+dUplyh3/xdpFvps6YkCwsXenIJxqxR1v9o+xtKTGbS9H7cps+2Vxjc8B1j96p75NmTGjIhtpQ==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.4.tgz", + "integrity": "sha512-LmRtTsOHuvM2+wlO2Db37dx5MiZhB0FvSunciw48YjdOkZz9KAiRbm8ujeMOA1INqmei5NapFxYEK1D1ZSidmw==", + "cpu": [ + "arm" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.4.tgz", + "integrity": "sha512-Y3dgX/6lE2QhQb+Gxy0WZxfg9MEm/JBjamZpS2IklP7xIQoKN4hzAm7KcMVGtaVDt3neE9OKBC7vAfonA/Lr1A==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.4.tgz", + "integrity": "sha512-Le6boB8Tai0Nis+gIxIpKx68UDVVIqdR8Tin5Yf1z2LJJQLDJvCDRqRu+jC2qCoD+eIomonmOwB4smBRxfVpYQ==", + "cpu": [ + "ppc64" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.4.tgz", + "integrity": "sha512-aHkkIEHPRdQEegJN20MLmGtxYD9R2wQr3Cwpddnu5+YKMt6Uzax7S9h5gpZTo8wyrGuZSlfQ63OevL5mTyOC7Q==", + "cpu": [ + "riscv64" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.4.tgz", + "integrity": "sha512-ra/mB6MikESDUO7Yg+Mi95bFBb9GsObURuhnOv3OqknjGe9sZrG8tCe9q0xSIGrtLgvgw0gKnFWcK4blSgQOuQ==", + "cpu": [ + "s390x" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.4.tgz", + "integrity": "sha512-GJ//SSXbnwSDes02umB3nDJLFcQzw8a18V8fyhqr6tV515tOEMdImjjxj1AoafMRz56F3PHgftnj1QEKSU1zkw==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.4.tgz", + "integrity": "sha512-hvulFwtjUcagsis6BBxHwGFwWoNZjgYmULGVrZcyfNbjA8hKILbRxGg15/7w5HDyXHXUos/j6baAWqnCyQ2DWA==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.4.tgz", + "integrity": "sha512-6zXKeE/p39I1AmA3cJG35eyBGNqNddLnUXjhwBnsGjFPWqf5VKkDBEqaEkPDoTEtkxwi2vv8Tcr2mDyP4So7Fg==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.5.tgz", + "integrity": "sha512-LEaXK2WdXVK5ykcw0buWyPMsmLLL2vpHLD6yrNSW+JGEL3BZPA4tpKN6iaMc4AxTTAoaX/sU1rOL51lcIz48ZQ==", + "cpu": [ + "arm" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.3.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.5.tgz", + "integrity": "sha512-LYVx5JTsOM2CBzmxreh+nl64/3H6Xb09iSLknqH47z2T2DFFxDeFLP5y4dJwe6H7uGQlHPyEEtIqyo3DYsRwdQ==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.3.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.5.tgz", + "integrity": "sha512-QVxAAq8evVRI9ia2vqgwrmWucn5Dfv+JdWzj75pD8omHLPSP7f8p20O8jxzjCcuCEQEOtYOZUmX1hkiZ0kdevA==", + "cpu": [ + "ppc64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.3.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.5.tgz", + "integrity": "sha512-LtdreXguaavKODPIfzJ4kffx7UNt1omwtK0rch4EBbbSTXPnxWmYSayXdLJw0fJzQ97kHt1gL/yh4tvU+nCyRQ==", + "cpu": [ + "riscv64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.3.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.5.tgz", + "integrity": "sha512-UZasTOFiYzotTsGOCu42BfUzP6Tu6Do/947iRm1RsLKvlllxwGcn4RN27LibGWceix4Y+Pmw3jsnTcCQIgWjqA==", + "cpu": [ + "s390x" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.3.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.5.tgz", + "integrity": "sha512-SxFtLTeJInhAA9Q836kux2vZNeOBQEx658qvbboZScr0wIARym3IcGmW7KpVD5sbVg0Ojy+udFQdayYIZyoNog==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.3.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.5.tgz", + "integrity": "sha512-9HbMclmI1zlNkFRs3z9/eBtDjfD0sGlrX1z6b1qwmiFY5ElDLh4BC0LPBdVp7z1DXFiKlIcznf+ZlsuZzLxQqg==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.3.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.5.tgz", + "integrity": "sha512-4KOphqB035HrVdqLZfCgMzzERrQkkzOwRhl4OAkRO1YCldbaFjySXMaK534Mo0V+LndnlJk+sbUyLeU0ULyD1A==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.3.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.5.tgz", + "integrity": "sha512-Ptsga1su4tQx+LLF1ECS9U6nz5kmrXKo6XVbtR48Ke3ZRxxgaWBu7IDtEe1quo8hiupwm6WFqxVlXaSf7IINGQ==", + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.11.3" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-webcontainers-wasm32": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.5.tgz", + "integrity": "sha512-hfhF/FmoQyTUkA0bIKFOtw536BQSeBMe6BF6QyWlrPxT754+TFLaZ7sKKTfvvM0yJgKgaYTwnFCIZ/GuDw5SUA==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/sharp-wasm32": "0.35.5" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.5.tgz", + "integrity": "sha512-X4t7g+7ZA5DKblCBEXGjUqqemj4vczING/5viFwAL8h4N3qYeyjwdCvRLHi4EdOUI+2Z7UFlp1VM+p/AuEtm6Q==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.5.tgz", + "integrity": "sha512-5Zm82LoBc43nhwNybZlG7Y1KO//Zhsn306fQl29ZOuStHLGTo3BWL83q3cznX0poxSAMuYL1On/BHBxkBeKr6A==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.5.tgz", + "integrity": "sha512-x76eH0vEiHlcMQu8Y8IenntaACtddpT6W0wmXtWrnKcnKI7ME5DdgqhAD6SEWOEl1v2zDvkZDhFA9KnURwpfqg==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@next/env": { + "version": "16.4.0", + "resolved": "https://registry.npmjs.org/@next/env/-/env-16.4.0.tgz", + "integrity": "sha512-pyLlT5oafGlX8X7xjgGNXz5dNIDSxREm+qD6i9VLSZiZFopA/XWl8NM/KPmLx/8EG/++c49iid3gNWSQW1+2dQ==", + "license": "MIT" + }, + "node_modules/@next/swc-darwin-arm64": { + "version": "16.4.0", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.4.0.tgz", + "integrity": "sha512-yyZ+9MgViAKLLb5l1gPBGjRwnq3teNLb5Zx6JEw8LzNSTjlsnp+baUHFxTgl7PpjCmKf48VQfyipG1eBflb48Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "16.4.0", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.4.0.tgz", + "integrity": "sha512-RePEEejEj1Auf5s9qEcG0JbcDP3ceNUS/ofNX8AhxzgIjTVlZIHMatlp16YplcSMpsVo/ZMQHJTjbMYJznjGUg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "16.4.0", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.4.0.tgz", + "integrity": "sha512-HhsyCqd+NpmvDDVY/LqkAN24f+QVvXh3ie+PfcwKinWL2Mu826JymQxJXGjAIfmJ4VlHjp4Pe/nZtXtUoEMAqw==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "16.4.0", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.4.0.tgz", + "integrity": "sha512-sN9Pf+Zh+RpZAOmbR7d8KAA8T61CVhawtbEJ2Bk6QrRzx6Z/6haaF4UiwBzytM96GRRRWZh9/WALIEO3npMamA==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "16.4.0", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.4.0.tgz", + "integrity": "sha512-D5M5xLWQVd1CvR+L7BickM1weRfmM41x3L1NWcnKY/oyJJqr1qwaCgmu3dMAhLBOMdZwSuDCRV6DJRRpiUkbDQ==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "16.4.0", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.4.0.tgz", + "integrity": "sha512-8d5WLS6a9YalqpOgXdlbvonFu+sIgjA+CO6tGzetrpPVcUL56foa7B7664h41rAlbLi5fJwK4vWji3zf4QDy+w==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "16.4.0", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.4.0.tgz", + "integrity": "sha512-sQuOIiW3snb48UwrBcDMvFA/u03qtw2LeuSIxFIQ9AJuW9UG/4Ddsfo+eV73PnZwBhyncMtsKVh7PrzMol2/fA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "16.4.0", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.4.0.tgz", + "integrity": "sha512-6/33lCuxpdHUy27RegyGM/5uTpF7YZd3UR4D0gpkOA/tWY5YR7msbQjqPgCZmSmxrrTLGCXYA4A8vT2H5DPGTQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@shikijs/core": { + "version": "3.23.0", + "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-3.23.0.tgz", + "integrity": "sha512-NSWQz0riNb67xthdm5br6lAkvpDJRTgB36fxlo37ZzM2yq0PQFFzbd8psqC2XMPgCzo1fW6cVi18+ArJ44wqgA==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.23.0", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4", + "hast-util-to-html": "^9.0.5" + } + }, + "node_modules/@shikijs/engine-javascript": { + "version": "3.23.0", + "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-3.23.0.tgz", + "integrity": "sha512-aHt9eiGFobmWR5uqJUViySI1bHMqrAgamWE1TYSUoftkAeCCAiGawPMwM+VCadylQtF4V3VNOZ5LmfItH5f3yA==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.23.0", + "@shikijs/vscode-textmate": "^10.0.2", + "oniguruma-to-es": "^4.3.4" + } + }, + "node_modules/@shikijs/engine-oniguruma": { + "version": "3.23.0", + "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-3.23.0.tgz", + "integrity": "sha512-1nWINwKXxKKLqPibT5f4pAFLej9oZzQTsby8942OTlsJzOBZ0MWKiwzMsd+jhzu8YPCHAswGnnN1YtQfirL35g==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.23.0", + "@shikijs/vscode-textmate": "^10.0.2" + } + }, + "node_modules/@shikijs/langs": { + "version": "3.23.0", + "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-3.23.0.tgz", + "integrity": "sha512-2Ep4W3Re5aB1/62RSYQInK9mM3HsLeB91cHqznAJMuylqjzNVAVCMnNWRHFtcNHXsoNRayP9z1qj4Sq3nMqYXg==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.23.0" + } + }, + "node_modules/@shikijs/themes": { + "version": "3.23.0", + "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-3.23.0.tgz", + "integrity": "sha512-5qySYa1ZgAT18HR/ypENL9cUSGOeI2x+4IvYJu4JgVJdizn6kG4ia5Q1jDEOi7gTbN4RbuYtmHh0W3eccOrjMA==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.23.0" + } + }, + "node_modules/@shikijs/types": { + "version": "3.23.0", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-3.23.0.tgz", + "integrity": "sha512-3JZ5HXOZfYjsYSk0yPwBrkupyYSLpAE26Qc0HLghhZNGTZg/SKxXIIgoxOpmmeQP0RRSDJTk1/vPfw9tbw+jSQ==", + "license": "MIT", + "dependencies": { + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" + } + }, + "node_modules/@shikijs/vscode-textmate": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz", + "integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==", + "license": "MIT" + }, + "node_modules/@swc/helpers": { + "version": "0.5.23", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.23.tgz", + "integrity": "sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@types/hast": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.5.tgz", + "integrity": "sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/node": { + "version": "22.20.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.5.tgz", + "integrity": "sha512-U2+DNr+wSjpsTS/wZGYHq7GcwfuSmKiKvoPvK22zwTlRhU91yOniN4qRR5KhIjvif7ysw/dz/hKmfDH0Ris4aA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/react": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.3.0.tgz", + "integrity": "sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.3.0.tgz", + "integrity": "sha512-ZI7bU42mZXXKHn/qNLEw2IrbiINU7X5+vfgdixBHkCNpYWXjKgfQ/P+uyGb5CjOLB9UcnTeg3rylQtV2hym44Q==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.3.0" + } + }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/@ungap/structured-clone": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.4.0.tgz", + "integrity": "sha512-1mEZtMKPM09vDmQt5y7YvmN2+DFTP7Tg0EWXdic8/C6VRnpb33e4ghisCIE3WZjsE2N8mf+QV1Zqh7ZFYLWInQ==", + "license": "ISC" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.11.28", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.28.tgz", + "integrity": "sha512-br0LOmjJqr6Rt4AEkadbcNf67Zpo7EPwKsWbjnRrJIa1QYFHQBqM4C7F2WTbV8ahTEcg1Eo+Y0aWjjORInYFyA==", + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001815", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001815.tgz", + "integrity": "sha512-uDybUEyfgbyJJ93J1vikTylBX31AkdVYeVPeffMQpf8XN7hSfr0oZOn4VFOIMNvtU5+rHvC5Udg2TgtJKHm23Q==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", + "license": "MIT" + }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/github-slugger": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/github-slugger/-/github-slugger-2.0.0.tgz", + "integrity": "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==", + "license": "ISC" + }, + "node_modules/hast-util-to-html": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz", + "integrity": "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-whitespace": "^3.0.0", + "html-void-elements": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "stringify-entities": "^4.0.0", + "zwitch": "^2.0.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/html-void-elements": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz", + "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/marked": { + "version": "15.0.12", + "resolved": "https://registry.npmjs.org/marked/-/marked-15.0.12.tgz", + "integrity": "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/mdast-util-to-hast": { + "version": "13.2.1", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", + "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-types": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.3.tgz", + "integrity": "sha512-oxB2Ik03hI0gv+VNn9tnh1t1YEe9MDPptViAEgfdf3YQHsn0pzGTgCdlSCJXcwhqm8phaEuM7zeEu3QQzVBrPg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.20", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.20.tgz", + "integrity": "sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/next": { + "version": "16.4.0", + "resolved": "https://registry.npmjs.org/next/-/next-16.4.0.tgz", + "integrity": "sha512-N2C+9q89Z/oLrsepSRS4jRMtwmUZAoZuAHU0e9T6+sAB5DdhADmrnOW+zjYijlrYFmVfmQhGVGy9eMFoB7UgKg==", + "license": "MIT", + "dependencies": { + "@next/env": "16.4.0", + "@swc/helpers": "0.5.23", + "baseline-browser-mapping": "^2.9.19", + "caniuse-lite": "^1.0.30001579", + "postcss": "8.5.23", + "styled-jsx": "5.1.6" + }, + "bin": { + "next": "dist/bin/next" + }, + "engines": { + "node": ">=20.9.0" + }, + "optionalDependencies": { + "@next/swc-darwin-arm64": "16.4.0", + "@next/swc-darwin-x64": "16.4.0", + "@next/swc-linux-arm64-gnu": "16.4.0", + "@next/swc-linux-arm64-musl": "16.4.0", + "@next/swc-linux-x64-gnu": "16.4.0", + "@next/swc-linux-x64-musl": "16.4.0", + "@next/swc-win32-arm64-msvc": "16.4.0", + "@next/swc-win32-x64-msvc": "16.4.0", + "sharp": "^0.35.4" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.1.0", + "@playwright/test": "^1.51.1", + "babel-plugin-react-compiler": "*", + "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "sass": "^1.3.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@playwright/test": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + }, + "sass": { + "optional": true + } + } + }, + "node_modules/oniguruma-parser": { + "version": "0.12.2", + "resolved": "https://registry.npmjs.org/oniguruma-parser/-/oniguruma-parser-0.12.2.tgz", + "integrity": "sha512-6HVa5oIrgMC6aA6WF6XyyqbhRPJrKR02L20+2+zpDtO5QAzGHAUGw5TKQvwi5vctNnRHkJYmjAhRVQF2EKdTQw==", + "license": "MIT" + }, + "node_modules/oniguruma-to-es": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-4.3.6.tgz", + "integrity": "sha512-csuQ9x3Yr0cEIs/Zgx/OEt9iBw9vqIunAPQkx19R/fiMq2oGVTgcMqO/V3Ybqefr1TBvosI6jU539ksaBULJyA==", + "license": "MIT", + "dependencies": { + "oniguruma-parser": "^0.12.2", + "regex": "^6.1.0", + "regex-recursion": "^6.0.2" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.5.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.23.tgz", + "integrity": "sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.16", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/property-information": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.2.0.tgz", + "integrity": "sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/react": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.3.0.tgz", + "integrity": "sha512-E8LUcbtBWt20bbl2YoHfx4ZDBdxVTfOKtCZn9cDSJ4l6/nuoApcpIBcj47t2wZoVX8g2ZHuMHbiShgCR1T5Sog==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.3.0.tgz", + "integrity": "sha512-JDk8dgif51OjFoDE70+OT9ICyYr+69HlmihNwp1+Nsfbna3t5sIiCa9ZJktDmQ4/1b/rn26hIAR2uYXDMr5r0Q==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.28.0" + }, + "peerDependencies": { + "react": "^19.3.0" + } + }, + "node_modules/regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/regex/-/regex-6.1.0.tgz", + "integrity": "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==", + "license": "MIT", + "dependencies": { + "regex-utilities": "^2.3.0" + } + }, + "node_modules/regex-recursion": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/regex-recursion/-/regex-recursion-6.0.2.tgz", + "integrity": "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==", + "license": "MIT", + "dependencies": { + "regex-utilities": "^2.3.0" + } + }, + "node_modules/regex-utilities": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/regex-utilities/-/regex-utilities-2.3.0.tgz", + "integrity": "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==", + "license": "MIT" + }, + "node_modules/scheduler": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.28.0.tgz", + "integrity": "sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sharp": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.5.tgz", + "integrity": "sha512-Ywn4OnzGukp7CDMrp08RQ50YKmuwG47brZgIVPTvBaaAfQlRlygrRqSrxdCiL9M+LlzLBiJ68IR1QqvzHyjC7g==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/colour": "^1.1.0", + "detect-libc": "^2.1.2", + "semver": "^7.8.5" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.35.5", + "@img/sharp-darwin-x64": "0.35.5", + "@img/sharp-freebsd-wasm32": "0.35.5", + "@img/sharp-libvips-darwin-arm64": "1.3.4", + "@img/sharp-libvips-darwin-x64": "1.3.4", + "@img/sharp-libvips-linux-arm": "1.3.4", + "@img/sharp-libvips-linux-arm64": "1.3.4", + "@img/sharp-libvips-linux-ppc64": "1.3.4", + "@img/sharp-libvips-linux-riscv64": "1.3.4", + "@img/sharp-libvips-linux-s390x": "1.3.4", + "@img/sharp-libvips-linux-x64": "1.3.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.3.4", + "@img/sharp-libvips-linuxmusl-x64": "1.3.4", + "@img/sharp-linux-arm": "0.35.5", + "@img/sharp-linux-arm64": "0.35.5", + "@img/sharp-linux-ppc64": "0.35.5", + "@img/sharp-linux-riscv64": "0.35.5", + "@img/sharp-linux-s390x": "0.35.5", + "@img/sharp-linux-x64": "0.35.5", + "@img/sharp-linuxmusl-arm64": "0.35.5", + "@img/sharp-linuxmusl-x64": "0.35.5", + "@img/sharp-webcontainers-wasm32": "0.35.5", + "@img/sharp-win32-arm64": "0.35.5", + "@img/sharp-win32-ia32": "0.35.5", + "@img/sharp-win32-x64": "0.35.5" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/shiki": { + "version": "3.23.0", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-3.23.0.tgz", + "integrity": "sha512-55Dj73uq9ZXL5zyeRPzHQsK7Nbyt6Y10k5s7OjuFZGMhpp4r/rsLBH0o/0fstIzX1Lep9VxefWljK/SKCzygIA==", + "license": "MIT", + "dependencies": { + "@shikijs/core": "3.23.0", + "@shikijs/engine-javascript": "3.23.0", + "@shikijs/engine-oniguruma": "3.23.0", + "@shikijs/langs": "3.23.0", + "@shikijs/themes": "3.23.0", + "@shikijs/types": "3.23.0", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" + } + }, + "node_modules/source-map-js": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz", + "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "license": "MIT", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/styled-jsx": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", + "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==", + "license": "MIT", + "dependencies": { + "client-only": "0.0.1" + }, + "engines": { + "node": ">= 12.0.0" + }, + "peerDependencies": { + "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unist-util-is": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", + "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz", + "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", + "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + } + } +} diff --git a/site/package.json b/site/package.json new file mode 100644 index 0000000..f20524b --- /dev/null +++ b/site/package.json @@ -0,0 +1,34 @@ +{ + "name": "delibera-site", + "version": "10.5.2", + "private": true, + "type": "module", + "description": "Project website for Delibera - static Next.js export published to GitHub Pages", + "license": "MIT", + "engines": { + "node": ">=22" + }, + "scripts": { + "sync-assets": "node scripts/sync-assets.mjs", + "predev": "npm run sync-assets", + "dev": "next dev", + "prebuild": "npm run sync-assets", + "build": "next build", + "verify": "node scripts/verify-export.mjs", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "github-slugger": "^2.0.0", + "marked": "^15.0.7", + "next": "16.4.0", + "react": "^19.3.0", + "react-dom": "^19.3.0", + "shiki": "^3.0.0" + }, + "devDependencies": { + "@types/node": "^22.15.0", + "@types/react": "^19.1.0", + "@types/react-dom": "^19.1.0", + "typescript": "^5.8.3" + } +} \ No newline at end of file diff --git a/site/scripts/serve-export.ps1 b/site/scripts/serve-export.ps1 new file mode 100644 index 0000000..9b58cbc --- /dev/null +++ b/site/scripts/serve-export.ps1 @@ -0,0 +1,51 @@ +param([string]$Root, [int]$Port = 8899) + +# Minimal static server used to eyeball the GitHub Pages export locally. +# +# It mounts the export at /Delibera on purpose: the deployed site lives under that prefix, and +# verifying a build at "/" would hide every missing-basePath bug. Directory requests resolve to +# index.html, which is what Pages does; anything unmatched returns the exported 404.html. + +$listener = New-Object System.Net.HttpListener +$listener.Prefixes.Add("http://localhost:$Port/") +$listener.Start() +Write-Output "serving $Root at http://localhost:$Port/" + +while ($listener.IsListening) { + try { + $ctx = $listener.GetContext() + } catch { + break + } + + $rel = $ctx.Request.Url.AbsolutePath.TrimStart('/') -replace '/', '\' + $path = Join-Path $Root $rel + + if (Test-Path $path -PathType Container) { + $path = Join-Path $path 'index.html' + } + + if (Test-Path $path -PathType Leaf) { + $status = 200 + } else { + $path = Join-Path $Root '404.html' + $status = 404 + } + + try { + $bytes = [System.IO.File]::ReadAllBytes($path) + $ctx.Response.StatusCode = $status + $ctx.Response.ContentType = + if ($path.EndsWith('.html')) { 'text/html; charset=utf-8' } + elseif ($path.EndsWith('.css')) { 'text/css; charset=utf-8' } + elseif ($path.EndsWith('.js')) { 'text/javascript; charset=utf-8' } + elseif ($path.EndsWith('.png')) { 'image/png' } + elseif ($path.EndsWith('.svg')) { 'image/svg+xml' } + else { 'application/octet-stream' } + $ctx.Response.ContentLength64 = $bytes.Length + $ctx.Response.OutputStream.Write($bytes, 0, $bytes.Length) + $ctx.Response.OutputStream.Close() + } catch { + # Client disconnected mid-write; nothing useful to do. + } +} \ No newline at end of file diff --git a/site/scripts/sync-assets.mjs b/site/scripts/sync-assets.mjs new file mode 100644 index 0000000..9891e07 --- /dev/null +++ b/site/scripts/sync-assets.mjs @@ -0,0 +1,24 @@ +// Copies the repository's brand images into `public/img`. +// +// The banner is one file with two consumers - the GitHub README and the website - and +// duplicating a binary in git is how they drift apart. `public/` is Next's only static +// asset root, so the copy has to happen before every dev/build; that is what the +// `predev` / `prebuild` npm hooks are for. The destination is gitignored. +import { copyFileSync, mkdirSync, readdirSync } from 'node:fs' +import { dirname, join } from 'node:path' +import { fileURLToPath } from 'node:url' + +const siteRoot = dirname(dirname(fileURLToPath(import.meta.url))) +const sourceDir = join(siteRoot, '..', 'img') +const targetDir = join(siteRoot, 'public', 'img') + +mkdirSync(targetDir, { recursive: true }) + +const copied = [] +for (const entry of readdirSync(sourceDir, { withFileTypes: true })) { + if (!entry.isFile() || !/\.png$/i.test(entry.name)) continue + copyFileSync(join(sourceDir, entry.name), join(targetDir, entry.name)) + copied.push(entry.name) +} + +console.log(`[sync-assets] ${copied.length} image(s) -> public/img: ${copied.join(', ')}`) \ No newline at end of file diff --git a/site/scripts/verify-export.mjs b/site/scripts/verify-export.mjs new file mode 100644 index 0000000..99ca143 --- /dev/null +++ b/site/scripts/verify-export.mjs @@ -0,0 +1,225 @@ +#!/usr/bin/env node +/** + * Verify the static export before it is published. + * + * `next build` proves the pages compiled; it does not prove they link to each other + * correctly. Three failure modes survive a green build and only show up on the live site: + * + * - an internal href that no emitted file matches (404 for the visitor); + * - a `basePath`-prefixed asset that the export does not actually contain; + * - a route still written in the old mixed case, which GitHub Pages resolves + * case-sensitively while every local check passes. + * + * Exits non-zero on the first class of failure, so CI refuses to deploy a broken artifact. + * + * Usage: node scripts/verify-export.mjs [outDir] + */ +import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs' +import { dirname, join, relative, resolve } from 'node:path' +import { fileURLToPath } from 'node:url' + +const siteRoot = dirname(dirname(fileURLToPath(import.meta.url))) +const outDir = resolve(siteRoot, process.argv[2] ?? 'out') + +if (!existsSync(outDir)) { + console.error(`[verify-export] ${outDir} does not exist - run "npm run build" first`) + process.exit(1) +} + +const basePath = process.env.PAGES_BASE_PATH ?? '' +const problems = [] +const checked = { pages: 0, internalLinks: 0, assets: 0 } + +/** Every file in the export, as forward-slash paths relative to `outDir`. */ +const files = [] +;(function walk(dir) { + for (const entry of readdirSync(dir, { withFileTypes: true })) { + const full = join(dir, entry.name) + if (entry.isDirectory()) walk(full) + else files.push(relative(outDir, full).replace(/\\/g, '/')) + } +})(outDir) + +const fileSet = new Set(files) + +/** + * Does this URL path correspond to a file the export produced? + * + * `trailingSlash: true` means `/blog/10.5.2/` is served by `blog/10.5.2/index.html`, while + * `/sitemap.xml` is a plain file. Both spellings are accepted. + */ +function targetExists(pathname) { + const clean = pathname.replace(/^\/+|\/+$/g, '') + + return ( + fileSet.has(clean) || + fileSet.has(`${clean}/index.html`) || + fileSet.has(`${clean}.html`) + ) +} + +function checkHtml(file) { + const html = readFileSync(join(outDir, file), 'utf8') + checked.pages += 1 + + for (const match of html.matchAll(/(?:href|src)="([^"]+)"/g)) { + const url = match[1] + if (!url || url.startsWith('#') || /^(https?:|mailto:|data:|\/\/)/i.test(url)) continue + + // Root-relative to this site: must carry the deployment base path. + if (!url.startsWith('/')) { + problems.push(`${file}: relative URL "${url}" - it will resolve against the current path`) + continue + } + + if (basePath) { + if (!url.startsWith(`${basePath}/`) && url !== basePath) { + problems.push(`${file}: "${url}" is missing the basePath prefix "${basePath}"`) + continue + } + } + + const pathname = url.slice(basePath.length) + if (pathname === '' || pathname === '/') continue + + const isAsset = /\.(png|jpe?g|gif|svg|webp|avif|css|js|woff2?|xml|txt|ico)$/i.test(pathname) + if (isAsset) checked.assets += 1 + else checked.internalLinks += 1 + + // A fragment addresses a position *within* an existing page, so it is not part of the + // file lookup: `/docs/quickstart/#cost-limits` resolves to `docs/quickstart/index.html` + // plus a scroll position. Checking the whole string would report a false 404. + const [pathPart, fragment] = pathname.split('#') + + if (!targetExists(pathPart)) { + problems.push(`${file}: link target "${url}" has no file in the export`) + continue + } + + // When the target page exists, verify the anchor too. A renamed heading leaves the page + // published but every cross-reference to it dead — which no build error reports. + if (fragment) { + const targetFile = resolveTargetFile(pathPart) + if (targetFile && !anchorsOf(targetFile).has(fragment)) { + problems.push(`${file}: "${url}" points at an anchor that does not exist on the page`) + } + } + } +} + +/** The exported file that actually serves a route path. */ +function resolveTargetFile(pathname) { + const clean = pathname.replace(/^\/+|\/+$/g, '') + const candidates = [clean, `${clean}/index.html`, `${clean}.html`] + return candidates.find((candidate) => fileSet.has(candidate)) +} + +/** Every `id="..."` declared by an exported HTML page, plus its named anchors. */ +const anchorCache = new Map() + +function anchorsOf(file) { + const cached = anchorCache.get(file) + if (cached) return cached + + const html = readFileSync(join(outDir, file), 'utf8') + const anchors = new Set() + for (const match of html.matchAll(/\sid="([^"]+)"/g)) { + if (match[1]) anchors.add(match[1]) + } + + anchorCache.set(file, anchors) + return anchors +} + +for (const file of files) { + if (file.endsWith('.html') || file.endsWith('.txt')) checkHtml(file) +} + +// sitemap.xml must reference only pages that exist, otherwise it advertises 404s. +const sitemap = join(outDir, 'sitemap.xml') +if (existsSync(sitemap)) { + const xml = readFileSync(sitemap, 'utf8') + + for (const match of xml.matchAll(/([^<]+)<\/loc>/g)) { + const loc = match[1] + if (!loc) continue + + // Entries may be absolute or root-relative; both need a base to parse. + let pathname + try { + pathname = new URL(loc, 'https://example.invalid').pathname + } catch { + problems.push(`sitemap.xml: ${loc} is not a valid URL`) + continue + } + + const relativePath = pathname.replace(new RegExp(`^${basePath}`), '').replace(/^\/+/, '') + if (relativePath === '') continue + + if (!targetExists(relativePath)) problems.push(`sitemap.xml: ${loc} has no file`) + } +} + +// The export must not carry source maps or development-only leftovers. +for (const file of files) { + if (/\.map$/.test(file)) problems.push(`${file}: source map shipped to production`) +} + +/** + * Every page must actually load a stylesheet. + * + * A stylesheet that is present in the source tree but never imported produces a build with + * zero CSS files, zero errors, and a site that renders as unstyled default HTML. Nothing in + * `next build` reports it, and the export verification above passes — it only checks links + * that exist, and a missing `` is not a link to anything. + */ +const htmlFiles = files.filter((file) => file.endsWith('.html')) +const pagesWithCss = htmlFiles.filter((file) => { + const html = readFileSync(join(outDir, file), 'utf8') + return /]+rel="stylesheet"/i.test(html) || /]/i.test(html) +}) + +if (files.some((file) => file.endsWith('.css'))) { + if (pagesWithCss.length === 0) { + problems.push('no page links a stylesheet - app/globals.css is probably never imported') + } else if (pagesWithCss.length < htmlFiles.length) { + const unstyled = htmlFiles.filter((file) => !pagesWithCss.includes(file)).slice(0, 5) + problems.push(`${htmlFiles.length - pagesWithCss.length} page(s) render without CSS: ${unstyled.join(', ')}`) + } +} else { + problems.push('the export contains no .css file at all') +} + +/** + * No doubled base path. + * + * `metadataBase` already carries the deployment prefix, and Next joins metadata URLs onto it + * path-wise rather than resolving them as absolute URLs. Passing an image path that *also* + * contains the prefix emits `.../Delibera/Delibera/img/...` — a URL that looks plausible in + * a diff and 404s for every crawler and social card. + */ +if (basePath) { + const doubled = basePath + basePath + for (const file of htmlFiles) { + const html = readFileSync(join(outDir, file), 'utf8') + if (html.includes(doubled)) { + problems.push(`${file}: contains a doubled basePath "${doubled}"`) + } + } +} + +const totalBytes = files.reduce((sum, file) => sum + statSync(join(outDir, file)).size, 0) + +console.log( + `[verify-export] ${checked.pages} html files, ${checked.internalLinks} internal links, ${checked.assets} asset refs, basePath="${basePath}"`, +) +console.log(`[verify-export] export size: ${(totalBytes / 1024 / 1024).toFixed(2)} MB`) + +if (problems.length > 0) { + console.error(`[verify-export] ${problems.length} problem(s):`) + for (const problem of problems.slice(0, 40)) console.error(` - ${problem}`) + if (problems.length > 40) console.error(` ... and ${problems.length - 40} more`) + process.exit(1) +} + +console.log('[verify-export] OK - every internal link and asset resolves inside the export') \ No newline at end of file diff --git a/site/tsconfig.json b/site/tsconfig.json new file mode 100644 index 0000000..f27d026 --- /dev/null +++ b/site/tsconfig.json @@ -0,0 +1,44 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], + "allowJs": false, + "skipLibCheck": true, + "strict": true, + "noUncheckedIndexedAccess": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "react-jsx", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": [ + "./*" + ] + } + }, + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts", + ".next/dev/types/**/*.ts" + ], + "exclude": [ + "node_modules", + "out", + ".next" + ] +} From 60fc71fbcc682ec951dfb32849f678e58aea2e6c Mon Sep 17 00:00:00 2001 From: Viktor Buzin Date: Sat, 10 Oct 2026 21:35:59 +0300 Subject: [PATCH 03/13] BuiltIn refactoring --- docs/NET10-Upgrade.md | 45 +- docs/performance-measurements.md | 54 + global.json | 6 + src/Delibera.Core/BuiltIn.cs | 353 ++++ src/Delibera.Core/Chunking/AutoChunker.cs | 5 +- .../Compression/DeduplicationCompressor.cs | 17 +- .../Compression/SemanticCompressor.cs | 8 +- src/Delibera.Core/Council/Chairman.cs | 16 +- src/Delibera.Core/Council/CouncilBuilder.cs | 7 +- src/Delibera.Core/Council/KnowledgeKeeper.cs | 5 +- .../Council/LocalDebateOrchestrator.cs | 37 +- src/Delibera.Core/Council/Operator.cs | 4 +- .../DependencyInjection/CouncilOptions.cs | 3 +- src/Delibera.Core/Models/CouncilMember.cs | 2 +- .../Providers/ProviderFactory.cs | 2 +- src/Delibera.Core/Templates/DebateTemplate.cs | 2 +- src/Delibera.Core/Voting/IVotingStrategy.cs | 21 +- src/Delibera.Redis/DebateWorkerService.cs | 7 +- src/Delibera.Redis/Delibera.Redis.csproj | 1 + src/Delibera.Redis/RedisDebateOrchestrator.cs | 61 +- .../RedisOrchestratorOptions.cs | 14 +- .../Serialization/RedisSerializer.cs | 3 +- .../Api/Endpoints/DebateEndpoints.cs | 3 +- .../Api/Endpoints/ScenarioEndpoints.cs | 3 +- .../Api/Filters/ValidationFilter.cs | 18 +- .../Infrastructure/DeliberaServerOptions.cs | 4 +- .../ProblemDetailsExceptionHandler.cs | 3 +- .../Infrastructure/RedisGateOptions.cs | 4 +- .../Infrastructure/ServerServiceExtensions.cs | 5 +- .../Middleware/CorrelationIdMiddleware.cs | 9 +- .../Middleware/TenantResolutionMiddleware.cs | 4 +- .../Scenarios/ScenarioBuilder.cs | 9 +- .../Services/DebateOrchestrationService.cs | 56 +- .../Services/ServerRagProviderFactory.cs | 41 +- .../Sse/SseDebateStreamWriter.cs | 120 +- .../Engineering/CodeReviewTemplate.cs | 19 +- .../ArchitectureDecisionTemplate.cs | 19 +- .../Governance/RiskCommitteeTemplate.cs | 22 +- .../Legal/LegalContractReviewTemplate.cs | 19 +- .../Product/RequirementsReviewTemplate.cs | 19 +- .../Templates/TemplateKnowledge.cs | 9 +- src/Delibera.WebUI/package-lock.json | 1853 +++++++++-------- src/Delibera.WebUI/package.json | 8 +- .../Endpoints/RequestValidationTests.cs | 44 + .../Sse/SseHeartbeatTests.cs | 89 + 45 files changed, 1952 insertions(+), 1101 deletions(-) create mode 100644 global.json create mode 100644 src/Delibera.Core/BuiltIn.cs diff --git a/docs/NET10-Upgrade.md b/docs/NET10-Upgrade.md index c6bee4d..0543ca9 100644 --- a/docs/NET10-Upgrade.md +++ b/docs/NET10-Upgrade.md @@ -39,18 +39,49 @@ The changes were made in the files: ### ⚠️ Important note about "C# 15" -At the time of the upgrade, the GA release **.NET SDK 10.0.301** is installed, in which the Roslyn -compiler supports language versions up to and including **14.0**, while the next one (the future -C# 15) is available **only** through the `preview` value. Specifying `15.0` -directly results in a compilation error: +At the time of the original upgrade, the installed SDK was **10.0.301**, whose Roslyn compiler +supports language versions up to and including **14.0**; the next one (the future C# 15) was +available **only** through the `preview` value. Specifying `15.0` +directly resulted in a compilation error: ``` error CS1617: Invalid option '15.0' for /langversion ``` -Therefore `preview` is used — this enables the maximum available set of -language features of the next version (C# 15). When an SDK with official `15.0` support is released, -the value can be replaced with `15.0` without any other changes to the code. +Therefore `preview` is used. When an SDK with official `15.0` support +is released, the value can be replaced with `15.0`. + +#### What `preview` actually buys, measured + +`LangVersion=preview` means different things on different SDKs, because the feature set comes from +the **compiler**, not from the target framework. Two SDKs of the same major band are not +equivalent here: + +| SDK | Roslyn | Stable language level | C# 15 features present | +|-----|--------|----------------------|------------------------| +| 9.0.318 | 4.14 | C# 13 | none | +| 10.0.112 | 5.0 | C# 14 | none | +| **10.0.401** | **5.9** | C# 14 | collection expression arguments, labeled `break`/`continue`, extension indexers | + +`global.json` now pins `10.0.401` (`rollForward: latestFeature`) so a machine holding only +10.0.112 cannot silently compile this codebase without the features it is written against. + +The gate was run by compiling each feature against `net10.0`, not by reading release notes: + +| Feature | Compiles on net10.0? | Note | +|---------|----------------------|------| +| Collection expression arguments — `[with(capacity: n), …]` | **yes** | used on the hot paths | +| Labeled `break` / `continue` | **yes** | ergonomics only; no hot-path candidate | +| Extension indexers | **yes** | no candidate in this codebase | +| Union types | **no** | needs `System.Runtime.CompilerServices.IUnion` | +| Closed hierarchies | **no** | needs `System.Runtime.CompilerServices.IsClosedTypeAttribute` | + +Union types and closed hierarchies are blocked by the **BCL**, not by the compiler: `IUnion` and +`IsClosedTypeAttribute` ship with **.NET 11**, so on `net10.0` the compiler rejects them with +`error CS0518: Predefined type 'System.Runtime.CompilerServices.IUnion' is not defined` and +`error CS0656: Missing compiler required member '…IsClosedTypeAttribute..ctor'` respectively. +They are therefore deferred to the `net11.0` retarget, together with the runtime work that +retarget unlocks (Runtime Async, JIT improvements, `System.Text.Json` union/JSONL support). --- diff --git a/docs/performance-measurements.md b/docs/performance-measurements.md index 02e9a1e..39b2f01 100644 --- a/docs/performance-measurements.md +++ b/docs/performance-measurements.md @@ -411,6 +411,60 @@ were git worktrees at `1297e38` and `978f4cb`. --- +## 9. C# 15 pass — what it did and did not move + +A pass adopted the C# 15 features that actually compile on `net10.0` and fixed three per-request +costs on the server side. The honest result is that **the framework-overhead benchmark does not +show it**, and the reason matters more than the number. + +### 9.1 The measured result: no change, and not noise-hiding a small win + +Same harness, same workload, baseline extracted from `HEAD` with `git archive` and built via +`-p:DeliberaCorePath=`. Three interleaved pairs, 25 debates each, after two warm-ups: + +| Run | Median (baseline) | Median (after) | Allocations, first debate | +|---|---|---|---| +| 1 | 1.06 ms | 1.06 ms | 4 730 408 B / 4 730 408 B | +| 2 | 0.86 ms | 1.45 ms | 4 730 344 B / 4 722 768 B | +| 3 | 1.05 ms | 1.09 ms | 4 722 768 B / 4 722 768 B | + +Allocations are equal, and the run-to-run spread (0.86–1.45 ms) is wider than the difference +between the two sides. Per the rule this document already follows — sub-millisecond differences +are not signal — **there is no measurable change here, in either direction.** + +That is expected rather than disappointing: the harness drives a three-member debate with a fake +provider, which exercises orchestration and prompt assembly. It does not chunk documents, does not +compress context, does not run a voting strategy and does not search a vector store. Those are +exactly the paths that were changed. + +### 9.2 What was changed, and where it should show up + +| Change | Path | Expected to matter on | +|---|---|---| +| `[with(capacity: n)]` on voting score tables, chunk lists, compression buffers, RAG merge | per round / per chunk | debates with context compression, chunked corpora, multi-corpus RAG | +| Closed generic `IValidator<>` per argument → cached map | per request, per argument | every HTTP request | +| Second `ConnectionMultiplexer` → reuse the DI singleton | process lifetime | any deployment using `Delibera.Redis` | +| SSE frame: 6 writes + flush → one `PipeWriter` frame | per SSE event | every streamed debate | + +The server-side changes are **not measured here at all** — `.bench/ServerBench/Measure-Server.ps1` +drives a running container on `:5200` across REST, SSE, MCP and corpus ingest, and it was not run +as part of this pass. Their value is structural and static: one fewer reflection call per request +argument, one fewer TCP pool per process, three fewer writes per event frame. Those are argued +from the code, not measured, and are labelled as such until someone runs the server harness. + +### 9.3 Regression coverage added + +Two tests were added rather than relying on the numbers above: + +- `RequestValidationTests.Validator_Type_Is_Resolved_Once_Per_Contract_Not_Per_Request` fails if the + cached validator type disappears, i.e. if the per-request `MakeGenericType` call returns. +- `SseHeartbeatTests.Round_Is_Flushed_While_The_Debate_Is_Still_Running` withholds the terminal + event and fails unless the round reaches the wire within three seconds. It was verified to have + teeth: removing the per-frame flush makes it fail, which is the buffering regression this + codebase has hit before. + +--- + ## Reproducing | harness | what it measures | diff --git a/global.json b/global.json new file mode 100644 index 0000000..ee6c4c9 --- /dev/null +++ b/global.json @@ -0,0 +1,6 @@ +{ + "sdk": { + "version": "10.0.401", + "rollForward": "latestFeature" + } +} \ No newline at end of file diff --git a/src/Delibera.Core/BuiltIn.cs b/src/Delibera.Core/BuiltIn.cs new file mode 100644 index 0000000..8b550bf --- /dev/null +++ b/src/Delibera.Core/BuiltIn.cs @@ -0,0 +1,353 @@ +using System.Text.Json; + +namespace Delibera.Core; + +/// +/// Built-in constants, presets and default values shared across all Delibera +/// projects. Eliminates duplicated magic strings and numbers that would +/// otherwise drift apart between Delibera.Core, Delibera.Redis, Delibera.Server +/// and consumer applications. +/// +/// +/// +/// Every constant here is a const (or static readonly for +/// non-primitive types) so the value is inlined at compile time — there is +/// no runtime cost and no allocation. Consumers reference the constant +/// directly (BuiltIn.HttpHeaders.CorrelationId) instead of copying +/// the literal into a private field. +/// +/// +/// Grouping is by domain: , +/// , , +/// , , , +/// , , , +/// , , . +/// +/// +/// SSE event names () are a contract with +/// the Web UI front-end. Changing a value here requires a matching change +/// in Delibera.WebUI. +/// +/// +public static class BuiltIn +{ + /// + /// Standard HTTP header names used across the server, middleware and + /// client code. + /// + public static class HttpHeaders + { + /// Correlation identifier header — X-Correlation-Id. + public const string CorrelationId = "X-Correlation-Id"; + + /// Tenant identifier header — X-Tenant-Id. + public const string TenantId = "X-Tenant-Id"; + + /// Nginx proxy buffering hint — X-Accel-Buffering. + public const string AccelBuffering = "X-Accel-Buffering"; + } + + /// + /// Common HTTP header values (content types, cache directives, auth scheme). + /// + public static class HttpHeaderValues + { + /// Server-Sent Events content type — text/event-stream. + public const string EventStream = "text/event-stream"; + + /// Disable caching directive — no-cache. + public const string NoCache = "no-cache"; + + /// Negative value used for buffering / connection toggles — no. + public const string No = "no"; + + /// JSON content type — application/json. + public const string ApplicationJson = "application/json"; + + /// Bearer authentication scheme — Bearer. + public const string Bearer = "Bearer"; + } + + /// + /// Server-Sent Events event type names. Contract with Delibera.WebUI — + /// the front-end listens for these exact strings; changing a value here + /// requires a matching change in the Web UI event map. + /// + public static class SseEvents + { + /// A debate round was produced — debate-round. + public const string Round = "debate-round"; + + /// The debate finished successfully — debate-completed. + public const string Completed = "debate-completed"; + + /// The debate failed — debate-error. + public const string Error = "debate-error"; + + /// The debate was cancelled — debate-cancelled. + public const string Cancelled = "debate-cancelled"; + + /// Keep-alive comment sent on idle — keep-alive. + public const string KeepAlive = "keep-alive"; + + /// Reconnect hint retry interval — retry: 3000. + public const string Retry = "retry: 3000"; + } + + /// + /// Top-level configuration section names used in appsettings.json + /// and . + /// + public static class ConfigSections + { + /// Root Delibera configuration section — Delibera. + public const string Root = "Delibera"; + + /// Redis orchestrator configuration section — Delibera:Redis. + public const string Redis = "Delibera:Redis"; + + /// Server configuration section — Delibera:Server. + public const string Server = "Delibera:Server"; + } + + /// + /// Fully-qualified configuration keys (section + key path) used with + /// indexer. + /// + public static class ConfigKeys + { + /// Default LLM provider endpoint — Delibera:Providers:DefaultEndpoint. + public const string ProvidersDefaultEndpoint = "Delibera:Providers:DefaultEndpoint"; + + /// Default LLM provider API key — Delibera:Providers:ApiKey. + public const string ProvidersApiKey = "Delibera:Providers:ApiKey"; + + /// Embedding model name — Delibera:Providers:EmbeddingModel. + public const string ProvidersEmbeddingModel = "Delibera:Providers:EmbeddingModel"; + + /// Fast/cheap model name — Delibera:Models:Fast. + public const string ModelsFast = "Delibera:Models:Fast"; + + /// Strong/expensive model name — Delibera:Models:Strong. + public const string ModelsStrong = "Delibera:Models:Strong"; + + /// RAG feature toggle — Delibera:Rag:Enabled. + public const string RagEnabled = "Delibera:Rag:Enabled"; + + /// RAG provider type — Delibera:Rag:ProviderType. + public const string RagProviderType = "Delibera:Rag:ProviderType"; + } + + /// + /// Default LLM model identifiers used when no model is explicitly + /// configured. These are Ollama model tags. + /// + public static class Models + { + /// Default fast/cheap model — llama3.2:3b. + public const string DefaultFast = "llama3.2:3b"; + + /// Default strong/expensive model — qwen2.5:7b. + public const string DefaultStrong = "qwen2.5:7b"; + + /// Default embedding model — nomic-embed-text. + public const string DefaultEmbedding = "nomic-embed-text"; + } + + /// + /// Default service endpoints for local and cloud LLM providers. + /// + public static class Endpoints + { + /// Local Ollama endpoint — http://localhost:11434. + public const string OllamaLocal = "http://localhost:11434"; + + /// Ollama cloud endpoint — https://api.ollama.com. + public const string OllamaCloud = "https://api.ollama.com"; + } + + /// + /// Shared timeout values. Declared as static readonly because + /// is a struct and cannot be const. + /// + public static class Timeouts + { + /// + /// How long a completed debate record is retained in orchestrator caches + /// before eviction — 30 minutes. + /// + public static readonly TimeSpan CompletedDebateLifetime = TimeSpan.FromMinutes(30); + + /// + /// SSE heartbeat interval — 15 seconds. + /// + public static readonly TimeSpan SseHeartbeat = TimeSpan.FromSeconds(15); + } + + /// + /// Redis key prefixes, stream names, consumer-group names and hash/stream + /// field names shared between Delibera.Redis and Delibera.Server. + /// + public static class Redis + { + /// Stream key for pending debate jobs — delibera:jobs. + public const string JobStreamKey = "delibera:jobs"; + + /// Stream key for debate round events — delibera:events. + public const string EventStreamKey = "delibera:events"; + + /// Key prefix for per-debate state hashes — delibera:state:. + public const string StateKeyPrefix = "delibera:state:"; + + /// Consumer group name for the orchestrator — orchestrator. + public const string OrchestratorGroup = "orchestrator"; + + /// Consumer group name for workers — workers. + public const string WorkerGroup = "workers"; + + /// + /// Hash field names and stream entry field names used in Redis payloads. + /// + public static class Fields + { + /// Debate status field — status. + public const string Status = "status"; + + /// Last-updated timestamp field — updatedAt. + public const string UpdatedAt = "updatedAt"; + + /// Debate result payload field — result. + public const string Result = "result"; + + /// Error message field — errorMessage. + public const string ErrorMessage = "errorMessage"; + + /// Debate identifier stream entry field — debateId. + public const string DebateId = "debateId"; + + /// Event type stream entry field — eventType. + public const string EventType = "eventType"; + + /// Event payload stream entry field — payload. + public const string Payload = "payload"; + + /// Job type stream entry field — jobType. + public const string JobType = "jobType"; + + /// Round-completed event type value — round-completed. + public const string RoundCompleted = "round-completed"; + } + } + + /// + /// Named client identifiers + /// registered in DI. + /// + public static class HttpClients + { + /// Local Ollama HTTP client name — Delibera.Ollama.Local. + public const string OllamaLocal = "Delibera.Ollama.Local"; + + /// Cloud Ollama HTTP client name — Delibera.Ollama.Cloud. + public const string OllamaCloud = "Delibera.Ollama.Cloud"; + + /// YandexGPT HTTP client name — Delibera.YandexGPT. + public const string YandexGpt = "Delibera.YandexGPT"; + } + + /// + /// Well-known council member role names. + /// + public static class Roles + { + /// Standard chairman role — Chairman. + public const string Chairman = "Chairman"; + + /// Voting chairman role — Voting Chairman. + public const string VotingChairman = "Voting Chairman"; + + /// Default expert member role — Expert. + public const string Expert = "Expert"; + } + + /// + /// Metadata key names used in vector-store payloads, agent memory records + /// and council execution metadata dictionaries. + /// + public static class MetadataKeys + { + /// Agent name metadata key — agent_name. + public const string AgentName = "agent_name"; + + /// Debate identifier metadata key — debate_id. + public const string DebateId = "debate_id"; + + /// Member role metadata key — role. + public const string Role = "role"; + + /// Source file path metadata key — source_path. + public const string SourcePath = "source_path"; + + /// Source name metadata key — source. + public const string Source = "source"; + + /// Chunk text payload key — text. + public const string Text = "text"; + + /// Topic metadata key — topic. + public const string Topic = "topic"; + } + + /// + /// Shared instances. Reusing a single + /// instance avoids per-call allocation and keeps serialisation behaviour + /// consistent across caches, SSE writers and persistence layers. + /// + public static class Json + { + /// + /// Default options: camelCase property naming, null values omitted. + /// + public static readonly JsonSerializerOptions Default = new() + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull + }; + + /// + /// Web-friendly options using + /// (camelCase + case-insensitive reads). + /// + public static readonly JsonSerializerOptions Web = new(JsonSerializerDefaults.Web); + } + + /// + /// Well-known file-system directory and collection names. + /// + public static class Paths + { + /// Default directory for debate result exports — debate_results. + public const string DebateResults = "debate_results"; + + /// Default directory for debate checkpoints — debate_checkpoints. + public const string DebateCheckpoints = "debate_checkpoints"; + + /// Default vector-store collection for council knowledge — council_knowledge. + public const string CouncilKnowledge = "council_knowledge"; + + /// Agent memory collection/table name prefix — agent_memory_. + public const string AgentMemoryPrefix = "agent_memory_"; + + /// Agent memory default table name — agent_memory. + public const string AgentMemoryTable = "agent_memory"; + } + + /// Redis consumer-group error marker — BUSYGROUP. + public const string BusyGroup = "BUSYGROUP"; + + /// CORS policy name — DeliberaCors. + public const string CorsPolicyName = "DeliberaCors"; + + /// MCP tenant identifier — mcp. + public const string McpTenantId = "mcp"; +} diff --git a/src/Delibera.Core/Chunking/AutoChunker.cs b/src/Delibera.Core/Chunking/AutoChunker.cs index 16bde4f..0652d57 100644 --- a/src/Delibera.Core/Chunking/AutoChunker.cs +++ b/src/Delibera.Core/Chunking/AutoChunker.cs @@ -192,7 +192,10 @@ public static ChunkingPlan CreatePlan( private static List ChunkBySemanticBoundaries(string doc, int maxTokens) { var sections = SplitByHeaders(doc); - var chunks = new List(); + // Every section yields at least one chunk, and an oversized one yields more, so the + // section count is a floor rather than a guess: it sizes the backing array for the + // common case where sections fit whole. + List chunks = [with(capacity: sections.Count)]; var index = 0; foreach (var (title, content, start, end) in sections) diff --git a/src/Delibera.Core/Compression/DeduplicationCompressor.cs b/src/Delibera.Core/Compression/DeduplicationCompressor.cs index bede7c9..c9c48dc 100644 --- a/src/Delibera.Core/Compression/DeduplicationCompressor.cs +++ b/src/Delibera.Core/Compression/DeduplicationCompressor.cs @@ -73,11 +73,17 @@ private async Task> DeduplicateWithEmbeddingsAsync( double threshold, CancellationToken ct) { - var texts = sentences.Select(s => s.Text).ToList(); + // One embedding per sentence, and the provider takes the batch as a list. + List texts = [with(capacity: sentences.Count)]; + foreach (var s in sentences) + texts.Add(s.Text); + var vectors = await _embeddingProvider!.EmbedBatchAsync(texts, ct).ConfigureAwait(false); - var kept = new List(); - var keptVectors = new List(); + // Nothing is ever added more than once, so the sentence count is an exact ceiling for + // both lists — deduplication can only shrink them. + List kept = [with(capacity: sentences.Count)]; + List keptVectors = [with(capacity: sentences.Count)]; for (var i = 0; i < sentences.Count; i += BatchSize) { @@ -113,8 +119,9 @@ private static List DeduplicateWithHeuristics( List sentences, double threshold) { - var kept = new List(); - var keptSets = new List(); + // As above: at most one entry per input sentence, so the input count is the exact ceiling. + List kept = [with(capacity: sentences.Count)]; + List keptSets = [with(capacity: sentences.Count)]; foreach (var s in sentences) { diff --git a/src/Delibera.Core/Compression/SemanticCompressor.cs b/src/Delibera.Core/Compression/SemanticCompressor.cs index dfd4dc6..edcbb66 100644 --- a/src/Delibera.Core/Compression/SemanticCompressor.cs +++ b/src/Delibera.Core/Compression/SemanticCompressor.cs @@ -91,11 +91,13 @@ private static List SelectTopSentences( TokenCounter counter, CompressionOptions options) { - var sorted = scored.OrderByDescending(s => s.Score).ToList(); - var selected = new List(); + // Ordered but never materialised: the loop below consumes every element, so sorting into a + // throwaway list first only bought a second copy of the same sentences. + // At most one sentence per input can be selected, which sizes the result up front. + List selected = [with(capacity: scored.Count)]; var currentTokens = 0; - foreach (var s in sorted) + foreach (var s in scored.OrderByDescending(s => s.Score)) { // Preserve code blocks and structured content if requested if (options.PreserveCodeBlocks && (s.Text.Contains("```") || s.Text.TrimStart().StartsWith(" "))) diff --git a/src/Delibera.Core/Council/Chairman.cs b/src/Delibera.Core/Council/Chairman.cs index 8bee8ca..b049a6d 100644 --- a/src/Delibera.Core/Council/Chairman.cs +++ b/src/Delibera.Core/Council/Chairman.cs @@ -20,9 +20,9 @@ public static class Chairman // ────────────────────────────────────────────── /// Creates a standard, neutral Chairman. - public static CouncilMember CreateStandard(string modelName, ILLMProvider provider) - { - return new CouncilMember(modelName, provider, "Chairman", +public static CouncilMember CreateStandard(string modelName, ILLMProvider provider) + { + return new CouncilMember(modelName, provider, BuiltIn.Roles.Chairman, """ You are the Chairman of an AI Council debate — neutral, objective and fair. You organise the debate process, ensure every participant is heard, @@ -54,9 +54,9 @@ You are a creative Chairman who looks beyond conventional answers. } /// Creates a Chairman with a custom persona prompt. - public static CouncilMember CreateCustom(string modelName, ILLMProvider provider, string personaPrompt) - { - return new CouncilMember(modelName, provider, "Chairman", personaPrompt); +public static CouncilMember CreateCustom(string modelName, ILLMProvider provider, string personaPrompt) + { + return new CouncilMember(modelName, provider, BuiltIn.Roles.Chairman, personaPrompt); } /// @@ -84,8 +84,8 @@ public static CouncilMember CreateVoting(string modelName, ILLMProvider provider // Encode the strategy as a special marker in the persona prompt so the executor // can detect it without changing the CouncilMember type. The marker is a // well-known prefix that CouncilExecutor checks for. - var persona = $"{VotingChairmanMarker}{votingStrategy.MethodName}:{votingStrategy.GetType().FullName}"; - return new CouncilMember(modelName, provider, "Voting Chairman", persona); +var persona = $"{VotingChairmanMarker}{votingStrategy.MethodName}:{votingStrategy.GetType().FullName}"; + return new CouncilMember(modelName, provider, BuiltIn.Roles.VotingChairman, persona); } // ────────────────────────────────────────────── diff --git a/src/Delibera.Core/Council/CouncilBuilder.cs b/src/Delibera.Core/Council/CouncilBuilder.cs index 18cf010..f1e925c 100644 --- a/src/Delibera.Core/Council/CouncilBuilder.cs +++ b/src/Delibera.Core/Council/CouncilBuilder.cs @@ -13,6 +13,7 @@ namespace Delibera.Core.Council; + /// /// Fluent builder for assembling and configuring a council debate session. /// @@ -146,14 +147,14 @@ public ICouncilBuilder SetChairman(CouncilMember chairman) { ArgumentNullException.ThrowIfNull(chairman); _chairman = chairman; - _chairman.Role = "Chairman"; + _chairman.Role = BuiltIn.Roles.Chairman; return this; } /// public ICouncilBuilder SetChairman(string modelName, ILLMProvider provider, string? persona = null) { - _chairman = new CouncilMember(modelName, provider, "Chairman", persona); + _chairman = new CouncilMember(modelName, provider, BuiltIn.Roles.Chairman, persona); return this; } @@ -714,7 +715,7 @@ public ICouncilBuilder WithKnowledgeKeeper( IRagProvider ragProvider, string modelName, ILLMProvider llmProvider, - string collectionName = "council_knowledge") + string collectionName = BuiltIn.Paths.CouncilKnowledge) { ArgumentNullException.ThrowIfNull(ragProvider); ArgumentNullException.ThrowIfNull(llmProvider); diff --git a/src/Delibera.Core/Council/KnowledgeKeeper.cs b/src/Delibera.Core/Council/KnowledgeKeeper.cs index 620286f..52b837a 100644 --- a/src/Delibera.Core/Council/KnowledgeKeeper.cs +++ b/src/Delibera.Core/Council/KnowledgeKeeper.cs @@ -82,7 +82,10 @@ private async Task> SearchAllAsync( // corpus for exactly `limit` and merging afterwards would let the largest corpus fill the // whole budget before a smaller one contributed anything. var perCollection = Math.Max(limit, limit * 2); - var merged = new List(); + // Each corpus is asked for exactly perCollection hits, so the final count is known + // before the first call — the merge below can allocate it in one go instead of + // growing (and copying) the list once per corpus. + List merged = [with(capacity: Collections.Count * perCollection)]; foreach (var collection in Collections) { diff --git a/src/Delibera.Core/Council/LocalDebateOrchestrator.cs b/src/Delibera.Core/Council/LocalDebateOrchestrator.cs index b25b065..3a0825e 100644 --- a/src/Delibera.Core/Council/LocalDebateOrchestrator.cs +++ b/src/Delibera.Core/Council/LocalDebateOrchestrator.cs @@ -9,33 +9,26 @@ namespace Delibera.Core.Council; /// This is the default implementation — no external infrastructure required. /// Round events are published via an in-process channel. /// -/// Completed entries are automatically evicted after +/// Completed entries are automatically evicted after /// to prevent unbounded memory growth. The default lifetime is 30 minutes. /// /// public sealed class LocalDebateOrchestrator : IDebateOrchestrator, IDisposable { - /// - /// Default lifetime for completed debate entries before they are evicted - /// from the in-memory dictionary. Set to to - /// disable eviction (entries remain indefinitely). - /// - public static readonly TimeSpan CompletedEntryLifetime = TimeSpan.FromMinutes(30); - - private readonly TimeSpan _completedEntryLifetime; - private readonly ConcurrentDictionary _entries = new(); - private readonly Timer? _evictionTimer; - - /// - /// Creates a new . - /// - /// - /// How long completed entries are kept before eviction. - /// Defaults to (30 minutes). - /// - public LocalDebateOrchestrator(TimeSpan? completedEntryLifetime = null) - { - _completedEntryLifetime = completedEntryLifetime ?? CompletedEntryLifetime; + private readonly TimeSpan _completedEntryLifetime; + private readonly ConcurrentDictionary _entries = new(); + private readonly Timer? _evictionTimer; + + /// + /// Creates a new . + /// + /// + /// How long completed entries are kept before eviction. + /// Defaults to (30 minutes). + /// + public LocalDebateOrchestrator(TimeSpan? completedEntryLifetime = null) + { + _completedEntryLifetime = completedEntryLifetime ?? BuiltIn.Timeouts.CompletedDebateLifetime; if (_completedEntryLifetime > TimeSpan.Zero) _evictionTimer = new Timer(EvictCompletedEntries, new WeakReference(this), _completedEntryLifetime, _completedEntryLifetime); diff --git a/src/Delibera.Core/Council/Operator.cs b/src/Delibera.Core/Council/Operator.cs index 806767a..6924eb9 100644 --- a/src/Delibera.Core/Council/Operator.cs +++ b/src/Delibera.Core/Council/Operator.cs @@ -70,8 +70,8 @@ public Operator( _compressor = compressor; _compressionOptions = compressionOptions; - if (_model.Role is null or "Expert") - _model.Role = "Operator"; + if (_model.Role is null or BuiltIn.Roles.Expert) + _model.Role = "Operator"; } /// diff --git a/src/Delibera.Core/DependencyInjection/CouncilOptions.cs b/src/Delibera.Core/DependencyInjection/CouncilOptions.cs index 445906c..c0e3488 100644 --- a/src/Delibera.Core/DependencyInjection/CouncilOptions.cs +++ b/src/Delibera.Core/DependencyInjection/CouncilOptions.cs @@ -3,6 +3,7 @@ namespace Delibera.Core.DependencyInjection; + /// /// Root configuration options for the Delibera framework. /// Bind to a configuration section (e.g., "Delibera") via IOptions<CouncilOptions>. @@ -10,7 +11,7 @@ namespace Delibera.Core.DependencyInjection; public sealed class CouncilOptions { /// Configuration section name (default: "Delibera"). - public const string SectionName = "Delibera"; + public const string SectionName = BuiltIn.ConfigSections.Root; /// Default debate strategy name (e.g., "Standard", "Critique", "Consensus"). public string Strategy { get; set; } = "Standard"; diff --git a/src/Delibera.Core/Models/CouncilMember.cs b/src/Delibera.Core/Models/CouncilMember.cs index 67f945c..9252524 100644 --- a/src/Delibera.Core/Models/CouncilMember.cs +++ b/src/Delibera.Core/Models/CouncilMember.cs @@ -24,7 +24,7 @@ public sealed class CouncilMember( public ILLMProvider Provider { get; } = provider ?? throw new ArgumentNullException(nameof(provider)); /// Role in the debate (Expert, Critic, Chairman, etc.). - public string Role { get; set; } = role ?? "Expert"; + public string Role { get; set; } = role ?? BuiltIn.Roles.Expert; /// Optional persona system-prompt that personalises the model's behaviour. public string? PersonaPrompt { get; set; } = personaPrompt; diff --git a/src/Delibera.Core/Providers/ProviderFactory.cs b/src/Delibera.Core/Providers/ProviderFactory.cs index 1448d8e..ab1f436 100644 --- a/src/Delibera.Core/Providers/ProviderFactory.cs +++ b/src/Delibera.Core/Providers/ProviderFactory.cs @@ -103,7 +103,7 @@ public ProviderFactory() { RegisterBuilder("Ollama", config => { - var endpoint = config["Endpoint"] ?? "http://localhost:11434"; + var endpoint = config["Endpoint"] ?? BuiltIn.Endpoints.OllamaLocal; var apiKey = config["ApiKey"] ?? ""; var maxOutputTokens = int.TryParse(config["MaxOutputTokens"], out var t) ? t diff --git a/src/Delibera.Core/Templates/DebateTemplate.cs b/src/Delibera.Core/Templates/DebateTemplate.cs index 9051b56..7ee67a2 100644 --- a/src/Delibera.Core/Templates/DebateTemplate.cs +++ b/src/Delibera.Core/Templates/DebateTemplate.cs @@ -137,7 +137,7 @@ public DebateTemplateBase WithParticipantLimit(int maxParticipants) /// Attaches a Knowledge Keeper built from a RAG provider (F-04 / existing). /// public DebateTemplateBase WithKnowledgeKeeper(IRagProvider ragProvider, string modelName, - string collectionName = "council_knowledge") + string collectionName = BuiltIn.Paths.CouncilKnowledge) { if (Provider is null) throw new InvalidOperationException("Call WithProvider(...) before WithKnowledgeKeeper(...)."); diff --git a/src/Delibera.Core/Voting/IVotingStrategy.cs b/src/Delibera.Core/Voting/IVotingStrategy.cs index 309116a..66e9152 100644 --- a/src/Delibera.Core/Voting/IVotingStrategy.cs +++ b/src/Delibera.Core/Voting/IVotingStrategy.cs @@ -78,7 +78,8 @@ public Task TallyAsync(IReadOnlyList ballots, C ArgumentNullException.ThrowIfNull(ballots); ct.ThrowIfCancellationRequested(); - var scores = new Dictionary(StringComparer.Ordinal); + // At most one distinct winner name per ballot, so the ballot count is an exact bound. + Dictionary scores = [with(capacity: ballots.Count, comparer: StringComparer.Ordinal)]; foreach (var ballot in ballots) { if (ballot.Rankings.Count == 0) continue; @@ -117,15 +118,18 @@ public Task TallyAsync(IReadOnlyList ballots, C ArgumentNullException.ThrowIfNull(ballots); ct.ThrowIfCancellationRequested(); - var scores = new Dictionary(StringComparer.Ordinal); + // A ballot can name options that appear on other ballots too, so ballots.Count is a + // starting size rather than a hard cap; the dictionary grows if a survey disagrees. + Dictionary scores = [with(capacity: ballots.Count, comparer: StringComparer.Ordinal)]; foreach (var ballot in ballots) { - var ranked = ballot.Rankings.OrderBy(r => r.Rank).ToList(); - var n = ranked.Count; - for (var i = 0; i < n; i++) + // Ranked in rank order without materialising the list: the points descend from n-1, + // so the position is tracked as a running value instead of an index into a copy. + var bordaPoints = ballot.Rankings.Count - 1; + foreach (var ranked in ballot.Rankings.OrderBy(r => r.Rank)) { - var bordaPoints = n - 1 - i; // top rank (i=0) gets n-1 points - scores[ranked[i].Name] = scores.GetValueOrDefault(ranked[i].Name) + bordaPoints; + scores[ranked.Name] = scores.GetValueOrDefault(ranked.Name) + bordaPoints; + bordaPoints--; } } @@ -172,7 +176,8 @@ public Task TallyAsync(IReadOnlyList ballots, C if (ballots.All(b => ResolveWeight(b) <= 0)) throw new InvalidOperationException("At least one ballot must have a positive weight."); - var scores = new Dictionary(StringComparer.Ordinal); + // As in MajorityVotingStrategy: one distinct winner name per ballot at most. + Dictionary scores = [with(capacity: ballots.Count, comparer: StringComparer.Ordinal)]; foreach (var ballot in ballots) { var weight = ResolveWeight(ballot); diff --git a/src/Delibera.Redis/DebateWorkerService.cs b/src/Delibera.Redis/DebateWorkerService.cs index d9e4315..e022609 100644 --- a/src/Delibera.Redis/DebateWorkerService.cs +++ b/src/Delibera.Redis/DebateWorkerService.cs @@ -1,3 +1,4 @@ +using Delibera.Core; using Delibera.Core.Interfaces; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; @@ -102,14 +103,14 @@ private async Task ProcessMessageAsync(StreamEntry message, CancellationToken ct var debateId = (string?)null; try { - debateId = message.Values.FirstOrDefault(v => v.Name == "debateId").Value; + debateId = message.Values.FirstOrDefault(v => v.Name == BuiltIn.Redis.Fields.DebateId).Value; if (string.IsNullOrEmpty(debateId)) { _logger.LogWarning("Received job message {MessageId} without debateId. Skipping.", message.Id); return; } - var jobType = message.Values.FirstOrDefault(v => v.Name == "jobType").Value; + var jobType = message.Values.FirstOrDefault(v => v.Name == BuiltIn.Redis.Fields.JobType).Value; _logger.LogInformation("Processing job {DebateId} of type {JobType}.", debateId, jobType); // Future: distributed turn-level execution will be handled here. @@ -132,7 +133,7 @@ await _db.StreamCreateConsumerGroupAsync( _options.WorkerConsumerGroup, "0-0").ConfigureAwait(false); } - catch (RedisException ex) when (ex.Message.Contains("BUSYGROUP")) + catch (RedisException ex) when (ex.Message.Contains(BuiltIn.BusyGroup)) { // Consumer group already exists — that's fine. } diff --git a/src/Delibera.Redis/Delibera.Redis.csproj b/src/Delibera.Redis/Delibera.Redis.csproj index 4181865..10ca02d 100644 --- a/src/Delibera.Redis/Delibera.Redis.csproj +++ b/src/Delibera.Redis/Delibera.Redis.csproj @@ -2,6 +2,7 @@ net10.0 + preview enable enable Delibera.Redis diff --git a/src/Delibera.Redis/RedisDebateOrchestrator.cs b/src/Delibera.Redis/RedisDebateOrchestrator.cs index 98aab0f..4ae0c16 100644 --- a/src/Delibera.Redis/RedisDebateOrchestrator.cs +++ b/src/Delibera.Redis/RedisDebateOrchestrator.cs @@ -1,10 +1,12 @@ using System.Collections.Concurrent; using System.Runtime.CompilerServices; using System.Threading.Channels; +using Delibera.Core; using Delibera.Core.Interfaces; using Delibera.Core.Models; using Delibera.Core.Voting; using Delibera.Redis.Serialization; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using StackExchange.Redis; @@ -26,18 +28,14 @@ namespace Delibera.Redis; /// public sealed class RedisDebateOrchestrator : IDebateOrchestrator, IAsyncDisposable { - /// - /// Completed entries are evicted after this timeout to prevent unbounded memory growth. - /// - private static readonly TimeSpan CompletedEntryLifetime = TimeSpan.FromMinutes(30); - private readonly IDatabase _db; private readonly ConcurrentDictionary _entries = new(); private readonly Timer _evictionTimer; private readonly ILogger _logger; private readonly RedisOrchestratorOptions _options; - private readonly ConnectionMultiplexer _redis; + private readonly IConnectionMultiplexer _redis; + private readonly bool _ownsRedis; private readonly IServiceProvider _services; private bool _disposed; @@ -49,10 +47,27 @@ public RedisDebateOrchestrator( _services = services; _logger = logger; _options = options.Value; - _redis = ConnectionMultiplexer.Connect(_options.ConnectionString); + + // Reuse the multiplexer the host already registered as a singleton instead of opening a + // second one. Each multiplexer owns its own TCP pool, handshake and reconnect timers, and + // this orchestrator lives for the whole process — RedisDebateCache, which documents + // itself as sharing this connection, already takes it from DI. Falling back to a private + // connection keeps the type usable outside a DI container. + var injected = services.GetService(); + if (injected is not null) + { + _redis = injected; + _ownsRedis = false; // the container owns it and will dispose it + } + else + { + _redis = ConnectionMultiplexer.Connect(_options.ConnectionString); + _ownsRedis = true; + } + _db = _redis.GetDatabase(); - _evictionTimer = new Timer(EvictCompletedEntries, null, CompletedEntryLifetime, CompletedEntryLifetime); + _evictionTimer = new Timer(EvictCompletedEntries, null, BuiltIn.Timeouts.CompletedDebateLifetime, BuiltIn.Timeouts.CompletedDebateLifetime); _ = EnsureConsumerGroupsAsync(); } @@ -72,7 +87,11 @@ public async ValueTask DisposeAsync() entry.Channel.Writer.TryComplete(); } - await _redis.DisposeAsync().ConfigureAwait(false); + // Only a connection this orchestrator opened is ours to close. A multiplexer injected by + // the container is disposed by the container; disposing it here would break every other + // Redis consumer sharing it. + if (_ownsRedis) + await _redis.DisposeAsync().ConfigureAwait(false); } /// @@ -246,18 +265,18 @@ private async Task SetStateAsync( var key = $"{_options.StateKeyPrefix}{debateId}"; var hash = new List { - new("status", status.ToString()), - new("updatedAt", DateTimeOffset.UtcNow.ToString("O")) + new(BuiltIn.Redis.Fields.Status, status.ToString()), + new(BuiltIn.Redis.Fields.UpdatedAt, DateTimeOffset.UtcNow.ToString("O")) }; if (result is not null) { var redisResult = result.ToRedisResult(); - hash.Add(new HashEntry("result", RedisSerializer.Serialize(redisResult))); + hash.Add(new HashEntry(BuiltIn.Redis.Fields.Result, RedisSerializer.Serialize(redisResult))); } if (errorMessage is not null) - hash.Add(new HashEntry("errorMessage", errorMessage)); + hash.Add(new HashEntry(BuiltIn.Redis.Fields.ErrorMessage, errorMessage)); await _db.HashSetAsync(key, [.. hash]).ConfigureAwait(false); @@ -284,9 +303,9 @@ private async Task SetStateAsync( if (entries.Length == 0) return null; - var statusStr = entries.FirstOrDefault(e => e.Name == "status").Value; - var resultStr = entries.FirstOrDefault(e => e.Name == "result").Value; - var errorStr = entries.FirstOrDefault(e => e.Name == "errorMessage").Value; + var statusStr = entries.FirstOrDefault(e => e.Name == BuiltIn.Redis.Fields.Status).Value; + var resultStr = entries.FirstOrDefault(e => e.Name == BuiltIn.Redis.Fields.Result).Value; + var errorStr = entries.FirstOrDefault(e => e.Name == BuiltIn.Redis.Fields.ErrorMessage).Value; if (!Enum.TryParse(statusStr, out var status)) return null; @@ -316,9 +335,9 @@ private async Task PublishRoundEventAsync(string debateId, DebateRound round) var maxLength = _options.StreamMaxLength > 0 ? _options.StreamMaxLength : (int?)null; await _db.StreamAddAsync(_options.EventStreamKey, [ - new NameValueEntry("debateId", debateId), - new NameValueEntry("eventType", "round-completed"), - new NameValueEntry("payload", json) + new NameValueEntry(BuiltIn.Redis.Fields.DebateId, debateId), + new NameValueEntry(BuiltIn.Redis.Fields.EventType, BuiltIn.Redis.Fields.RoundCompleted), + new NameValueEntry(BuiltIn.Redis.Fields.Payload, json) ], // MAXLEN ~ N: approximate trimming is O(1) and may overshoot the cap slightly, // which is the right trade for an event log — an unbounded stream would grow with @@ -340,7 +359,7 @@ async Task CreateGroupIfNotExists(string streamKey, string groupName) { await _db.StreamCreateConsumerGroupAsync(streamKey, groupName, "0-0").ConfigureAwait(false); } - catch (RedisException ex) when (ex.Message.Contains("BUSYGROUP")) + catch (RedisException ex) when (ex.Message.Contains(BuiltIn.BusyGroup)) { // Consumer group already exists — that's fine. } @@ -354,7 +373,7 @@ async Task CreateGroupIfNotExists(string streamKey, string groupName) private void EvictCompletedEntries(object? state) { - var cutoff = DateTimeOffset.UtcNow - CompletedEntryLifetime; + var cutoff = DateTimeOffset.UtcNow - BuiltIn.Timeouts.CompletedDebateLifetime; foreach (var kvp in _entries) if (kvp.Value.Status is not DebateOrchestrationStatus.Running && kvp.Value.CompletedAt < cutoff) _entries.TryRemove(kvp.Key, out _); diff --git a/src/Delibera.Redis/RedisOrchestratorOptions.cs b/src/Delibera.Redis/RedisOrchestratorOptions.cs index 44fdbfc..a089638 100644 --- a/src/Delibera.Redis/RedisOrchestratorOptions.cs +++ b/src/Delibera.Redis/RedisOrchestratorOptions.cs @@ -1,3 +1,5 @@ +using Delibera.Core; + namespace Delibera.Redis; /// @@ -5,7 +7,7 @@ namespace Delibera.Redis; /// public sealed class RedisOrchestratorOptions { - public const string SectionName = "Delibera:Redis"; + public const string SectionName = BuiltIn.ConfigSections.Redis; /// /// Redis connection string (e.g. "localhost:6379,abortConnect=false"). @@ -16,23 +18,23 @@ public sealed class RedisOrchestratorOptions /// Redis Streams key for member-turn jobs published by the orchestrator. /// Workers read from this stream to pick up turn execution work. /// - public string JobStreamKey { get; set; } = "delibera:jobs"; + public string JobStreamKey { get; set; } = BuiltIn.Redis.JobStreamKey; /// /// Redis Streams key for debate round events published by workers. /// The orchestrator reads from this stream to push SSE events to clients. /// - public string EventStreamKey { get; set; } = "delibera:events"; + public string EventStreamKey { get; set; } = BuiltIn.Redis.EventStreamKey; /// /// Consumer group name for the orchestrator's event listener. /// - public string OrchestratorConsumerGroup { get; set; } = "orchestrator"; + public string OrchestratorConsumerGroup { get; set; } = BuiltIn.Redis.OrchestratorGroup; /// /// Consumer group name for worker instances. /// - public string WorkerConsumerGroup { get; set; } = "workers"; + public string WorkerConsumerGroup { get; set; } = BuiltIn.Redis.WorkerGroup; /// /// Consumer name within the worker group. Defaults to the machine name. @@ -79,7 +81,7 @@ public sealed class RedisOrchestratorOptions /// /// Prefix for Redis hash keys storing debate state (status, result). /// - public string StateKeyPrefix { get; set; } = "delibera:state:"; + public string StateKeyPrefix { get; set; } = BuiltIn.Redis.StateKeyPrefix; /// /// Maximum number of pending messages before claiming stalled messages. diff --git a/src/Delibera.Redis/Serialization/RedisSerializer.cs b/src/Delibera.Redis/Serialization/RedisSerializer.cs index a780ed9..2df7087 100644 --- a/src/Delibera.Redis/Serialization/RedisSerializer.cs +++ b/src/Delibera.Redis/Serialization/RedisSerializer.cs @@ -1,5 +1,6 @@ using System.Text.Json; using System.Text.Json.Serialization; +using Delibera.Core; using Delibera.Core.Models; namespace Delibera.Redis.Serialization; @@ -67,7 +68,7 @@ public static RedisRoundEvent ToRedisEvent(this DebateRound round, string debate return new RedisRoundEvent { DebateId = debateId, - EventType = "round-completed", + EventType = BuiltIn.Redis.Fields.RoundCompleted, RoundNumber = round.RoundNumber, RoundName = round.RoundName, Description = round.Description, diff --git a/src/Delibera.Server/Api/Endpoints/DebateEndpoints.cs b/src/Delibera.Server/Api/Endpoints/DebateEndpoints.cs index 5a76ab4..7f37eaf 100644 --- a/src/Delibera.Server/Api/Endpoints/DebateEndpoints.cs +++ b/src/Delibera.Server/Api/Endpoints/DebateEndpoints.cs @@ -1,4 +1,5 @@ using System.Text; +using Delibera.Core; using Delibera.Core.Interfaces; using Delibera.Server.Middleware; using Delibera.Server.Sse; @@ -49,7 +50,7 @@ public static IEndpointRouteBuilder MapDebateEndpoints(this IEndpointRouteBuilde group.MapGet("/{id}/stream", StreamDebateAsync) .WithName("StreamDebate") .WithSummary("Server-Sent Events stream of DebateRound payloads.") - .Produces(StatusCodes.Status200OK, contentType: "text/event-stream"); + .Produces(StatusCodes.Status200OK, contentType: BuiltIn.HttpHeaderValues.EventStream); // GET /api/v1/debates/{id}/rounds — paginated group.MapGet("/{id}/rounds", GetDebateRoundsAsync) diff --git a/src/Delibera.Server/Api/Endpoints/ScenarioEndpoints.cs b/src/Delibera.Server/Api/Endpoints/ScenarioEndpoints.cs index bcad613..24cd4f0 100644 --- a/src/Delibera.Server/Api/Endpoints/ScenarioEndpoints.cs +++ b/src/Delibera.Server/Api/Endpoints/ScenarioEndpoints.cs @@ -1,3 +1,4 @@ +using Delibera.Core; using Delibera.Core.Interfaces; using Delibera.Server.Middleware; using Delibera.Server.Scenarios; @@ -40,7 +41,7 @@ public static IEndpointRouteBuilder MapScenarioEndpoints(this IEndpointRouteBuil group.MapGet("/{id}/stream", StreamScenarioAsync) .WithName("StreamScenario") .WithSummary("SSE stream of DebateRound payloads for a running scenario.") - .Produces(StatusCodes.Status200OK, contentType: "text/event-stream"); + .Produces(StatusCodes.Status200OK, contentType: BuiltIn.HttpHeaderValues.EventStream); return routes; } diff --git a/src/Delibera.Server/Api/Filters/ValidationFilter.cs b/src/Delibera.Server/Api/Filters/ValidationFilter.cs index 71413a0..493a004 100644 --- a/src/Delibera.Server/Api/Filters/ValidationFilter.cs +++ b/src/Delibera.Server/Api/Filters/ValidationFilter.cs @@ -1,3 +1,5 @@ +using System.Collections.Concurrent; + namespace Delibera.Server.Api.Filters; /// @@ -6,6 +8,18 @@ namespace Delibera.Server.Api.Filters; /// public sealed class ValidationFilter(IServiceProvider sp) : IEndpointFilter { + /// + /// Argument runtime type -> closed IValidator<> type. + /// + /// Building a closed generic type is not free: MakeGenericType walks the + /// generic parameters and allocates a fresh runtime type on every call. Doing that + /// once per argument per request repeated work whose result never changes, so the + /// mapping is cached. The key set is finite — arguments come from endpoint + /// contracts, which are sealed records fixed at compile time. + /// + /// + private static readonly ConcurrentDictionary ValidatorTypes = new(); + public async ValueTask InvokeAsync( EndpointFilterInvocationContext ctx, EndpointFilterDelegate next) @@ -13,7 +27,9 @@ public sealed class ValidationFilter(IServiceProvider sp) : IEndpointFilter foreach (var arg in ctx.Arguments) { if (arg is null) continue; - var validatorType = typeof(IValidator<>).MakeGenericType(arg.GetType()); + var validatorType = ValidatorTypes.GetOrAdd( + arg.GetType(), + static argumentType => typeof(IValidator<>).MakeGenericType(argumentType)); if (sp.GetService(validatorType) is not IValidator validator) continue; var result = await validator.ValidateAsync( diff --git a/src/Delibera.Server/Infrastructure/DeliberaServerOptions.cs b/src/Delibera.Server/Infrastructure/DeliberaServerOptions.cs index 701b15d..8f27353 100644 --- a/src/Delibera.Server/Infrastructure/DeliberaServerOptions.cs +++ b/src/Delibera.Server/Infrastructure/DeliberaServerOptions.cs @@ -1,3 +1,5 @@ +using Delibera.Core; + namespace Delibera.Server.Infrastructure; public sealed class DeliberaServerOptions @@ -7,7 +9,7 @@ public sealed class DeliberaServerOptions /// ("Delibera": { "Server": { … } }) — a mismatch here silently leaves /// every option at its default, including the OTLP endpoint. /// - public const string SectionName = "Delibera:Server"; + public const string SectionName = BuiltIn.ConfigSections.Server; /// Default tenant id used when X-Tenant-Id header is absent. public string DefaultTenantId { get; init; } = "default"; diff --git a/src/Delibera.Server/Infrastructure/ProblemDetailsExceptionHandler.cs b/src/Delibera.Server/Infrastructure/ProblemDetailsExceptionHandler.cs index 436419f..0389416 100644 --- a/src/Delibera.Server/Infrastructure/ProblemDetailsExceptionHandler.cs +++ b/src/Delibera.Server/Infrastructure/ProblemDetailsExceptionHandler.cs @@ -1,3 +1,4 @@ +using Delibera.Core; using Microsoft.AspNetCore.Diagnostics; namespace Delibera.Server.Infrastructure; @@ -23,7 +24,7 @@ public sealed class ProblemDetailsExceptionHandler( ILogger logger) : IExceptionHandler { /// Header the correlation id is echoed on, and the key it is stored under. - public const string CorrelationIdHeader = "X-Correlation-Id"; + public const string CorrelationIdHeader = BuiltIn.HttpHeaders.CorrelationId; public async ValueTask TryHandleAsync( HttpContext httpContext, diff --git a/src/Delibera.Server/Infrastructure/RedisGateOptions.cs b/src/Delibera.Server/Infrastructure/RedisGateOptions.cs index a0d1ef5..12b36e6 100644 --- a/src/Delibera.Server/Infrastructure/RedisGateOptions.cs +++ b/src/Delibera.Server/Infrastructure/RedisGateOptions.cs @@ -1,3 +1,5 @@ +using Delibera.Core; + namespace Delibera.Server.Infrastructure; /// @@ -20,7 +22,7 @@ public sealed class RedisGateOptions /// /// The same section RedisOrchestratorOptions.SectionName resolves to. /// - public const string SectionName = "Delibera:Redis"; + public const string SectionName = BuiltIn.ConfigSections.Redis; /// /// Replaces the in-process LocalDebateOrchestrator with diff --git a/src/Delibera.Server/Infrastructure/ServerServiceExtensions.cs b/src/Delibera.Server/Infrastructure/ServerServiceExtensions.cs index 06a5060..bc98aaf 100644 --- a/src/Delibera.Server/Infrastructure/ServerServiceExtensions.cs +++ b/src/Delibera.Server/Infrastructure/ServerServiceExtensions.cs @@ -1,3 +1,4 @@ +using Delibera.Core; using Delibera.Redis; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; @@ -17,7 +18,7 @@ public static class ServerServiceExtensions /// when the policy exists — ASP.NET throws at request time for an unknown policy, which is /// a far worse failure mode than simply not enabling CORS. /// - public const string CorsPolicyName = "DeliberaCors"; + public const string CorsPolicyName = BuiltIn.CorsPolicyName; /// /// Registers a named CORS policy, but only when origins are explicitly configured. @@ -70,7 +71,7 @@ public static IServiceCollection AddDeliberaCors( .AllowAnyMethod() // The correlation id is how a caller finds its entry in the server log; without this // the browser hides it from a cross-origin client. - .WithExposedHeaders("X-Correlation-Id"); + .WithExposedHeaders(BuiltIn.HttpHeaders.CorrelationId); if (cors.AllowCredentials) policy.AllowCredentials(); diff --git a/src/Delibera.Server/Middleware/CorrelationIdMiddleware.cs b/src/Delibera.Server/Middleware/CorrelationIdMiddleware.cs index 446f2ec..157a1a2 100644 --- a/src/Delibera.Server/Middleware/CorrelationIdMiddleware.cs +++ b/src/Delibera.Server/Middleware/CorrelationIdMiddleware.cs @@ -1,3 +1,4 @@ +using Delibera.Core; using Serilog.Context; namespace Delibera.Server.Middleware; @@ -7,14 +8,12 @@ namespace Delibera.Server.Middleware; /// public sealed class CorrelationIdMiddleware(RequestDelegate next) { - private const string HeaderName = "X-Correlation-Id"; - public async Task InvokeAsync(HttpContext ctx) { - var correlationId = ctx.Request.Headers[HeaderName].FirstOrDefault() ?? Guid.NewGuid().ToString("N"); + var correlationId = ctx.Request.Headers[BuiltIn.HttpHeaders.CorrelationId].FirstOrDefault() ?? Guid.NewGuid().ToString("N"); - ctx.Items[HeaderName] = correlationId; - ctx.Response.Headers[HeaderName] = correlationId; + ctx.Items[BuiltIn.HttpHeaders.CorrelationId] = correlationId; + ctx.Response.Headers[BuiltIn.HttpHeaders.CorrelationId] = correlationId; using (LogContext.PushProperty("CorrelationId", correlationId)) { diff --git a/src/Delibera.Server/Middleware/TenantResolutionMiddleware.cs b/src/Delibera.Server/Middleware/TenantResolutionMiddleware.cs index 0f549a4..4a7cc32 100644 --- a/src/Delibera.Server/Middleware/TenantResolutionMiddleware.cs +++ b/src/Delibera.Server/Middleware/TenantResolutionMiddleware.cs @@ -1,3 +1,4 @@ +using Delibera.Core; using Delibera.Server.Infrastructure; using Microsoft.Extensions.Options; @@ -10,12 +11,11 @@ public sealed class TenantResolutionMiddleware( RequestDelegate next, IOptions options) { - private const string HeaderName = "X-Tenant-Id"; public const string ItemKey = "TenantId"; public async Task InvokeAsync(HttpContext ctx) { - var tenantId = ctx.Request.Headers[HeaderName].FirstOrDefault() ?? options.Value.DefaultTenantId; + var tenantId = ctx.Request.Headers[BuiltIn.HttpHeaders.TenantId].FirstOrDefault() ?? options.Value.DefaultTenantId; ctx.Items[ItemKey] = tenantId; await next(ctx); } diff --git a/src/Delibera.Server/Scenarios/ScenarioBuilder.cs b/src/Delibera.Server/Scenarios/ScenarioBuilder.cs index 01296b1..165f4b8 100644 --- a/src/Delibera.Server/Scenarios/ScenarioBuilder.cs +++ b/src/Delibera.Server/Scenarios/ScenarioBuilder.cs @@ -1,3 +1,4 @@ +using Delibera.Core; using Delibera.Core.Debate; using Delibera.Core.Interfaces; using Delibera.Core.Providers; @@ -14,10 +15,10 @@ public static ICouncilBuilder Build( if (request.Members is not { Length: > 0 }) throw new ArgumentException("A scenario must have at least one member.", nameof(request)); - var endpoint = configuration["Delibera:Providers:DefaultEndpoint"] ?? "http://localhost:11434"; - var apiKey = configuration["Delibera:Providers:ApiKey"]; - var fastModel = configuration["Delibera:Models:Fast"] ?? "llama3.2:3b"; - var strongModel = configuration["Delibera:Models:Strong"] ?? "qwen2.5:7b"; + var endpoint = configuration[BuiltIn.ConfigKeys.ProvidersDefaultEndpoint] ?? BuiltIn.Endpoints.OllamaLocal; + var apiKey = configuration[BuiltIn.ConfigKeys.ProvidersApiKey]; + var fastModel = configuration[BuiltIn.ConfigKeys.ModelsFast] ?? BuiltIn.Models.DefaultFast; + var strongModel = configuration[BuiltIn.ConfigKeys.ModelsStrong] ?? BuiltIn.Models.DefaultStrong; using var factory = new ProviderFactory(); ILLMProvider DefaultProvider() diff --git a/src/Delibera.Server/Services/DebateOrchestrationService.cs b/src/Delibera.Server/Services/DebateOrchestrationService.cs index ce7d5e7..28cb667 100644 --- a/src/Delibera.Server/Services/DebateOrchestrationService.cs +++ b/src/Delibera.Server/Services/DebateOrchestrationService.cs @@ -1,4 +1,5 @@ using System.Collections.Concurrent; +using Delibera.Core; using Delibera.Core.Interfaces; using Delibera.Server.Scenarios; @@ -10,40 +11,35 @@ namespace Delibera.Server.Services; /// execution to . /// /// Completed debate records are automatically evicted after -/// to prevent unbounded memory growth. +/// to prevent unbounded memory growth. /// /// public sealed class DebateOrchestrationService : IDebateOrchestrationService, IDisposable { - /// - /// Default lifetime for completed debate records before eviction. - /// - public static readonly TimeSpan CompletedRecordLifetime = TimeSpan.FromMinutes(30); - - private readonly TimeSpan _completedRecordLifetime; - private readonly IConfiguration _configuration; - private readonly Timer _evictionTimer; - private readonly ILogger _logger; - private readonly IDebateOrchestrator _orchestrator; - - private readonly ConcurrentDictionary _records = new(); - private readonly IServiceProvider _services; - private readonly ITemplateRegistry _templates; - - public DebateOrchestrationService( - ILogger logger, - ITemplateRegistry templates, - IServiceProvider services, - IConfiguration configuration, - IDebateOrchestrator orchestrator, - TimeSpan? completedRecordLifetime = null) - { - _logger = logger; - _templates = templates; - _services = services; - _configuration = configuration; - _orchestrator = orchestrator; - _completedRecordLifetime = completedRecordLifetime ?? CompletedRecordLifetime; + private readonly TimeSpan _completedRecordLifetime; + private readonly IConfiguration _configuration; + private readonly Timer _evictionTimer; + private readonly ILogger _logger; + private readonly IDebateOrchestrator _orchestrator; + + private readonly ConcurrentDictionary _records = new(); + private readonly IServiceProvider _services; + private readonly ITemplateRegistry _templates; + + public DebateOrchestrationService( + ILogger logger, + ITemplateRegistry templates, + IServiceProvider services, + IConfiguration configuration, + IDebateOrchestrator orchestrator, + TimeSpan? completedRecordLifetime = null) + { + _logger = logger; + _templates = templates; + _services = services; + _configuration = configuration; + _orchestrator = orchestrator; + _completedRecordLifetime = completedRecordLifetime ?? BuiltIn.Timeouts.CompletedDebateLifetime; _evictionTimer = new Timer(EvictCompletedRecords, null, _completedRecordLifetime, _completedRecordLifetime); } diff --git a/src/Delibera.Server/Services/ServerRagProviderFactory.cs b/src/Delibera.Server/Services/ServerRagProviderFactory.cs index 5277794..cf96601 100644 --- a/src/Delibera.Server/Services/ServerRagProviderFactory.cs +++ b/src/Delibera.Server/Services/ServerRagProviderFactory.cs @@ -1,3 +1,4 @@ +using Delibera.Core; using Delibera.Core.Interfaces; using Delibera.Core.Providers.LLM; @@ -30,11 +31,11 @@ public sealed class ServerRagProviderFactory( private OllamaProvider? _embeddingSource; private IRagProvider? _rag; - /// Whether Delibera:Rag:Enabled is set. - public bool Enabled => configuration.GetValue("Delibera:Rag:Enabled", false); - - /// Provider type, e.g. Qdrant or PgVector. - public string ProviderType => configuration["Delibera:Rag:ProviderType"] ?? "Qdrant"; + /// Whether Delibera:Rag:Enabled is set. + public bool Enabled => configuration.GetValue(BuiltIn.ConfigKeys.RagEnabled, false); + + /// Provider type, e.g. Qdrant or PgVector. + public string ProviderType => configuration[BuiltIn.ConfigKeys.RagProviderType] ?? "Qdrant"; public void Dispose() { @@ -69,21 +70,21 @@ public static string CollectionForCorpus(string corpusId) if (_rag is not null) return _rag; - var endpoint = configuration["Delibera:Providers:DefaultEndpoint"] - ?? throw new InvalidOperationException( - "Delibera:Providers:DefaultEndpoint is required for RAG."); - var apiKey = configuration["Delibera:Providers:ApiKey"]; - var embeddingModel = configuration["Delibera:Providers:EmbeddingModel"] ?? "nomic-embed-text"; - - // The embedding provider borrows its HTTP client from the Ollama provider, so the two - // share one endpoint. That is why RAG needs a provider that serves embeddings: Ollama - // Cloud exposes none, so this configuration only works against a local Ollama. - _embeddingSource = new OllamaProvider(endpoint, apiKey ?? string.Empty, TimeSpan.FromMinutes(2)); - var embeddings = new OllamaEmbeddingProvider(_embeddingSource, embeddingModel); - - // The factory reads Host/Port (Qdrant) or ConnectionString (PgVector) from the section, - // which is exactly the shape of Delibera:Rag in configuration. - _rag = vectorStores.Create(embeddingModel, ProviderType, configuration.GetSection("Delibera:Rag"), embeddings); + var endpoint = configuration[BuiltIn.ConfigKeys.ProvidersDefaultEndpoint] + ?? throw new InvalidOperationException( + "Delibera:Providers:DefaultEndpoint is required for RAG."); + var apiKey = configuration[BuiltIn.ConfigKeys.ProvidersApiKey]; + var embeddingModel = configuration[BuiltIn.ConfigKeys.ProvidersEmbeddingModel] ?? BuiltIn.Models.DefaultEmbedding; + + // The embedding provider borrows its HTTP client from the Ollama provider, so the two + // share one endpoint. That is why RAG needs a provider that serves embeddings: Ollama + // Cloud exposes none, so this configuration only works against a local Ollama. + _embeddingSource = new OllamaProvider(endpoint, apiKey ?? string.Empty, TimeSpan.FromMinutes(2)); + var embeddings = new OllamaEmbeddingProvider(_embeddingSource, embeddingModel); + + // The factory reads Host/Port (Qdrant) or ConnectionString (PgVector) from the section, + // which is exactly the shape of Delibera:Rag in configuration. + _rag = vectorStores.Create(embeddingModel, ProviderType, configuration.GetSection("Delibera:Rag"), embeddings); loggerFactory.CreateLogger() .LogInformation( diff --git a/src/Delibera.Server/Sse/SseDebateStreamWriter.cs b/src/Delibera.Server/Sse/SseDebateStreamWriter.cs index 6967a0e..53a05b2 100644 --- a/src/Delibera.Server/Sse/SseDebateStreamWriter.cs +++ b/src/Delibera.Server/Sse/SseDebateStreamWriter.cs @@ -1,4 +1,5 @@ using System.Text; +using Delibera.Core; using Delibera.Core.Interfaces; namespace Delibera.Server.Sse; @@ -11,19 +12,13 @@ namespace Delibera.Server.Sse; /// public static class SseDebateStreamWriter { - private static readonly JsonSerializerOptions _json = new() - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull - }; - /// /// How long the stream may stay silent before a keep-alive comment is sent. A /// parameter of rather than a static, so a test can prove a /// heartbeat exists without waiting 15 seconds and without mutating global state that /// other test classes running in parallel would observe. /// - public static readonly TimeSpan DefaultHeartbeatInterval = TimeSpan.FromSeconds(15); + public static readonly TimeSpan DefaultHeartbeatInterval = BuiltIn.Timeouts.SseHeartbeat; private static readonly byte[] CommentPrefix = ": "u8.ToArray(); @@ -49,27 +44,27 @@ public static async Task WriteAsync( TimeSpan? heartbeatInterval = null) { var heartbeat = heartbeatInterval ?? DefaultHeartbeatInterval; - ctx.Response.Headers.ContentType = "text/event-stream"; - ctx.Response.Headers.CacheControl = "no-cache"; - ctx.Response.Headers["X-Accel-Buffering"] = "no"; + ctx.Response.Headers.ContentType = BuiltIn.HttpHeaderValues.EventStream; + ctx.Response.Headers.CacheControl = BuiltIn.HttpHeaderValues.NoCache; + ctx.Response.Headers[BuiltIn.HttpHeaders.AccelBuffering] = BuiltIn.HttpHeaderValues.No; // If already terminal, write the result and close immediately. if (record.Status is DebateStatus.Completed) { - await WriteSseEventAsync(ctx, "debate-round", record.RoundsSnapshot().Select(r => r.ToDto()), ct); - await WriteSseEventAsync(ctx, "debate-completed", BuildCompletedPayload(record), ct); + await WriteSseEventAsync(ctx, BuiltIn.SseEvents.Round, record.RoundsSnapshot().Select(r => r.ToDto()), ct); + await WriteSseEventAsync(ctx, BuiltIn.SseEvents.Completed, BuildCompletedPayload(record), ct); return; } if (record.Status is DebateStatus.Failed) { - await WriteSseEventAsync(ctx, "debate-error", BuildErrorPayload(record), ct); + await WriteSseEventAsync(ctx, BuiltIn.SseEvents.Error, BuildErrorPayload(record), ct); return; } if (record.Status is DebateStatus.Cancelled) { - await WriteSseEventAsync(ctx, "debate-cancelled", new { record.DebateId }, ct); + await WriteSseEventAsync(ctx, BuiltIn.SseEvents.Cancelled, new { record.DebateId }, ct); return; } @@ -86,7 +81,7 @@ public static async Task WriteAsync( // A comment line every heartbeat interval. An idle debate produces no events for // minutes at a time, and an intermediary proxy is entitled to close a connection // that has been silent — which looked to the client exactly like a dead debate. - await WriteSseCommentAsync(ctx, "retry: 3000", ct); + await WriteSseCommentAsync(ctx, BuiltIn.SseEvents.Retry, ct); enumerator = orchestrator .StreamAsync(record.DebateId, ct) @@ -125,7 +120,7 @@ public static async Task WriteAsync( // pending MoveNextAsync — it is not cancelled or abandoned, and it must // not be re-issued: the iterator rejects a concurrent move. heartbeatCts.Cancel(); - await WriteSseCommentAsync(ctx, "keep-alive", ct); + await WriteSseCommentAsync(ctx, BuiltIn.SseEvents.KeepAlive, ct); continue; } @@ -139,26 +134,26 @@ public static async Task WriteAsync( switch (enumerator.Current) { case DebateRoundEvent.RoundCompleted rc: - await WriteSseEventAsync(ctx, "debate-round", rc.Round.ToDto(), ct); + await WriteSseEventAsync(ctx, BuiltIn.SseEvents.Round, rc.Round.ToDto(), ct); break; case DebateRoundEvent.DebateCompleted dc: record.Result = dc.Result; record.Status = DebateStatus.Completed; record.CompletedAt = dc.Result?.CompletedAt ?? DateTimeOffset.UtcNow; - await WriteSseEventAsync(ctx, "debate-completed", BuildCompletedPayload(record), ct); + await WriteSseEventAsync(ctx, BuiltIn.SseEvents.Completed, BuildCompletedPayload(record), ct); terminalWritten = true; return; case DebateRoundEvent.DebateFailed df: record.ErrorMessage = df.Error; record.Status = DebateStatus.Failed; record.CompletedAt = DateTimeOffset.UtcNow; - await WriteSseEventAsync(ctx, "debate-error", BuildErrorPayload(record), ct); + await WriteSseEventAsync(ctx, BuiltIn.SseEvents.Error, BuildErrorPayload(record), ct); terminalWritten = true; return; case DebateRoundEvent.DebateCancelled: record.Status = DebateStatus.Cancelled; record.CompletedAt = DateTimeOffset.UtcNow; - await WriteSseEventAsync(ctx, "debate-cancelled", new { record.DebateId }, ct); + await WriteSseEventAsync(ctx, BuiltIn.SseEvents.Cancelled, new { record.DebateId }, ct); terminalWritten = true; return; } @@ -187,7 +182,7 @@ public static async Task WriteAsync( record.ErrorMessage ??= ct.IsCancellationRequested ? "The client closed the stream before the debate finished." : "The event stream ended before a terminal event was received."; - await WriteSseEventAsync(ctx, "debate-error", BuildErrorPayload(record), ct); + await WriteSseEventAsync(ctx, BuiltIn.SseEvents.Error, BuildErrorPayload(record), ct); } catch (Exception ex) when (ex is IOException or OperationCanceledException or ObjectDisposedException) { @@ -241,26 +236,85 @@ private static async Task WriteSseCommentAsync( string comment, CancellationToken ct) { - var bytes = Encoding.UTF8.GetBytes(comment + "\n\n"); - await ctx.Response.Body.WriteAsync(CommentPrefix, ct); - await ctx.Response.Body.WriteAsync(bytes, ct); - await ctx.Response.Body.FlushAsync(ct); + var writer = ctx.Response.BodyWriter; + var maxLength = CommentPrefix.Length + + Encoding.UTF8.GetMaxByteCount(comment.Length) + + DoubleNewlineBytes.Length; + var span = writer.GetSpan(maxLength); + + var written = 0; + CommentPrefix.CopyTo(span[written..]); + written += CommentPrefix.Length; + written += Encoding.UTF8.GetBytes(comment.AsSpan(), span[written..]); + DoubleNewlineBytes.CopyTo(span[written..]); + written += DoubleNewlineBytes.Length; + + writer.Advance(written); + await writer.FlushAsync(ct); } + /// + /// The wire names this writer emits, as UTF-8 literals. + /// + /// Event names are a closed set — the switch below covers all four, and the call + /// sites pass constants. A u8 literal is baked into the assembly, so the + /// common path allocates nothing at all; the fallback only runs for a name that + /// is not part of this protocol. + /// + /// + private static ReadOnlySpan EventNameBytes(string eventType) => eventType switch + { + BuiltIn.SseEvents.Round => "debate-round"u8, + BuiltIn.SseEvents.Completed => "debate-completed"u8, + BuiltIn.SseEvents.Error => "debate-error"u8, + BuiltIn.SseEvents.Cancelled => "debate-cancelled"u8, + _ => Encoding.UTF8.GetBytes(eventType) + }; + + /// + /// Writes one SSE frame as a single write to the response pipe. + /// + /// The frame used to go out as six separate Body.WriteAsync calls plus a + /// flush, and the event name was re-encoded from string to UTF-8 on every event. + /// The pipe lets the whole frame be assembled in one span and handed over once, + /// which is both fewer syscalls and no per-event name allocation. + /// + /// + /// Flush stays per frame, deliberately: a buffering proxy once passed every + /// functional test in this suite while delivering each round 150–200 seconds + /// late, so a frame is only observable once it has actually left. + /// + /// private static async Task WriteSseEventAsync( HttpContext ctx, string eventType, T payload, CancellationToken ct) { - var bytes = JsonSerializer.SerializeToUtf8Bytes(payload, _json); - await ctx.Response.Body.WriteAsync(EventPrefix, ct); - await ctx.Response.Body.WriteAsync(Encoding.UTF8.GetBytes(eventType), ct); - await ctx.Response.Body.WriteAsync(NewlineBytes, ct); - await ctx.Response.Body.WriteAsync(DataPrefix, ct); - await ctx.Response.Body.WriteAsync(bytes, ct); - await ctx.Response.Body.WriteAsync(DoubleNewlineBytes, ct); - await ctx.Response.Body.FlushAsync(ct); + var json = JsonSerializer.SerializeToUtf8Bytes(payload, BuiltIn.Json.Default); + var name = EventNameBytes(eventType); + + var writer = ctx.Response.BodyWriter; + var span = writer.GetSpan( + EventPrefix.Length + name.Length + NewlineBytes.Length + + DataPrefix.Length + json.Length + DoubleNewlineBytes.Length); + + var written = 0; + EventPrefix.CopyTo(span[written..]); + written += EventPrefix.Length; + name.CopyTo(span[written..]); + written += name.Length; + NewlineBytes.CopyTo(span[written..]); + written += NewlineBytes.Length; + DataPrefix.CopyTo(span[written..]); + written += DataPrefix.Length; + json.CopyTo(span[written..]); + written += json.Length; + DoubleNewlineBytes.CopyTo(span[written..]); + written += DoubleNewlineBytes.Length; + + writer.Advance(written); + await writer.FlushAsync(ct); } private static object BuildCompletedPayload(DebateRecord record) diff --git a/src/Delibera.Server/Templates/Engineering/CodeReviewTemplate.cs b/src/Delibera.Server/Templates/Engineering/CodeReviewTemplate.cs index 0ec71b6..6aa7d3d 100644 --- a/src/Delibera.Server/Templates/Engineering/CodeReviewTemplate.cs +++ b/src/Delibera.Server/Templates/Engineering/CodeReviewTemplate.cs @@ -1,4 +1,5 @@ -using Delibera.Core.Interfaces; +using Delibera.Core; +using Delibera.Core.Interfaces; using Delibera.Core.Providers; using Delibera.Core.Voting; @@ -32,15 +33,15 @@ public ICouncilBuilder Configure( IServiceProvider services, IConfiguration configuration) { - var endpoint = configuration["Delibera:Providers:DefaultEndpoint"] ?? "http://localhost:11434"; - var apiKey = configuration["Delibera:Providers:ApiKey"]; - using var factory = new ProviderFactory(); - var llm = string.IsNullOrEmpty(apiKey) - ? factory.CreateOllama(endpoint) - : factory.CreateCloudOllama(endpoint, apiKey); + var endpoint = configuration[BuiltIn.ConfigKeys.ProvidersDefaultEndpoint] ?? BuiltIn.Endpoints.OllamaLocal; + var apiKey = configuration[BuiltIn.ConfigKeys.ProvidersApiKey]; + using var factory = new ProviderFactory(); + var llm = string.IsNullOrEmpty(apiKey) + ? factory.CreateOllama(endpoint) + : factory.CreateCloudOllama(endpoint, apiKey); - var fastModel = configuration["Delibera:Models:Fast"] ?? "llama3.2:3b"; - var strongModel = configuration["Delibera:Models:Strong"] ?? "qwen2.5:7b"; + var fastModel = configuration[BuiltIn.ConfigKeys.ModelsFast] ?? BuiltIn.Models.DefaultFast; + var strongModel = configuration[BuiltIn.ConfigKeys.ModelsStrong] ?? BuiltIn.Models.DefaultStrong; var maxRounds = request.Options?.MaxRounds ?? DefaultMaxRounds; var temp = request.Options?.Temperature ?? 0.4f; diff --git a/src/Delibera.Server/Templates/Governance/ArchitectureDecisionTemplate.cs b/src/Delibera.Server/Templates/Governance/ArchitectureDecisionTemplate.cs index f926ffb..ef88e5a 100644 --- a/src/Delibera.Server/Templates/Governance/ArchitectureDecisionTemplate.cs +++ b/src/Delibera.Server/Templates/Governance/ArchitectureDecisionTemplate.cs @@ -1,4 +1,5 @@ -using Delibera.Core.Interfaces; +using Delibera.Core; +using Delibera.Core.Interfaces; using Delibera.Core.Providers; using Delibera.Core.Voting; @@ -30,15 +31,15 @@ public ICouncilBuilder Configure( IServiceProvider services, IConfiguration configuration) { - var endpoint = configuration["Delibera:Providers:DefaultEndpoint"] ?? "http://localhost:11434"; - var apiKey = configuration["Delibera:Providers:ApiKey"]; - using var factory = new ProviderFactory(); - var llm = string.IsNullOrEmpty(apiKey) - ? factory.CreateOllama(endpoint) - : factory.CreateCloudOllama(endpoint, apiKey); + var endpoint = configuration[BuiltIn.ConfigKeys.ProvidersDefaultEndpoint] ?? BuiltIn.Endpoints.OllamaLocal; + var apiKey = configuration[BuiltIn.ConfigKeys.ProvidersApiKey]; + using var factory = new ProviderFactory(); + var llm = string.IsNullOrEmpty(apiKey) + ? factory.CreateOllama(endpoint) + : factory.CreateCloudOllama(endpoint, apiKey); - var fastModel = configuration["Delibera:Models:Fast"] ?? "llama3.2:3b"; - var strongModel = configuration["Delibera:Models:Strong"] ?? "qwen2.5:7b"; + var fastModel = configuration[BuiltIn.ConfigKeys.ModelsFast] ?? BuiltIn.Models.DefaultFast; + var strongModel = configuration[BuiltIn.ConfigKeys.ModelsStrong] ?? BuiltIn.Models.DefaultStrong; var maxRounds = request.Options?.MaxRounds ?? DefaultMaxRounds; var builder = new CouncilBuilder() diff --git a/src/Delibera.Server/Templates/Governance/RiskCommitteeTemplate.cs b/src/Delibera.Server/Templates/Governance/RiskCommitteeTemplate.cs index 7cc40a6..76eeff3 100644 --- a/src/Delibera.Server/Templates/Governance/RiskCommitteeTemplate.cs +++ b/src/Delibera.Server/Templates/Governance/RiskCommitteeTemplate.cs @@ -1,4 +1,5 @@ -using Delibera.Core.Interfaces; +using Delibera.Core; +using Delibera.Core.Interfaces; using Delibera.Core.Providers; using Delibera.Core.Providers.LLM; using Delibera.Core.Voting; @@ -32,18 +33,17 @@ public ICouncilBuilder Configure( IServiceProvider services, IConfiguration configuration) { - var providerSection = configuration.GetSection("Delibera:Providers"); - var endpoint = providerSection["DefaultEndpoint"] ?? "http://localhost:11434"; - var apiKey = providerSection["ApiKey"]; - var embeddingModel = providerSection["EmbeddingModel"] ?? "nomic-embed-text"; + var endpoint = configuration[BuiltIn.ConfigKeys.ProvidersDefaultEndpoint] ?? BuiltIn.Endpoints.OllamaLocal; + var apiKey = configuration[BuiltIn.ConfigKeys.ProvidersApiKey]; + var embeddingModel = configuration[BuiltIn.ConfigKeys.ProvidersEmbeddingModel] ?? BuiltIn.Models.DefaultEmbedding; - using var factory = new ProviderFactory(); - var llm = string.IsNullOrEmpty(apiKey) - ? factory.CreateOllama(endpoint) - : factory.CreateCloudOllama(endpoint, apiKey); + using var factory = new ProviderFactory(); + var llm = string.IsNullOrEmpty(apiKey) + ? factory.CreateOllama(endpoint) + : factory.CreateCloudOllama(endpoint, apiKey); - var fastModel = configuration["Delibera:Models:Fast"] ?? "llama3.2:3b"; - var strongModel = configuration["Delibera:Models:Strong"] ?? "qwen2.5:7b"; + var fastModel = configuration[BuiltIn.ConfigKeys.ModelsFast] ?? BuiltIn.Models.DefaultFast; + var strongModel = configuration[BuiltIn.ConfigKeys.ModelsStrong] ?? BuiltIn.Models.DefaultStrong; var maxRounds = request.Options?.MaxRounds ?? DefaultMaxRounds; var temp = request.Options?.Temperature ?? 0.7f; diff --git a/src/Delibera.Server/Templates/Legal/LegalContractReviewTemplate.cs b/src/Delibera.Server/Templates/Legal/LegalContractReviewTemplate.cs index 88fcf23..b94fa75 100644 --- a/src/Delibera.Server/Templates/Legal/LegalContractReviewTemplate.cs +++ b/src/Delibera.Server/Templates/Legal/LegalContractReviewTemplate.cs @@ -1,4 +1,5 @@ -using Delibera.Core.Interfaces; +using Delibera.Core; +using Delibera.Core.Interfaces; using Delibera.Core.Providers; using Delibera.Core.Voting; @@ -34,15 +35,15 @@ public ICouncilBuilder Configure( IServiceProvider services, IConfiguration configuration) { - var endpoint = configuration["Delibera:Providers:DefaultEndpoint"] ?? "http://localhost:11434"; - var apiKey = configuration["Delibera:Providers:ApiKey"]; - using var factory = new ProviderFactory(); - var llm = string.IsNullOrEmpty(apiKey) - ? factory.CreateOllama(endpoint) - : factory.CreateCloudOllama(endpoint, apiKey); + var endpoint = configuration[BuiltIn.ConfigKeys.ProvidersDefaultEndpoint] ?? BuiltIn.Endpoints.OllamaLocal; + var apiKey = configuration[BuiltIn.ConfigKeys.ProvidersApiKey]; + using var factory = new ProviderFactory(); + var llm = string.IsNullOrEmpty(apiKey) + ? factory.CreateOllama(endpoint) + : factory.CreateCloudOllama(endpoint, apiKey); - var fastModel = configuration["Delibera:Models:Fast"] ?? "llama3.2:3b"; - var strongModel = configuration["Delibera:Models:Strong"] ?? "qwen2.5:7b"; + var fastModel = configuration[BuiltIn.ConfigKeys.ModelsFast] ?? BuiltIn.Models.DefaultFast; + var strongModel = configuration[BuiltIn.ConfigKeys.ModelsStrong] ?? BuiltIn.Models.DefaultStrong; var maxRounds = request.Options?.MaxRounds ?? DefaultMaxRounds; var temp = request.Options?.Temperature ?? 0.3f; diff --git a/src/Delibera.Server/Templates/Product/RequirementsReviewTemplate.cs b/src/Delibera.Server/Templates/Product/RequirementsReviewTemplate.cs index 41fd6c9..d53d3cc 100644 --- a/src/Delibera.Server/Templates/Product/RequirementsReviewTemplate.cs +++ b/src/Delibera.Server/Templates/Product/RequirementsReviewTemplate.cs @@ -1,4 +1,5 @@ -using Delibera.Core.Interfaces; +using Delibera.Core; +using Delibera.Core.Interfaces; using Delibera.Core.Providers; using Delibera.Core.Voting; using Delibera.Server.Templates.Engineering; @@ -36,15 +37,15 @@ public ICouncilBuilder Configure( IServiceProvider services, IConfiguration configuration) { - var endpoint = configuration["Delibera:Providers:DefaultEndpoint"] ?? "http://localhost:11434"; - var apiKey = configuration["Delibera:Providers:ApiKey"]; - var factory = new ProviderFactory(); - var llm = string.IsNullOrEmpty(apiKey) - ? factory.CreateOllama(endpoint) - : factory.CreateCloudOllama(endpoint, apiKey); + var endpoint = configuration[BuiltIn.ConfigKeys.ProvidersDefaultEndpoint] ?? BuiltIn.Endpoints.OllamaLocal; + var apiKey = configuration[BuiltIn.ConfigKeys.ProvidersApiKey]; + var factory = new ProviderFactory(); + var llm = string.IsNullOrEmpty(apiKey) + ? factory.CreateOllama(endpoint) + : factory.CreateCloudOllama(endpoint, apiKey); - var fastModel = configuration["Delibera:Models:Fast"] ?? "llama3.2:3b"; - var strongModel = configuration["Delibera:Models:Strong"] ?? "qwen2.5:7b"; + var fastModel = configuration[BuiltIn.ConfigKeys.ModelsFast] ?? BuiltIn.Models.DefaultFast; + var strongModel = configuration[BuiltIn.ConfigKeys.ModelsStrong] ?? BuiltIn.Models.DefaultStrong; var maxRounds = request.Options?.MaxRounds ?? DefaultMaxRounds; var temp = request.Options?.Temperature ?? 0.6f; diff --git a/src/Delibera.Server/Templates/TemplateKnowledge.cs b/src/Delibera.Server/Templates/TemplateKnowledge.cs index 162e97d..7814551 100644 --- a/src/Delibera.Server/Templates/TemplateKnowledge.cs +++ b/src/Delibera.Server/Templates/TemplateKnowledge.cs @@ -1,3 +1,4 @@ +using Delibera.Core; using Delibera.Core.Interfaces; using Delibera.Core.Providers; @@ -45,10 +46,10 @@ public static ICouncilBuilder Attach( // The keeper's model is a council member, so it uses the configured strong model: retrieval // synthesis is a comprehension job, and answering it on the fast model showed up as short, // unsourced answers. - var configuration = services.GetService(); - var strongModel = configuration?["Delibera:Models:Strong"] ?? "qwen2.5:7b"; - var endpoint = configuration?["Delibera:Providers:DefaultEndpoint"] ?? "http://localhost:11434"; - var apiKey = configuration?["Delibera:Providers:ApiKey"]; + var configuration = services.GetService(); + var strongModel = configuration?[BuiltIn.ConfigKeys.ModelsStrong] ?? BuiltIn.Models.DefaultStrong; + var endpoint = configuration?[BuiltIn.ConfigKeys.ProvidersDefaultEndpoint] ?? BuiltIn.Endpoints.OllamaLocal; + var apiKey = configuration?[BuiltIn.ConfigKeys.ProvidersApiKey]; // A dedicated factory instance rather than one from DI: disposing it here must not tear // down the provider the debate itself is about to use. diff --git a/src/Delibera.WebUI/package-lock.json b/src/Delibera.WebUI/package-lock.json index be03d12..ad5f3d0 100644 --- a/src/Delibera.WebUI/package-lock.json +++ b/src/Delibera.WebUI/package-lock.json @@ -1,18 +1,17 @@ { "name": "delibera-webui", - "version": "10.5.1", + "version": "10.5.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "delibera-webui", - "version": "10.5.1", + "version": "10.5.2", "hasInstallScript": true, "license": "MIT", "dependencies": { - "nuxt": "^4.0.0", - "vue": "^3.5.0", - "vue-router": "^4.5.0" + "nuxt": "^4.6.1", + "vue": "^3.5.43" }, "devDependencies": { "@nuxt/test-utils": "^3.19.0", @@ -23,7 +22,7 @@ "vue-tsc": "^2.2.0" }, "engines": { - "node": ">=22" + "node": ">=22.22.3" } }, "node_modules/@babel/code-frame": { @@ -429,31 +428,6 @@ "node": ">=6.9.0" } }, - "node_modules/@bomb.sh/tab": { - "version": "0.0.19", - "resolved": "https://registry.npmjs.org/@bomb.sh/tab/-/tab-0.0.19.tgz", - "integrity": "sha512-dTRfo9Q9B+lbLG3JCu8a/AGQSfD2XXcFcnakQzVjSOX+VvR/s9zpsH8TlqV3iHqazniRn1Ypwd1hcRlXcu/4BA==", - "license": "MIT", - "bin": { - "tab": "dist/bin/cli.mjs" - }, - "peerDependencies": { - "cac": "^6.7.14", - "citty": "^0.1.6 || ^0.2.0", - "commander": "^13.1.0 || ^14.0.0 || ^15.0.0" - }, - "peerDependenciesMeta": { - "cac": { - "optional": true - }, - "citty": { - "optional": true - }, - "commander": { - "optional": true - } - } - }, "node_modules/@clack/core": { "version": "1.0.0-alpha.7", "resolved": "https://registry.npmjs.org/@clack/core/-/core-1.0.0-alpha.7.tgz", @@ -487,11 +461,30 @@ } }, "node_modules/@colordx/core": { - "version": "5.8.0", - "resolved": "https://registry.npmjs.org/@colordx/core/-/core-5.8.0.tgz", - "integrity": "sha512-cG0QJAO6VkaRUlIb0zOzX9gfJgs1pjoOL9gZ/PK1kfvBs0GCkADu5oQh6gPxXgQnZ3gV575h+lQRC0NlMDTclA==", + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/@colordx/core/-/core-7.3.0.tgz", + "integrity": "sha512-LVzTTPhX7wpy7TkGjouA9zbbCBIw92RGm7ZQqSKFE0oYE3sMx2FGXz90eVpgw7k/+wrrY+qJ86nHXifZWMS5jA==", "license": "MIT" }, + "node_modules/@csstools/css-tokenizer": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.2.tgz", + "integrity": "sha512-OoKoR0f76dCY666JlcbhmVTs2drYj1GUXZTYTcbUgJjh9Nv41aFfZ21bPQTERm5+L5cBDo466NltB2lplS5GBw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + } + }, "node_modules/@dxup/nuxt": { "version": "0.5.10", "resolved": "https://registry.npmjs.org/@dxup/nuxt/-/nuxt-0.5.10.tgz", @@ -679,18 +672,6 @@ } } }, - "node_modules/@dxup/nuxt/node_modules/verkit": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/verkit/-/verkit-0.5.0.tgz", - "integrity": "sha512-u4J1hr6Y+C/hit34GwL3eIcUf3wngT8TlYUjK7tk0jxydKLAq3sYvifjdTDA6U3KCKRcQCQDGf/srjxfZF3l7g==", - "license": "MIT", - "engines": { - "node": ">=18.12.0" - }, - "funding": { - "url": "https://github.com/sponsors/sxzz" - } - }, "node_modules/@dxup/unimport": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/@dxup/unimport/-/unimport-0.1.2.tgz", @@ -1355,39 +1336,34 @@ } }, "node_modules/@nuxt/cli": { - "version": "3.37.0", - "resolved": "https://registry.npmjs.org/@nuxt/cli/-/cli-3.37.0.tgz", - "integrity": "sha512-Zj9NwHjEBzVrgezsgMFjpMhNqwNgROk9DzNi/dyfk1mCbXIRigV11br2xOl0Satek30bmv7oZAfMtCnDe6Ip0Q==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@nuxt/cli/-/cli-4.0.1.tgz", + "integrity": "sha512-vtB77CmkrYUMoIKwk+HUdNQX1vuMPpnu7ExNYB91WuIxzzGrGQLSxGUpXa360xsyUkZ4h9s+qElAQQW9ZQP4WA==", "license": "MIT", "dependencies": { - "@bomb.sh/tab": "^0.0.19", - "@clack/prompts": "^1.7.0", - "c12": "^3.3.4", - "citty": "^0.2.2", - "confbox": "^0.2.4", + "@clack/prompts": "^1.8.1", + "args-tokenizer": "^0.3.0", + "clickable-path": "^0.1.1", + "confbox": "^0.3.1", "consola": "^3.4.2", - "debug": "^4.4.3", "defu": "^6.1.7", - "exsolve": "^1.1.0", - "fuse.js": "^7.4.2", - "fzf": "^0.5.2", - "giget": "^3.3.0", - "jiti": "^2.7.0", - "listhen": "^1.10.0", - "nypm": "^0.6.8", - "ofetch": "^1.5.1", - "ohash": "^2.0.11", + "exsolve": "^1.1.3", + "fuzzysort": "^4.0.2", + "get-port-please": "^3.2.0", + "my-bad": "^0.2.9", + "obug": "^3.0.0", + "package-manager-detector": "^1.9.0", "pathe": "^2.0.3", "perfect-debounce": "^2.1.0", - "pkg-types": "^2.3.1", + "pkg-types": "^2.3.3", + "rc9": "^3.1.0", "scule": "^1.3.0", - "semver": "^7.8.5", - "srvx": "^0.11.22", - "std-env": "^4.2.0", - "tinyclip": "^0.1.15", - "tinyexec": "^1.2.4", - "ufo": "^1.6.4", - "youch": "^4.1.1" + "srvx": "^1.0.5", + "std-env": "^4.3.0", + "tinyclip": "^1.0.3", + "tinyexec": "^1.3.1", + "uqr": "^0.1.3", + "verkit": "^0.5.0" }, "bin": { "nuxi": "bin/nuxi.mjs", @@ -1396,14 +1372,34 @@ "nuxt-cli": "bin/nuxi.mjs" }, "engines": { - "node": "^16.14.0 || >=18.0.0" + "node": "^22.21.0 || ^24.11.0 || >=26.0.0" }, "peerDependencies": { - "@nuxt/schema": "^4.4.6" + "@nuxt/docs": "^4.0.0", + "@nuxt/schema": "^3.0.0 || ^4.0.0", + "jiti": "^2.3.0", + "oxc-parser": ">=0.56.3", + "rolldown": "^1.0.0-rc.16 || ^1.0.0", + "serve": "^14.0.0" }, "peerDependenciesMeta": { + "@nuxt/docs": { + "optional": true + }, "@nuxt/schema": { "optional": true + }, + "jiti": { + "optional": true + }, + "oxc-parser": { + "optional": true + }, + "rolldown": { + "optional": true + }, + "serve": { + "optional": true } } }, @@ -1435,16 +1431,23 @@ "node": ">= 20.12.0" } }, - "node_modules/@nuxt/cli/node_modules/srvx": { - "version": "0.11.22", - "resolved": "https://registry.npmjs.org/srvx/-/srvx-0.11.22.tgz", - "integrity": "sha512-LqZxxBDMKuMAZzFzJnDCkFOrs9MZQZr0LvHiO/SuSZVdQaXD7xQ5UWTUxheJrQPve1qk9MG2B/yttUvJxw8egQ==", + "node_modules/@nuxt/cli/node_modules/confbox": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.3.1.tgz", + "integrity": "sha512-cKUSoKa8YxFZZSmraVi7onONx3amu77ngK3kGpsYHDH7drPwCRkQE1RYMPlLRrMtnciRj274XNRxcHxnKmDSnA==", + "license": "MIT" + }, + "node_modules/@nuxt/cli/node_modules/obug": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/obug/-/obug-3.0.0.tgz", + "integrity": "sha512-5vvB5+W7ePv+p3uqxi+RcW1XAzLW0/hxt3/4X4Lc4qHudzOhmBiBwOY6DRob4WnanAEGvNcLjF+KNOufrUoEQw==", + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], "license": "MIT", - "bin": { - "srvx": "bin/srvx.mjs" - }, "engines": { - "node": ">=20.16.0" + "node": ">=12.20.0" } }, "node_modules/@nuxt/cli/node_modules/std-env": { @@ -1628,78 +1631,6 @@ } } }, - "node_modules/@nuxt/devtools-kit/node_modules/unplugin": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-3.4.0.tgz", - "integrity": "sha512-9skdIFlCsPdFV7wUfZxNsFInlW+7nJmGu2gkTu0OUhF56aXGsHab9x52/QhdJ4lC7ZDPWTxbiC4ANqVlsuaW3w==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "@jridgewell/remapping": "^2.3.5", - "picomatch": "^4.0.7", - "webpack-virtual-modules": "^0.6.2" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "peerDependencies": { - "@farmfe/core": "*", - "@rsbuild/core": "*", - "@rspack/core": "*", - "bun-types-no-globals": "*", - "esbuild": "*", - "rolldown": "*", - "rollup": "*", - "unloader": "*", - "vite": "*", - "webpack": "*" - }, - "peerDependenciesMeta": { - "@farmfe/core": { - "optional": true - }, - "@rsbuild/core": { - "optional": true - }, - "@rspack/core": { - "optional": true - }, - "bun-types-no-globals": { - "optional": true - }, - "esbuild": { - "optional": true - }, - "rolldown": { - "optional": true - }, - "rollup": { - "optional": true - }, - "unloader": { - "optional": true - }, - "vite": { - "optional": true - }, - "webpack": { - "optional": true - } - } - }, - "node_modules/@nuxt/devtools-kit/node_modules/verkit": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/verkit/-/verkit-0.5.0.tgz", - "integrity": "sha512-u4J1hr6Y+C/hit34GwL3eIcUf3wngT8TlYUjK7tk0jxydKLAq3sYvifjdTDA6U3KCKRcQCQDGf/srjxfZF3l7g==", - "license": "MIT", - "engines": { - "node": ">=18.12.0" - }, - "funding": { - "url": "https://github.com/sponsors/sxzz" - } - }, "node_modules/@nuxt/devtools-wizard": { "version": "3.4.2", "resolved": "https://registry.npmjs.org/@nuxt/devtools-wizard/-/devtools-wizard-3.4.2.tgz", @@ -1850,78 +1781,6 @@ } } }, - "node_modules/@nuxt/devtools/node_modules/unplugin": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-3.4.0.tgz", - "integrity": "sha512-9skdIFlCsPdFV7wUfZxNsFInlW+7nJmGu2gkTu0OUhF56aXGsHab9x52/QhdJ4lC7ZDPWTxbiC4ANqVlsuaW3w==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "@jridgewell/remapping": "^2.3.5", - "picomatch": "^4.0.7", - "webpack-virtual-modules": "^0.6.2" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "peerDependencies": { - "@farmfe/core": "*", - "@rsbuild/core": "*", - "@rspack/core": "*", - "bun-types-no-globals": "*", - "esbuild": "*", - "rolldown": "*", - "rollup": "*", - "unloader": "*", - "vite": "*", - "webpack": "*" - }, - "peerDependenciesMeta": { - "@farmfe/core": { - "optional": true - }, - "@rsbuild/core": { - "optional": true - }, - "@rspack/core": { - "optional": true - }, - "bun-types-no-globals": { - "optional": true - }, - "esbuild": { - "optional": true - }, - "rolldown": { - "optional": true - }, - "rollup": { - "optional": true - }, - "unloader": { - "optional": true - }, - "vite": { - "optional": true - }, - "webpack": { - "optional": true - } - } - }, - "node_modules/@nuxt/devtools/node_modules/verkit": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/verkit/-/verkit-0.5.0.tgz", - "integrity": "sha512-u4J1hr6Y+C/hit34GwL3eIcUf3wngT8TlYUjK7tk0jxydKLAq3sYvifjdTDA6U3KCKRcQCQDGf/srjxfZF3l7g==", - "license": "MIT", - "engines": { - "node": ">=18.12.0" - }, - "funding": { - "url": "https://github.com/sponsors/sxzz" - } - }, "node_modules/@nuxt/kit": { "version": "3.21.11", "resolved": "https://registry.npmjs.org/@nuxt/kit/-/kit-3.21.11.tgz", @@ -1955,48 +1814,48 @@ } }, "node_modules/@nuxt/nitro-server": { - "version": "4.5.2", - "resolved": "https://registry.npmjs.org/@nuxt/nitro-server/-/nitro-server-4.5.2.tgz", - "integrity": "sha512-sx/vtT8D1WIRUOMuKQz/9z8nZb7jgVWc6HWwlkXKDjYZ84otPFtYCozKqhXWv6xf3JxJvge/RUAOwh9Z7P4nQQ==", + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/@nuxt/nitro-server/-/nitro-server-4.6.1.tgz", + "integrity": "sha512-ecMNTxJ44uany4cjA58EW5AMbqcG0XgqP0d0HJmZ7z3CupPCc+qmd9InvzsX53uU7ZVzAkWmQ8K+0lrzl9Xp3A==", "license": "MIT", "dependencies": { - "@nuxt/devalue": "^2.0.2", - "@nuxt/kit": "4.5.2", - "@unhead/vue": "^3.3.1", - "@vue/shared": "^3.5.40", + "@nuxt/kit": "4.6.1", + "@unhead/vue": "^3.4.2", "consola": "^3.4.2", "defu": "^6.1.7", "destr": "^2.0.5", - "devalue": "^5.9.0", - "errx": "^0.1.2", + "devalue": "^6.0.2", + "errx": "^0.2.2", "escape-string-regexp": "^5.0.0", - "exsolve": "^1.1.1", + "exsolve": "^1.1.3", + "flru": "^1.0.2", "h3": "^1.15.11", - "impound": "^1.1.6", + "impound": "^1.2.0", "klona": "^2.0.6", "mocked-exports": "^0.1.1", + "my-bad": "^0.2.11", "nitropack": "^2.13.4", - "nostics": "^1.2.0", - "nypm": "^0.6.9", - "ohash": "^2.0.11", + "nostics": "^1.3.0", + "ohash": "^2.0.12", "pathe": "^2.0.3", - "rou3": "^0.9.1", - "std-env": "^4.2.0", + "rou3": "^0.9.2", + "std-env": "^4.3.0", "ufo": "^1.6.4", - "unctx": "^3.0.0", + "unctx": "^3.0.1", + "unrouting": "^0.2.3", "unstorage": "^1.17.5", - "vue": "^3.5.40", - "vue-bundle-renderer": "^2.3.1", + "vue": "^3.5.43", + "vue-bundle-renderer": "^2.4.0", "vue-devtools-stub": "^0.1.0" }, "engines": { - "node": "^22.19.0 || ^24.11.0 || >=26.0.0" + "node": "^22.22.3 || ^24.15.0 || >=26.0.0" }, "peerDependencies": { "@babel/plugin-proposal-decorators": "^7.25.0 || ^8.0.0", "@babel/plugin-syntax-typescript": "^7.25.0 || ^8.0.0", "@rollup/plugin-babel": "^6.0.0 || ^7.0.0", - "nuxt": "^4.5.2" + "nuxt": "^4.6.1" }, "peerDependenciesMeta": { "@babel/plugin-proposal-decorators": { @@ -2011,37 +1870,76 @@ } }, "node_modules/@nuxt/nitro-server/node_modules/@nuxt/kit": { - "version": "4.5.2", - "resolved": "https://registry.npmjs.org/@nuxt/kit/-/kit-4.5.2.tgz", - "integrity": "sha512-l66LU9DcJYjmNwqwAj2I5UGRrUbnG2DOKGChnN70zIGtn0eq/z87gi/FRgha6eMb9/FmB1PFHgtx6PWVml1C2Q==", + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/@nuxt/kit/-/kit-4.6.1.tgz", + "integrity": "sha512-ld5+/o4EMqyw6Qy+SdEYkezfprS87+p+xOBhnQgXwyrg1EdtILfLeudQ+hX6UyCnsoF6o0SOw3pjN1P4Tg9O0w==", "license": "MIT", "dependencies": { - "c12": "^3.3.4", "consola": "^3.4.2", "defu": "^6.1.7", "destr": "^2.0.5", - "errx": "^0.1.2", - "exsolve": "^1.1.1", - "ignore": "^7.0.6", - "jiti": "^2.7.0", + "errx": "^0.2.2", + "exsolve": "^1.1.3", + "ignore": "^7.0.12", "klona": "^2.0.6", - "mlly": "^1.8.2", - "nostics": "^1.2.0", - "ohash": "^2.0.11", + "microdiff": "^1.6.0", + "nostics": "^1.3.0", + "package-manager-detector": "^1.9.0", "pathe": "^2.0.3", - "pkg-types": "^2.3.1", - "rc9": "^3.0.1", + "rc9": "^3.1.0", "scule": "^1.3.0", - "tinyglobby": "^0.2.17", - "ufo": "^1.6.4", - "unctx": "^3.0.0", - "untyped": "^2.0.0", - "verkit": "^0.3.1" + "std-env": "^4.3.0", + "tinyexec": "^1.3.1", + "tinyglobby": "^0.2.17", + "unctx": "^3.0.1", + "verkit": "^0.5.0" }, "engines": { - "node": ">=18.12.0" + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@nuxt/schema": "^3.0.0 || ^4.0.0 || ^5.0.0-0", + "confbox": ">=0.1.0", + "dotenv": "*", + "giget": ">=3.1.0", + "jiti": "^2.0.0", + "mlly": "^1.2.0", + "oxc-parser": ">=0.56.0", + "rolldown": ">=1.0.0" + }, + "peerDependenciesMeta": { + "@nuxt/schema": { + "optional": true + }, + "confbox": { + "optional": true + }, + "dotenv": { + "optional": true + }, + "giget": { + "optional": true + }, + "jiti": { + "optional": true + }, + "mlly": { + "optional": true + }, + "oxc-parser": { + "optional": true + }, + "rolldown": { + "optional": true + } } }, + "node_modules/@nuxt/nitro-server/node_modules/errx": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/errx/-/errx-0.2.2.tgz", + "integrity": "sha512-HaPN6cxm6iZ9bIbQQ5orChcw7SY5smUzM08ZMc6OtV4UQhrrc3Ms1G8Ww2n/9lXkWqiioNTssF8bCvchrSbFEQ==", + "license": "MIT" + }, "node_modules/@nuxt/nitro-server/node_modules/rou3": { "version": "0.9.2", "resolved": "https://registry.npmjs.org/rou3/-/rou3-0.9.2.tgz", @@ -2080,78 +1978,18 @@ } } }, - "node_modules/@nuxt/nitro-server/node_modules/unplugin": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-3.4.0.tgz", - "integrity": "sha512-9skdIFlCsPdFV7wUfZxNsFInlW+7nJmGu2gkTu0OUhF56aXGsHab9x52/QhdJ4lC7ZDPWTxbiC4ANqVlsuaW3w==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "@jridgewell/remapping": "^2.3.5", - "picomatch": "^4.0.7", - "webpack-virtual-modules": "^0.6.2" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "peerDependencies": { - "@farmfe/core": "*", - "@rsbuild/core": "*", - "@rspack/core": "*", - "bun-types-no-globals": "*", - "esbuild": "*", - "rolldown": "*", - "rollup": "*", - "unloader": "*", - "vite": "*", - "webpack": "*" - }, - "peerDependenciesMeta": { - "@farmfe/core": { - "optional": true - }, - "@rsbuild/core": { - "optional": true - }, - "@rspack/core": { - "optional": true - }, - "bun-types-no-globals": { - "optional": true - }, - "esbuild": { - "optional": true - }, - "rolldown": { - "optional": true - }, - "rollup": { - "optional": true - }, - "unloader": { - "optional": true - }, - "vite": { - "optional": true - }, - "webpack": { - "optional": true - } - } - }, "node_modules/@nuxt/schema": { - "version": "4.5.2", - "resolved": "https://registry.npmjs.org/@nuxt/schema/-/schema-4.5.2.tgz", - "integrity": "sha512-h9g1kt9O2iU9nX6HDFu9gvwtpn+8oSJX0RSTWRBnEx/Pkz+WlOHtjuS0SbkTAXw5aT1+H1GysWVwNTjJfBY/0w==", + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/@nuxt/schema/-/schema-4.6.1.tgz", + "integrity": "sha512-GecoPGRBk5Okpj51On87wZzBMseOqrp0bm5oa9CFIOEYR3rN9QNVsIwPw7P9OA4GOaK/smIVbo0tsmUScJFQiw==", "license": "MIT", "dependencies": { - "@vue/shared": "^3.5.40", + "@vue/shared": "^3.5.43", "defu": "^6.1.7", - "nostics": "^1.2.0", + "nostics": "^1.3.0", "pathe": "^2.0.3", - "pkg-types": "^2.3.1", - "std-env": "^4.2.0" + "pkg-types": "^2.3.3", + "std-env": "^4.3.0" }, "engines": { "node": "^14.18.0 || >=16.10.0" @@ -2274,48 +2112,47 @@ } }, "node_modules/@nuxt/vite-builder": { - "version": "4.5.2", - "resolved": "https://registry.npmjs.org/@nuxt/vite-builder/-/vite-builder-4.5.2.tgz", - "integrity": "sha512-fC8KUs5F1VpSEjTIjmx5pdflciMlp3FDCubx3AXElvRPLtBAPmyB69kmqVFjRnVQ8+vRHNyI7iBHUg9Se8srLA==", + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/@nuxt/vite-builder/-/vite-builder-4.6.1.tgz", + "integrity": "sha512-i4cGwvYVAOzEgnZ8kZh02gWX+44rg+PI71GIxLqc2F9fdHTfLP0ZG0rsMmGWZbed9DgzpO4hK9uhPC1DwCrW6w==", "license": "MIT", "dependencies": { - "@nuxt/kit": "4.5.2", - "@vitejs/plugin-vue": "^6.0.8", + "@nuxt/kit": "4.6.1", + "@vitejs/plugin-vue": "^6.0.9", "@vitejs/plugin-vue-jsx": "^5.1.6", - "autoprefixer": "^10.5.4", + "autoprefixer": "^10.6.1", + "clickable-path": "^0.1.1", "consola": "^3.4.2", - "cssnano": "^8.0.2", + "cssnano": "^9.4.0", "defu": "^6.1.7", "escape-string-regexp": "^5.0.0", - "exsolve": "^1.1.1", + "exsolve": "^1.1.3", "generic-names": "^4.0.0", "get-port-please": "^3.2.0", - "jiti": "^2.7.0", "js-tokens": "^10.0.0", "knitwork": "^1.3.0", - "mlly": "^1.8.2", "mocked-exports": "^0.1.1", - "nypm": "^0.6.9", + "my-bad": "^0.2.11", "pathe": "^2.0.3", - "pkg-types": "^2.3.1", - "postcss": "^8.5.25", + "pkg-types": "^2.3.3", + "postcss": "^8.5.29", "rolldown-string": "^0.3.1", - "seroval": "^1.6.2", - "std-env": "^4.2.0", + "seroval": "^1.6.8", + "std-env": "^4.3.0", "ufo": "^1.6.4", "unenv": "^2.0.0-rc.24", - "vite": "^8.2.0", + "vite": "^8.3.2", "vite-node": "^6.0.0", "vite-plugin-checker": "^0.14.5", - "vue-bundle-renderer": "^2.3.1" + "vue-bundle-renderer": "^2.4.0" }, "engines": { - "node": "^22.18.0 || ^24.11.0 || >=26.0.0" + "node": "^22.22.3 || ^24.15.0 || >=26.0.0" }, "peerDependencies": { "@babel/plugin-proposal-decorators": "^7.25.0 || ^8.0.0", "@babel/plugin-syntax-jsx": "^7.25.0 || ^8.0.0", - "nuxt": "4.5.2", + "nuxt": "4.6.1", "rolldown": "^1.0.0", "rollup-plugin-visualizer": "^6.0.0 || ^7.0.1", "vue": "^3.3.4" @@ -2336,37 +2173,76 @@ } }, "node_modules/@nuxt/vite-builder/node_modules/@nuxt/kit": { - "version": "4.5.2", - "resolved": "https://registry.npmjs.org/@nuxt/kit/-/kit-4.5.2.tgz", - "integrity": "sha512-l66LU9DcJYjmNwqwAj2I5UGRrUbnG2DOKGChnN70zIGtn0eq/z87gi/FRgha6eMb9/FmB1PFHgtx6PWVml1C2Q==", + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/@nuxt/kit/-/kit-4.6.1.tgz", + "integrity": "sha512-ld5+/o4EMqyw6Qy+SdEYkezfprS87+p+xOBhnQgXwyrg1EdtILfLeudQ+hX6UyCnsoF6o0SOw3pjN1P4Tg9O0w==", "license": "MIT", "dependencies": { - "c12": "^3.3.4", "consola": "^3.4.2", "defu": "^6.1.7", "destr": "^2.0.5", - "errx": "^0.1.2", - "exsolve": "^1.1.1", - "ignore": "^7.0.6", - "jiti": "^2.7.0", + "errx": "^0.2.2", + "exsolve": "^1.1.3", + "ignore": "^7.0.12", "klona": "^2.0.6", - "mlly": "^1.8.2", - "nostics": "^1.2.0", - "ohash": "^2.0.11", + "microdiff": "^1.6.0", + "nostics": "^1.3.0", + "package-manager-detector": "^1.9.0", "pathe": "^2.0.3", - "pkg-types": "^2.3.1", - "rc9": "^3.0.1", + "rc9": "^3.1.0", "scule": "^1.3.0", + "std-env": "^4.3.0", + "tinyexec": "^1.3.1", "tinyglobby": "^0.2.17", - "ufo": "^1.6.4", - "unctx": "^3.0.0", - "untyped": "^2.0.0", - "verkit": "^0.3.1" + "unctx": "^3.0.1", + "verkit": "^0.5.0" }, "engines": { - "node": ">=18.12.0" + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@nuxt/schema": "^3.0.0 || ^4.0.0 || ^5.0.0-0", + "confbox": ">=0.1.0", + "dotenv": "*", + "giget": ">=3.1.0", + "jiti": "^2.0.0", + "mlly": "^1.2.0", + "oxc-parser": ">=0.56.0", + "rolldown": ">=1.0.0" + }, + "peerDependenciesMeta": { + "@nuxt/schema": { + "optional": true + }, + "confbox": { + "optional": true + }, + "dotenv": { + "optional": true + }, + "giget": { + "optional": true + }, + "jiti": { + "optional": true + }, + "mlly": { + "optional": true + }, + "oxc-parser": { + "optional": true + }, + "rolldown": { + "optional": true + } } }, + "node_modules/@nuxt/vite-builder/node_modules/errx": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/errx/-/errx-0.2.2.tgz", + "integrity": "sha512-HaPN6cxm6iZ9bIbQQ5orChcw7SY5smUzM08ZMc6OtV4UQhrrc3Ms1G8Ww2n/9lXkWqiioNTssF8bCvchrSbFEQ==", + "license": "MIT" + }, "node_modules/@nuxt/vite-builder/node_modules/npm-run-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-6.0.0.tgz", @@ -2439,117 +2315,192 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@nuxt/vite-builder/node_modules/unplugin": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-3.4.0.tgz", - "integrity": "sha512-9skdIFlCsPdFV7wUfZxNsFInlW+7nJmGu2gkTu0OUhF56aXGsHab9x52/QhdJ4lC7ZDPWTxbiC4ANqVlsuaW3w==", + "node_modules/@nuxt/vite-builder/node_modules/vite-plugin-checker": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/vite-plugin-checker/-/vite-plugin-checker-0.14.5.tgz", + "integrity": "sha512-c9lQ92eisUO+F7Fd93aelojmiOS+NQpPgQ1XR2LTQHox1/laZf4yAoQj+L3RA9Vgh10e2nFd9b8r2LLyYZsbpA==", "license": "MIT", - "optional": true, - "peer": true, "dependencies": { - "@jridgewell/remapping": "^2.3.5", - "picomatch": "^4.0.7", - "webpack-virtual-modules": "^0.6.2" + "@babel/code-frame": "^7.29.0", + "chokidar": "^5.0.0", + "npm-run-path": "^6.0.0", + "picocolors": "^1.1.1", + "picomatch": "^4.0.4", + "proper-lockfile": "^4.1.2", + "tiny-invariant": "^1.3.3" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@biomejs/biome": ">=2.4.12", + "eslint": ">=9.39.4", + "meow": "^13.2.0 || ^14.0.0", + "optionator": "^0.9.4", + "oxlint": ">=1", + "stylelint": ">=16.26.1", + "typescript": "*", + "vite": ">=5.4.21", + "vue-tsc": "~2.2.10 || ^3.0.0" + }, + "peerDependenciesMeta": { + "@biomejs/biome": { + "optional": true + }, + "eslint": { + "optional": true + }, + "meow": { + "optional": true + }, + "optionator": { + "optional": true + }, + "oxlint": { + "optional": true + }, + "stylelint": { + "optional": true + }, + "typescript": { + "optional": true + }, + "vue-tsc": { + "optional": true + } + } + }, + "node_modules/@nuxt/vite-server": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/@nuxt/vite-server/-/vite-server-4.6.1.tgz", + "integrity": "sha512-4/CE30fFc3loj2k9m37Q9kskLwFCjKa+2AePafb5Q6AMtHE9RYGH+lShbH4xJVk/uFXSiibUU8VB3H+84O9ZoQ==", + "license": "MIT", + "dependencies": { + "@nuxt/kit": "4.6.1", + "clickable-path": "^0.1.1", + "defu": "^6.1.7", + "exsolve": "^1.1.3", + "mocked-exports": "^0.1.1", + "my-bad": "^0.2.11", + "pathe": "^2.0.3", + "rou3": "^0.9.2", + "srvx": "1.0.5", + "ufo": "^1.6.4", + "ultrahtml": "^1.7.0" + }, + "engines": { + "node": "^22.22.3 || ^24.15.0 || >=26.0.0" + }, + "peerDependencies": { + "nuxt": "^4.6.1", + "vite": "^8.3.2" + } + }, + "node_modules/@nuxt/vite-server/node_modules/@nuxt/kit": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/@nuxt/kit/-/kit-4.6.1.tgz", + "integrity": "sha512-ld5+/o4EMqyw6Qy+SdEYkezfprS87+p+xOBhnQgXwyrg1EdtILfLeudQ+hX6UyCnsoF6o0SOw3pjN1P4Tg9O0w==", + "license": "MIT", + "dependencies": { + "consola": "^3.4.2", + "defu": "^6.1.7", + "destr": "^2.0.5", + "errx": "^0.2.2", + "exsolve": "^1.1.3", + "ignore": "^7.0.12", + "klona": "^2.0.6", + "microdiff": "^1.6.0", + "nostics": "^1.3.0", + "package-manager-detector": "^1.9.0", + "pathe": "^2.0.3", + "rc9": "^3.1.0", + "scule": "^1.3.0", + "std-env": "^4.3.0", + "tinyexec": "^1.3.1", + "tinyglobby": "^0.2.17", + "unctx": "^3.0.1", + "verkit": "^0.5.0" }, "engines": { "node": "^20.19.0 || >=22.12.0" }, "peerDependencies": { - "@farmfe/core": "*", - "@rsbuild/core": "*", - "@rspack/core": "*", - "bun-types-no-globals": "*", - "esbuild": "*", - "rolldown": "*", - "rollup": "*", - "unloader": "*", - "vite": "*", - "webpack": "*" + "@nuxt/schema": "^3.0.0 || ^4.0.0 || ^5.0.0-0", + "confbox": ">=0.1.0", + "dotenv": "*", + "giget": ">=3.1.0", + "jiti": "^2.0.0", + "mlly": "^1.2.0", + "oxc-parser": ">=0.56.0", + "rolldown": ">=1.0.0" }, "peerDependenciesMeta": { - "@farmfe/core": { - "optional": true - }, - "@rsbuild/core": { - "optional": true - }, - "@rspack/core": { + "@nuxt/schema": { "optional": true }, - "bun-types-no-globals": { + "confbox": { "optional": true }, - "esbuild": { + "dotenv": { "optional": true }, - "rolldown": { + "giget": { "optional": true }, - "rollup": { + "jiti": { "optional": true }, - "unloader": { + "mlly": { "optional": true }, - "vite": { + "oxc-parser": { "optional": true }, - "webpack": { + "rolldown": { "optional": true } } }, - "node_modules/@nuxt/vite-builder/node_modules/vite-plugin-checker": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/vite-plugin-checker/-/vite-plugin-checker-0.14.5.tgz", - "integrity": "sha512-c9lQ92eisUO+F7Fd93aelojmiOS+NQpPgQ1XR2LTQHox1/laZf4yAoQj+L3RA9Vgh10e2nFd9b8r2LLyYZsbpA==", + "node_modules/@nuxt/vite-server/node_modules/errx": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/errx/-/errx-0.2.2.tgz", + "integrity": "sha512-HaPN6cxm6iZ9bIbQQ5orChcw7SY5smUzM08ZMc6OtV4UQhrrc3Ms1G8Ww2n/9lXkWqiioNTssF8bCvchrSbFEQ==", + "license": "MIT" + }, + "node_modules/@nuxt/vite-server/node_modules/rou3": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/rou3/-/rou3-0.9.2.tgz", + "integrity": "sha512-3SOzvaAg8rkHrXtRjpCvCvbyO5to9oOO27Z/XqHEYXfMRVSw/qMIVdmaOk9W2lcRLtR6dlqTjo9hDeJk70QBYQ==", + "license": "MIT" + }, + "node_modules/@nuxt/vite-server/node_modules/std-env": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.3.0.tgz", + "integrity": "sha512-OtU/EgQ1kIm5KwqQpBC6ZEMXrZRui11w8zgfTWp8cdO9B8OaPsbA8bTHO2P+HNo1VlUTGMVBwPhydu6poeXiag==", + "license": "MIT" + }, + "node_modules/@nuxt/vite-server/node_modules/unctx": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/unctx/-/unctx-3.0.1.tgz", + "integrity": "sha512-5RAt2etv7g362RXyd33R82gm9u/kbtQlpoaOs9Bgm4E32GRMJ0xjbZyvdxUTmiuHk3V1IQb1aUNrp5IWY+JaWw==", "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.29.0", - "chokidar": "^5.0.0", - "npm-run-path": "^6.0.0", - "picocolors": "^1.1.1", - "picomatch": "^4.0.4", - "proper-lockfile": "^4.1.2", - "tiny-invariant": "^1.3.3" - }, - "engines": { - "node": ">=20.19.0" - }, "peerDependencies": { - "@biomejs/biome": ">=2.4.12", - "eslint": ">=9.39.4", - "meow": "^13.2.0 || ^14.0.0", - "optionator": "^0.9.4", - "oxlint": ">=1", - "stylelint": ">=16.26.1", - "typescript": "*", - "vite": ">=5.4.21", - "vue-tsc": "~2.2.10 || ^3.0.0" + "magic-string": ">=0.30.21", + "oxc-parser": ">=0.140.0", + "rolldown": "^1.1.5", + "unplugin": "^3.3.0" }, "peerDependenciesMeta": { - "@biomejs/biome": { - "optional": true - }, - "eslint": { - "optional": true - }, - "meow": { - "optional": true - }, - "optionator": { - "optional": true - }, - "oxlint": { + "magic-string": { "optional": true }, - "stylelint": { + "oxc-parser": { "optional": true }, - "typescript": { + "rolldown": { "optional": true }, - "vue-tsc": { + "unplugin": { "optional": true } } @@ -3713,9 +3664,9 @@ "license": "MIT" }, "node_modules/@vitejs/plugin-vue": { - "version": "6.0.9", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.9.tgz", - "integrity": "sha512-rD/MORlhaZMlXWW0rEn4FB4wMinWC0z/D7Ye160S5+Rs1mC8ZDAdcDp4SjKfUhG5t8q1ktcPVw4xuTkPRlzrSA==", + "version": "6.0.10", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.10.tgz", + "integrity": "sha512-b80aUQh3n+qEzzZm5oQp3rT2QImeCx58K6eFl/RkJ3d65KW0kTuv5lNsLRVAjbW9YRUpo/wzmvzMgV34bTAItA==", "license": "MIT", "dependencies": { "@rolldown/pluginutils": "^1.0.1" @@ -3725,7 +3676,7 @@ }, "peerDependencies": { "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0", - "vue": "^3.2.25" + "vue": "^3.2.25 || ^3.6.0-rc.10" } }, "node_modules/@vitejs/plugin-vue-jsx": { @@ -4035,12 +3986,6 @@ "he": "^1.2.0" } }, - "node_modules/@vue/devtools-api": { - "version": "6.6.4", - "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz", - "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==", - "license": "MIT" - }, "node_modules/@vue/devtools-core": { "version": "8.2.1", "resolved": "https://registry.npmjs.org/@vue/devtools-core/-/devtools-core-8.2.1.tgz", @@ -4452,6 +4397,12 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/args-tokenizer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/args-tokenizer/-/args-tokenizer-0.3.0.tgz", + "integrity": "sha512-xXAd7G2Mll5W8uo37GETpQ2VrE84M181Z7ugHFGQnJZ50M2mbOv0osSZ9VsSgPfJQ+LVG0prSi0th+ELMsno7Q==", + "license": "MIT" + }, "node_modules/assertion-error": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", @@ -4644,9 +4595,9 @@ } }, "node_modules/bare-fs": { - "version": "4.8.3", - "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.8.3.tgz", - "integrity": "sha512-KQi9ShyPS/nOCtKpma7At8wbq4ORSNUmD67hsZrPcaWJcEmL8PHyS2xVWT308y8/LvHFOUTytTTDsD8dInx6bA==", + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.8.4.tgz", + "integrity": "sha512-MPC2eyrpbk3A873sK4y6rk+Iv4o2enxTjHZ8zH3HO0mjrpm5XjnguY2XmfidIsDV6vnjpbMBKUAJpV0ISD2SsQ==", "license": "Apache-2.0", "dependencies": { "bare-events": "^2.5.4", @@ -4908,7 +4859,7 @@ "version": "6.7.14", "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -5001,6 +4952,15 @@ "integrity": "sha512-+6vJA3L98yv+IdfKGZHBNiGW5KHn22e/JwID0Strsz8h4S/csAu/OuICwxrg44k5MRiZHWIo8XXuJgQTriRP4w==", "license": "MIT" }, + "node_modules/clickable-path": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/clickable-path/-/clickable-path-0.1.1.tgz", + "integrity": "sha512-ROn/mdV2pR8WFdpJjFDCSouLRg9myf95xacO1iy8G/AVO/x5y124pjF3EaoWrTBRWC3vh0DPqa/MhKyx3mMeKg==", + "license": "MIT", + "engines": { + "node": ">=22.1.0" + } + }, "node_modules/cliui": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", @@ -5069,7 +5029,7 @@ "version": "14.0.3", "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=20" @@ -5306,78 +5266,80 @@ } }, "node_modules/cssnano": { - "version": "8.0.10", - "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-8.0.10.tgz", - "integrity": "sha512-aMlBsvQW5g1b8vfPnkuy5Bk7g/jyRsf5eMpbhe4nKnePx+n2IrC0L6dFqnd3r4/OGNIYGEHfAt09oywi84fWKQ==", + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-9.5.0.tgz", + "integrity": "sha512-PlL7BWzHLWAgOyK9dohMmi0g4TwmsCwc3oxLsxa8S1UMaXLawaP0yle0/8X6nKC/ojk7vE38meARlwz7/Q5BAQ==", "license": "MIT", "dependencies": { - "cssnano-preset-default": "^8.0.10", - "lilconfig": "^3.1.3" + "cssnano-preset-default": "^9.5.0" }, "engines": { - "node": "^22.11.0 || ^24.11.0 || >=26.0" + "node": "^22.22.3 || ^24.15.0 || >=26.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/cssnano" }, "peerDependencies": { - "postcss": "^8.5.26" + "postcss": "^8.5.29" } }, "node_modules/cssnano-preset-default": { - "version": "8.0.10", - "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-8.0.10.tgz", - "integrity": "sha512-34uq3q7OXgmfxXn+MHTgIqysCLH7zRZZUMAwrBiO09F5/KLVD33iDFNRRUNYStKdj0SaAIUVr6Akao2Fpp0aVA==", - "license": "MIT", - "dependencies": { - "browserslist": "^4.28.8", - "cssnano-utils": "^6.0.4", - "postcss-calc": "^10.1.1", - "postcss-colormin": "^8.0.5", - "postcss-convert-values": "^8.0.4", - "postcss-discard-comments": "^8.0.4", - "postcss-discard-duplicates": "^8.0.4", - "postcss-discard-empty": "^8.0.5", - "postcss-discard-overridden": "^8.0.4", - "postcss-merge-longhand": "^8.0.4", - "postcss-merge-rules": "^8.0.5", - "postcss-minify-font-values": "^8.0.4", - "postcss-minify-gradients": "^8.0.6", - "postcss-minify-params": "^8.0.4", - "postcss-minify-selectors": "^8.0.5", - "postcss-normalize-charset": "^8.0.5", - "postcss-normalize-display-values": "^8.0.4", - "postcss-normalize-positions": "^8.0.4", - "postcss-normalize-repeat-style": "^8.0.4", - "postcss-normalize-string": "^8.0.4", - "postcss-normalize-timing-functions": "^8.0.4", - "postcss-normalize-unicode": "^8.0.4", - "postcss-normalize-url": "^8.0.4", - "postcss-normalize-whitespace": "^8.0.5", - "postcss-ordered-values": "^8.0.5", - "postcss-reduce-initial": "^8.0.5", - "postcss-reduce-transforms": "^8.0.4", - "postcss-svgo": "^8.0.6", - "postcss-unique-selectors": "^8.0.4" - }, - "engines": { - "node": "^22.11.0 || ^24.11.0 || >=26.0" + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-9.5.0.tgz", + "integrity": "sha512-gubPgjwbJLttmWV5LuRn2HBJ1a84XNXqEsEj1gZ57rqyMzo/c96BZ6BdybyrrI2AwUfWNFcrxG2ZKPEcvUlvNw==", + "license": "MIT", + "dependencies": { + "browserslist": "^4.29.3", + "cssnano-utils": "^8.2.1", + "postcss-calc": "^11.2.2", + "postcss-colormin": "^9.0.7", + "postcss-convert-values": "^9.0.8", + "postcss-discard-comments": "^9.0.7", + "postcss-discard-duplicates": "^9.0.6", + "postcss-discard-empty": "^9.0.7", + "postcss-discard-overridden": "^9.0.6", + "postcss-merge-longhand": "^9.3.0", + "postcss-merge-rules": "^9.1.3", + "postcss-minify-font-values": "^9.0.8", + "postcss-minify-gradients": "^9.0.7", + "postcss-minify-params": "^9.0.7", + "postcss-minify-selectors": "^9.0.7", + "postcss-normalize-charset": "^9.0.6", + "postcss-normalize-display-values": "^9.0.6", + "postcss-normalize-positions": "^9.0.7", + "postcss-normalize-repeat-style": "^9.0.6", + "postcss-normalize-string": "^9.0.6", + "postcss-normalize-timing-functions": "^9.0.6", + "postcss-normalize-unicode": "^9.1.0", + "postcss-normalize-url": "^9.0.6", + "postcss-normalize-whitespace": "^9.0.7", + "postcss-ordered-values": "^9.0.8", + "postcss-reduce-initial": "^9.0.7", + "postcss-reduce-transforms": "^9.0.5", + "postcss-svgo": "^9.0.5", + "postcss-unique-selectors": "^9.0.6" + }, + "engines": { + "node": "^22.22.3 || ^24.15.0 || >=26.0" }, "peerDependencies": { - "postcss": "^8.5.26" + "postcss": "^8.5.29" } }, "node_modules/cssnano-utils": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/cssnano-utils/-/cssnano-utils-6.0.4.tgz", - "integrity": "sha512-j1z2mW4MqtcGM4I8TxXAdOXUifp1DZ5/bZnyYnCpHlzQ/ilCj2voLxE0aqEKnBDA5hF6HahSY13JU8kyZ1s8Mg==", + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/cssnano-utils/-/cssnano-utils-8.2.1.tgz", + "integrity": "sha512-ZdZOIbZdO6bDQbRAEdZKsWuaRD00sAltCSkSx0ipDjAc1THTV01Lw3NVAYMrgYbb4Yz3hySIpCHi+x0qUGzohA==", "license": "MIT", + "dependencies": { + "@csstools/css-tokenizer": "^4.0.2" + }, "engines": { - "node": "^22.11.0 || ^24.11.0 || >=26.0" + "node": "^22.22.3 || ^24.15.0 || >=26.0" }, "peerDependencies": { - "postcss": "^8.5.26" + "postcss": "^8.5.29" } }, "node_modules/csso": { @@ -5576,10 +5538,13 @@ } }, "node_modules/devalue": { - "version": "5.9.4", - "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.9.4.tgz", - "integrity": "sha512-sPAT4pztbu6586/hrhOnMKS17IJrvg12mXiSPSS3W5qDeN2RGgvZ0diZCm31dBbnevfVmujNO3IM2wrS4Y2Rhg==", - "license": "MIT" + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/devalue/-/devalue-6.0.2.tgz", + "integrity": "sha512-6V1HzU4to74skg//ScZR2mp9bdz+rESC9jQzVIIG/ha0HYgymHcXIQQCjH0+ivRaSuY82fa9fZ7Ro6QZT4s4uw==", + "license": "MIT", + "engines": { + "node": ">=22.17" + } }, "node_modules/diff": { "version": "8.0.4", @@ -6042,6 +6007,12 @@ } } }, + "node_modules/fetchdts": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/fetchdts/-/fetchdts-0.2.0.tgz", + "integrity": "sha512-PJb4ZTGT2/wK78RmVrxZITStWabhZlKBsL52VOgq+c7clEkdHdEB+0rtzrXCbZJqeYJj+IwNWYqB5/G6vouA6A==", + "license": "MIT" + }, "node_modules/file-uri-to-path": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", @@ -6060,6 +6031,15 @@ "node": ">=8" } }, + "node_modules/flru": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/flru/-/flru-1.0.2.tgz", + "integrity": "sha512-kWyh8ADvHBFz6ua5xYOPnUroZTT/bwWfrCeL0Wj1dzG4/YOmOcfJ99W8dOVyyynJN35rZ9aCOtHChqQovV7yog==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/fnv1a-64": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/fnv1a-64/-/fnv1a-64-0.1.2.tgz", @@ -6127,24 +6107,11 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/fuse.js": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-7.5.0.tgz", - "integrity": "sha512-sQtrEfA+ez/3G0cCZecF70oqpCRttCexYUG4mUrtWL49ULUzUyxokt5kyqwtKzj1270RaKih+hcP3qLcumccow==", - "license": "Apache-2.0", - "engines": { - "node": ">=10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/krisk" - } - }, - "node_modules/fzf": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fzf/-/fzf-0.5.2.tgz", - "integrity": "sha512-Tt4kuxLXFKHy8KT40zwsUPUkg1CrsgY25FxA2U/j/0WgEDCk3ddc/zLTCCcbSHX9FcKtLuVaDGtGE/STWC+j3Q==", - "license": "BSD-3-Clause" + "node_modules/fuzzysort": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/fuzzysort/-/fuzzysort-4.0.2.tgz", + "integrity": "sha512-3c7yD6rK1EXIsqpcu1ZVzXJugODWNMzvUij6yTiziaAHrpNS8kzFOI9mJLE8pmG4K9JOaVgzQRgklfOEk8f4Tg==", + "license": "MIT" }, "node_modules/generic-names": { "version": "4.0.0", @@ -7205,18 +7172,6 @@ "url": "https://opencollective.com/parcel" } }, - "node_modules/lilconfig": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", - "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", - "license": "MIT", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/antonk52" - } - }, "node_modules/listhen": { "version": "1.10.1", "resolved": "https://registry.npmjs.org/listhen/-/listhen-1.10.1.tgz", @@ -7259,6 +7214,15 @@ "integrity": "sha512-OtU/EgQ1kIm5KwqQpBC6ZEMXrZRui11w8zgfTWp8cdO9B8OaPsbA8bTHO2P+HNo1VlUTGMVBwPhydu6poeXiag==", "license": "MIT" }, + "node_modules/listhen/node_modules/tinyclip": { + "version": "0.1.15", + "resolved": "https://registry.npmjs.org/tinyclip/-/tinyclip-0.1.15.tgz", + "integrity": "sha512-uo33abH+Ays0xYaDysoBt494Hb3hsEczMpcC0MwFl773pazORx4fmvKhclhR1wonUbB6vvpRsvVMwnhfqeMc+A==", + "license": "MIT", + "engines": { + "node": "^16.14.0 || >= 17.3.0" + } + }, "node_modules/loader-utils": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-3.3.1.tgz", @@ -7538,6 +7502,34 @@ "integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==", "license": "MIT" }, + "node_modules/my-bad": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/my-bad/-/my-bad-0.2.11.tgz", + "integrity": "sha512-K1R6DRz31BR7YYaDDLRr97XWKWku3SQhHKpDgz7i/VS3O3pvphIzbJIrrEXupSEmPtZElY9UzCrDc0nWvG4HyA==", + "license": "MIT", + "dependencies": { + "clickable-path": "^0.1.1", + "errx": "^0.2.2", + "fnv1a-64": "^0.1.2" + }, + "engines": { + "node": ">=22.12.0" + }, + "peerDependencies": { + "vite": ">=6" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } + } + }, + "node_modules/my-bad/node_modules/errx": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/errx/-/errx-0.2.2.tgz", + "integrity": "sha512-HaPN6cxm6iZ9bIbQQ5orChcw7SY5smUzM08ZMc6OtV4UQhrrc3Ms1G8Ww2n/9lXkWqiioNTssF8bCvchrSbFEQ==", + "license": "MIT" + }, "node_modules/nanoid": { "version": "3.3.20", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.20.tgz", @@ -7647,41 +7639,145 @@ "node": "^20.19.0 || >=22.12.0" }, "peerDependencies": { - "xml2js": "^0.6.2" + "xml2js": "^0.6.2" + }, + "peerDependenciesMeta": { + "xml2js": { + "optional": true + } + } + }, + "node_modules/nitropack/node_modules/cookie-es": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/cookie-es/-/cookie-es-2.0.1.tgz", + "integrity": "sha512-aVf4A4hI2w70LnF7GG+7xDQUkliwiXWXFvTjkip4+b64ygDQ2sJPRSKFDHbxn8o0xu9QzPkMuuiWIXyFSE2slA==", + "license": "MIT" + }, + "node_modules/nitropack/node_modules/crossws": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/crossws/-/crossws-0.3.5.tgz", + "integrity": "sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA==", + "license": "MIT", + "dependencies": { + "uncrypto": "^0.1.3" + } + }, + "node_modules/nitropack/node_modules/hookable": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz", + "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==", + "license": "MIT" + }, + "node_modules/nitropack/node_modules/std-env": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.3.0.tgz", + "integrity": "sha512-OtU/EgQ1kIm5KwqQpBC6ZEMXrZRui11w8zgfTWp8cdO9B8OaPsbA8bTHO2P+HNo1VlUTGMVBwPhydu6poeXiag==", + "license": "MIT" + }, + "node_modules/nitropack/node_modules/unimport": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/unimport/-/unimport-6.5.0.tgz", + "integrity": "sha512-t0KLJLRbebz3f7NrQe+vy2ObeugVeM1XqI2oeYnauddmHSglYj3U6bxOJ65IbXmOFkcAGFJK2OyGH7aqHPM6Ug==", + "license": "MIT", + "dependencies": { + "acorn": "^8.18.0", + "escape-string-regexp": "^5.0.0", + "estree-walker": "^3.0.3", + "local-pkg": "^1.2.1", + "magic-string": "^1.2.3", + "mlly": "^1.8.2", + "pathe": "^2.0.3", + "picomatch": "^4.0.7", + "pkg-types": "^2.3.3", + "scule": "^1.3.0", + "strip-literal": "^4.0.0", + "tinyglobby": "^0.2.17", + "unplugin": "^3.3.0", + "unplugin-utils": "^0.3.2" + }, + "engines": { + "node": ">=18.12.0" + }, + "peerDependencies": { + "oxc-parser": "*", + "rolldown": "^1.0.0" + }, + "peerDependenciesMeta": { + "oxc-parser": { + "optional": true + }, + "rolldown": { + "optional": true + } + } + }, + "node_modules/nitropack/node_modules/unimport/node_modules/magic-string": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.4.3.tgz", + "integrity": "sha512-z12OxmaPGE0F4xlpGdjuAUckipLpQlTAuAmyGhNoD7ODpYUzvDfvtxUbjM/jwznyP178oju/KDdyi220wNJ0RQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.6.0" + } + }, + "node_modules/nitropack/node_modules/unplugin": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-3.4.0.tgz", + "integrity": "sha512-9skdIFlCsPdFV7wUfZxNsFInlW+7nJmGu2gkTu0OUhF56aXGsHab9x52/QhdJ4lC7ZDPWTxbiC4ANqVlsuaW3w==", + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "picomatch": "^4.0.7", + "webpack-virtual-modules": "^0.6.2" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@farmfe/core": "*", + "@rsbuild/core": "*", + "@rspack/core": "*", + "bun-types-no-globals": "*", + "esbuild": "*", + "rolldown": "*", + "rollup": "*", + "unloader": "*", + "vite": "*", + "webpack": "*" }, "peerDependenciesMeta": { - "xml2js": { + "@farmfe/core": { + "optional": true + }, + "@rsbuild/core": { + "optional": true + }, + "@rspack/core": { + "optional": true + }, + "bun-types-no-globals": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "rolldown": { + "optional": true + }, + "rollup": { + "optional": true + }, + "unloader": { + "optional": true + }, + "vite": { + "optional": true + }, + "webpack": { "optional": true } } }, - "node_modules/nitropack/node_modules/cookie-es": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/cookie-es/-/cookie-es-2.0.1.tgz", - "integrity": "sha512-aVf4A4hI2w70LnF7GG+7xDQUkliwiXWXFvTjkip4+b64ygDQ2sJPRSKFDHbxn8o0xu9QzPkMuuiWIXyFSE2slA==", - "license": "MIT" - }, - "node_modules/nitropack/node_modules/crossws": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/crossws/-/crossws-0.3.5.tgz", - "integrity": "sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA==", - "license": "MIT", - "dependencies": { - "uncrypto": "^0.1.3" - } - }, - "node_modules/nitropack/node_modules/hookable": { - "version": "5.5.3", - "resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz", - "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==", - "license": "MIT" - }, - "node_modules/nitropack/node_modules/std-env": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.3.0.tgz", - "integrity": "sha512-OtU/EgQ1kIm5KwqQpBC6ZEMXrZRui11w8zgfTWp8cdO9B8OaPsbA8bTHO2P+HNo1VlUTGMVBwPhydu6poeXiag==", - "license": "MIT" - }, "node_modules/node-fetch": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", @@ -7814,77 +7910,84 @@ } }, "node_modules/nuxt": { - "version": "4.5.2", - "resolved": "https://registry.npmjs.org/nuxt/-/nuxt-4.5.2.tgz", - "integrity": "sha512-tR3fcqeHlHmmkLMpIg3V7Y+1ltr302lW8djMw/iy+myfo7QSSz+BVJDuQhg5j73b9oteSyBfOKTDYTgvMtj6TA==", - "license": "MIT", - "dependencies": { - "@dxup/nuxt": "^0.5.6", - "@nuxt/cli": "^3.37.0", - "@nuxt/devtools": "^3.4.1", - "@nuxt/kit": "4.5.2", - "@nuxt/nitro-server": "4.5.2", - "@nuxt/schema": "4.5.2", - "@nuxt/telemetry": "^2.8.0", - "@nuxt/vite-builder": "4.5.2", - "@unhead/vue": "^3.3.1", - "@vue/shared": "^3.5.40", + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/nuxt/-/nuxt-4.6.1.tgz", + "integrity": "sha512-L6Emg0OezAgqVlFrQ64Mjd8NpSu3FfR0GchDmmSjYrjhGyDFgVzY0pw201UCApXJaeK/06jW7ZjxE2pLZkELTw==", + "license": "MIT", + "dependencies": { + "@dxup/nuxt": "^0.5.10", + "@jridgewell/trace-mapping": "^0.3.31", + "@nuxt/cli": "^4.0.1", + "@nuxt/devalue": "^2.0.2", + "@nuxt/devtools": "^3.4.2", + "@nuxt/kit": "4.6.1", + "@nuxt/nitro-server": "4.6.1", + "@nuxt/schema": "4.6.1", + "@nuxt/telemetry": "^2.9.1", + "@nuxt/vite-builder": "4.6.1", + "@nuxt/vite-server": "4.6.1", + "@unhead/vue": "^3.4.2", + "@vue/shared": "^3.5.43", "chokidar": "^5.0.0", + "clickable-path": "^0.1.1", "compatx": "^0.2.0", "consola": "^3.4.2", "cookie-es": "^3.1.1", "defu": "^6.1.7", - "devalue": "^5.9.0", - "errx": "^0.1.2", + "destr": "^2.0.5", + "devalue": "^6.0.2", + "errx": "^0.2.2", "escape-string-regexp": "^5.0.0", - "exsolve": "^1.1.1", + "exsolve": "^1.1.3", + "fetchdts": "^0.2.0", "fnv1a-64": "^0.1.2", - "hookable": "^6.1.1", - "ignore": "^7.0.6", - "impound": "^1.1.6", + "hookable": "^6.1.2", + "ignore": "^7.0.12", + "impound": "^1.2.0", + "iron-webcrypto": "^2.0.0", "jiti": "^2.7.0", "klona": "^2.0.6", "knitwork": "^1.3.0", - "magic-string": "^1.1.0", - "mlly": "^1.8.2", + "magic-string": "^1.4.3", + "my-bad": "^0.2.11", "nanotar": "^0.3.0", - "nostics": "^1.2.0", - "nypm": "^0.6.9", + "nostics": "^1.3.0", "object-identity": "^0.2.3", "ofetch": "^1.5.1", - "ohash": "^2.0.11", + "ohash": "^2.0.12", "on-change": "^6.0.2", "oxc-walker": "^1.1.1", "pathe": "^2.0.3", "perfect-debounce": "^2.1.0", - "picomatch": "^4.0.5", - "pkg-types": "^2.3.1", + "picomatch": "^4.0.7", + "pkg-types": "^2.3.3", "rolldown-string": "^0.3.1", - "rou3": "^0.9.1", + "rou3": "^0.9.2", "scule": "^1.3.0", - "std-env": "^4.2.0", + "std-env": "^4.3.0", "tinyglobby": "^0.2.17", "ufo": "^1.6.4", "ultrahtml": "^1.7.0", "uncrypto": "^0.1.3", - "unctx": "^3.0.0", - "undici": "^8.10.0", - "unhead": "^3.3.1", - "unimport": "^6.4.0", - "unplugin": "^3.3.0", - "unrouting": "^0.2.2", + "unctx": "^3.0.1", + "undici": "^8.11.2", + "unhead": "^3.4.2", + "unimport": "^7.0.2", + "unplugin": "^3.4.0", + "unrouting": "^0.2.3", "untyped": "^2.0.0", - "verkit": "^0.3.1", - "vue": "^3.5.40", - "vue-component-type-helpers": "^3.3.9", - "vue-router": "^5.2.0" + "verkit": "^0.5.0", + "vue": "^3.5.43", + "vue-bundle-renderer": "^2.4.0", + "vue-component-type-helpers": "^3.3.12", + "vue-router": "^5.4.0" }, "bin": { "nuxi": "bin/nuxt.mjs", "nuxt": "bin/nuxt.mjs" }, "engines": { - "node": "^22.19.0 || ^24.11.0 || >=26.0.0" + "node": "^22.22.3 || ^24.15.0 || >=26.0.0" }, "peerDependencies": { "@parcel/watcher": "^2.1.0", @@ -7901,35 +8004,68 @@ } }, "node_modules/nuxt/node_modules/@nuxt/kit": { - "version": "4.5.2", - "resolved": "https://registry.npmjs.org/@nuxt/kit/-/kit-4.5.2.tgz", - "integrity": "sha512-l66LU9DcJYjmNwqwAj2I5UGRrUbnG2DOKGChnN70zIGtn0eq/z87gi/FRgha6eMb9/FmB1PFHgtx6PWVml1C2Q==", + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/@nuxt/kit/-/kit-4.6.1.tgz", + "integrity": "sha512-ld5+/o4EMqyw6Qy+SdEYkezfprS87+p+xOBhnQgXwyrg1EdtILfLeudQ+hX6UyCnsoF6o0SOw3pjN1P4Tg9O0w==", "license": "MIT", "dependencies": { - "c12": "^3.3.4", "consola": "^3.4.2", "defu": "^6.1.7", "destr": "^2.0.5", - "errx": "^0.1.2", - "exsolve": "^1.1.1", - "ignore": "^7.0.6", - "jiti": "^2.7.0", + "errx": "^0.2.2", + "exsolve": "^1.1.3", + "ignore": "^7.0.12", "klona": "^2.0.6", - "mlly": "^1.8.2", - "nostics": "^1.2.0", - "ohash": "^2.0.11", + "microdiff": "^1.6.0", + "nostics": "^1.3.0", + "package-manager-detector": "^1.9.0", "pathe": "^2.0.3", - "pkg-types": "^2.3.1", - "rc9": "^3.0.1", + "rc9": "^3.1.0", "scule": "^1.3.0", + "std-env": "^4.3.0", + "tinyexec": "^1.3.1", "tinyglobby": "^0.2.17", - "ufo": "^1.6.4", - "unctx": "^3.0.0", - "untyped": "^2.0.0", - "verkit": "^0.3.1" + "unctx": "^3.0.1", + "verkit": "^0.5.0" }, "engines": { - "node": ">=18.12.0" + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@nuxt/schema": "^3.0.0 || ^4.0.0 || ^5.0.0-0", + "confbox": ">=0.1.0", + "dotenv": "*", + "giget": ">=3.1.0", + "jiti": "^2.0.0", + "mlly": "^1.2.0", + "oxc-parser": ">=0.56.0", + "rolldown": ">=1.0.0" + }, + "peerDependenciesMeta": { + "@nuxt/schema": { + "optional": true + }, + "confbox": { + "optional": true + }, + "dotenv": { + "optional": true + }, + "giget": { + "optional": true + }, + "jiti": { + "optional": true + }, + "mlly": { + "optional": true + }, + "oxc-parser": { + "optional": true + }, + "rolldown": { + "optional": true + } } }, "node_modules/nuxt/node_modules/@vue/devtools-api": { @@ -7953,6 +8089,24 @@ "integrity": "sha512-UaXxwISYJPTr9hwQxMFYZ7kNhSXboMXP+Z3TRX6f1/NyaGPfuNUZOWP1pUEb75B2HjfklIYLVRfWiFZJyC6Npg==", "license": "MIT" }, + "node_modules/nuxt/node_modules/errx": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/errx/-/errx-0.2.2.tgz", + "integrity": "sha512-HaPN6cxm6iZ9bIbQQ5orChcw7SY5smUzM08ZMc6OtV4UQhrrc3Ms1G8Ww2n/9lXkWqiioNTssF8bCvchrSbFEQ==", + "license": "MIT" + }, + "node_modules/nuxt/node_modules/iron-webcrypto": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/iron-webcrypto/-/iron-webcrypto-2.0.0.tgz", + "integrity": "sha512-rtffZKDUHciZElM8mjFCufBC7nVhCxHYyWHESqs89OioEDz4parOofd8/uhrejh/INhQFfYQfByS22LlezR9sQ==", + "license": "MIT", + "dependencies": { + "uint8array-extras": "^1.5.0" + }, + "funding": { + "url": "https://github.com/sponsors/brc-dd" + } + }, "node_modules/nuxt/node_modules/magic-string": { "version": "1.4.3", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.4.3.tgz", @@ -8394,429 +8548,431 @@ } }, "node_modules/postcss-calc": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-10.1.1.tgz", - "integrity": "sha512-NYEsLHh8DgG/PRH2+G9BTuUdtf9ViS+vdoQ0YA5OQdGsfN4ztiwtDWNtBl9EKeqNMFnIu8IKZ0cLxEQ5r5KVMw==", + "version": "11.2.3", + "resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-11.2.3.tgz", + "integrity": "sha512-pFW3MR2a4W7uHcSGUiyQGGXZWHon83ljbNwnUJ1mRKublWQbv+3o9NiqhlV4TJfNby+U+zLY2OgiywzhA5eFqw==", "license": "MIT", "dependencies": { - "postcss-selector-parser": "^7.0.0", - "postcss-value-parser": "^4.2.0" + "@csstools/css-tokenizer": "^4.0.2" }, "engines": { - "node": "^18.12 || ^20.9 || >=22.0" + "node": "^22.22.3 || ^24.15 || >=26.0" }, "peerDependencies": { - "postcss": "^8.4.38" + "postcss": "^8.5.29" } }, "node_modules/postcss-colormin": { - "version": "8.0.5", - "resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-8.0.5.tgz", - "integrity": "sha512-U/N+7tkHgHoD24NB6q9k9uFcRfVJRdzVSit0fM0T2L6fvnpUoTIAYgzkyjD56LHEoCoPNfkMcfTB47SW/lNegQ==", + "version": "9.0.7", + "resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-9.0.7.tgz", + "integrity": "sha512-3pQKlwdbY9gcRFC97Nc1XgoW5e7MZI9euzzb6AZEZFejnLrzucaNSqadRZSkCCnBzXbS/1ihJ9Feawn+5FrFmw==", "license": "MIT", "dependencies": { - "@colordx/core": "^5.6.0", - "browserslist": "^4.28.8", + "@colordx/core": "^7.3.0", + "@csstools/css-tokenizer": "^4.0.2", + "browserslist": "^4.29.3", "caniuse-api": "^4.0.0", - "postcss-value-parser": "^4.2.0" + "cssnano-utils": "^8.2.1" }, "engines": { - "node": "^22.11.0 || ^24.11.0 || >=26.0" + "node": "^22.22.3 || ^24.15.0 || >=26.0" }, "peerDependencies": { - "postcss": "^8.5.26" + "postcss": "^8.5.29" } }, "node_modules/postcss-convert-values": { - "version": "8.0.4", - "resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-8.0.4.tgz", - "integrity": "sha512-ifBmAJBfpDymi7r/CqxYRJWlG+BLdVmusUC/kFPOqH/+TitdBeHA68CYqY79wXL+iQnqg6e4LGHA2Mte14X6Ig==", + "version": "9.0.8", + "resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-9.0.8.tgz", + "integrity": "sha512-KjOiKWDlE0wjJibcn+fEvhzRpaftPbrnqldLT8FiYKXDX8x8Xiap38l0gD5kQzrkgGCoHE+CJzYecwbRk07NBQ==", "license": "MIT", "dependencies": { - "browserslist": "^4.28.8", - "postcss-value-parser": "^4.2.0" + "browserslist": "^4.29.3", + "cssnano-utils": "^8.2.1" }, "engines": { - "node": "^22.11.0 || ^24.11.0 || >=26.0" + "node": "^22.22.3 || ^24.15.0 || >=26.0" }, "peerDependencies": { - "postcss": "^8.5.26" + "postcss": "^8.5.29" } }, "node_modules/postcss-discard-comments": { - "version": "8.0.4", - "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-8.0.4.tgz", - "integrity": "sha512-SU1uLYRQPKLJJmqEmqzu+Ze+9xDurBm7m/LOzXG/ANBAfAGLjbQF+oIyZnf3jV3F155q5rRMYXsoTNEJsPyHng==", + "version": "9.0.7", + "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-9.0.7.tgz", + "integrity": "sha512-+D9KEeLfphB8DYOkfZFvCkDK9A3vc/hKIQfAKtyDXo15+VKuYb0h2i+ehPWJdif3wImf3AZcN4SkXZ+IHDvqGg==", "license": "MIT", "dependencies": { - "postcss-selector-parser": "^7.1.5" + "@csstools/css-tokenizer": "^4.0.2", + "cssnano-utils": "^8.2.1" }, "engines": { - "node": "^22.11.0 || ^24.11.0 || >=26.0" + "node": "^22.22.3 || ^24.15.0 || >=26.0" }, "peerDependencies": { - "postcss": "^8.5.26" + "postcss": "^8.5.29" } }, "node_modules/postcss-discard-duplicates": { - "version": "8.0.4", - "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-8.0.4.tgz", - "integrity": "sha512-/ugMYYTE+IpHpTmwz6/S8w+blCz9pZNroUgadTdlbPnHoLLWis9QvwmoyWui2mOgFzuRM6Me7AnoGU3i0Ua2ng==", + "version": "9.0.6", + "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-9.0.6.tgz", + "integrity": "sha512-YPsUgxZ4OepCttAaixZQ4/VP2DE7vuf5936R+HFda+qgB+mzWNwUI+ghGendy3YLH5WDYS/BbPJbJAsjVd8Sfg==", "license": "MIT", + "dependencies": { + "cssnano-utils": "^8.2.1" + }, "engines": { - "node": "^22.11.0 || ^24.11.0 || >=26.0" + "node": "^22.22.3 || ^24.15.0 || >=26.0" }, "peerDependencies": { - "postcss": "^8.5.26" + "postcss": "^8.5.29" } }, "node_modules/postcss-discard-empty": { - "version": "8.0.5", - "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-8.0.5.tgz", - "integrity": "sha512-im0QtfNFa6V3SGPrfqzrWYuhzSz1ztWORKsuhZHQbWdKMh0DiNj3pmqBnABbhdKjyeW6nZ1oZYGh4dMc3biyaw==", + "version": "9.0.7", + "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-9.0.7.tgz", + "integrity": "sha512-+Hnvh0x+yyDg9X8g42m12M0MUvfTeeyjsJjOwXQ281sJVgD1naWUsF3ji+SmxUrCmeNOPtSgeL8LBlfyEXNTlA==", "license": "MIT", + "dependencies": { + "cssnano-utils": "^8.2.1" + }, "engines": { - "node": "^22.11.0 || ^24.11.0 || >=26.0" + "node": "^22.22.3 || ^24.15.0 || >=26.0" }, "peerDependencies": { - "postcss": "^8.5.26" + "postcss": "^8.5.29" } }, "node_modules/postcss-discard-overridden": { - "version": "8.0.4", - "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-8.0.4.tgz", - "integrity": "sha512-NqupxmnSSfWPJJDYbQk1qXWRwr+lw4Kyp/LSOjqwTpy/vSwo34EVXYKywgemSWpgh39P7Cs/G+tqynbRlXoJhw==", + "version": "9.0.6", + "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-9.0.6.tgz", + "integrity": "sha512-9wT4dst0GdTpi3QWh/UP6ft1UiRhqNCfCKvyB4THpYzsHdpF7r2QduDBdeNUlVsgqaQhvzs+r43z3ZVZFequzQ==", "license": "MIT", "engines": { - "node": "^22.11.0 || ^24.11.0 || >=26.0" + "node": "^22.22.3 || ^24.15.0 || >=26.0" }, "peerDependencies": { - "postcss": "^8.5.26" + "postcss": "^8.5.29" } }, "node_modules/postcss-merge-longhand": { - "version": "8.0.4", - "resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-8.0.4.tgz", - "integrity": "sha512-ammolHhMvuTz/L9YN/ge1SWbUDcD3Y9o2gXlo3S6H4MCJHheyVPwcI/LAnSuNT4gcUmSF9pNTXo7+3M/2MFPAQ==", + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-9.3.0.tgz", + "integrity": "sha512-8x6X2Gv07dQPfXNY0RNg1WZsnNLJmNk66fZ4MgpznqA3YhNygtK/Vi3ZumpR0PHRc9H50v850NMTeoSPS+FLRQ==", "license": "MIT", "dependencies": { - "postcss-value-parser": "^4.2.0", - "stylehacks": "^8.0.4" + "@csstools/css-tokenizer": "^4.0.2", + "browserslist": "^4.29.3", + "cssnano-utils": "^8.2.1", + "stylehacks": "^9.0.8" }, "engines": { - "node": "^22.11.0 || ^24.11.0 || >=26.0" + "node": "^22.22.3 || ^24.15.0 || >=26.0" }, "peerDependencies": { - "postcss": "^8.5.26" + "postcss": "^8.5.29" } }, "node_modules/postcss-merge-rules": { - "version": "8.0.5", - "resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-8.0.5.tgz", - "integrity": "sha512-jz+QaKz4VhjsqTVsJMRuH+EvrX1F9l6kYFuF8g6geVxFoYnPEZN4G+zqCb6zhxOIcc2aeAo38XEl2pOIZi2gBw==", + "version": "9.1.3", + "resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-9.1.3.tgz", + "integrity": "sha512-KKzmq/jC3h0wQo6s20AYw8FBcyCRDMMMP1SHozOZyfA/TSd+ECRtBgfel0Dami5Z3st3Fig3kZDHV8bcSrY35g==", "license": "MIT", "dependencies": { - "browserslist": "^4.28.8", + "@csstools/css-tokenizer": "^4.0.2", + "browserslist": "^4.29.3", "caniuse-api": "^4.0.0", - "cssnano-utils": "^6.0.4", - "postcss-selector-parser": "^7.1.5" + "cssnano-utils": "^8.2.1", + "postcss-minify-selectors": "^9.0.7" }, "engines": { - "node": "^22.11.0 || ^24.11.0 || >=26.0" + "node": "^22.22.3 || ^24.15.0 || >=26.0" }, "peerDependencies": { - "postcss": "^8.5.26" + "postcss": "^8.5.29" } }, "node_modules/postcss-minify-font-values": { - "version": "8.0.4", - "resolved": "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-8.0.4.tgz", - "integrity": "sha512-wZZgJ87U5WbQCEM0EOY/oM7M1a+sqW6vrOClGC9lNbsY0HYmbMFvFuukBn8PzF+ikKmaT0QB+uSAHTRLzMkD+A==", + "version": "9.0.8", + "resolved": "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-9.0.8.tgz", + "integrity": "sha512-JtJTBUC7W0WQOdmyo5UlJ2i+5ZmP8+XAlkbp+yCiC/kGI9jMgQjVGBO8Rx4sQ7tl4ygrJ0EcLBXozQmwQd+jzw==", "license": "MIT", "dependencies": { - "postcss-value-parser": "^4.2.0" + "@csstools/css-tokenizer": "^4.0.2", + "cssnano-utils": "^8.2.1" }, "engines": { - "node": "^22.11.0 || ^24.11.0 || >=26.0" + "node": "^22.22.3 || ^24.15.0 || >=26.0" }, "peerDependencies": { - "postcss": "^8.5.26" + "postcss": "^8.5.29" } }, "node_modules/postcss-minify-gradients": { - "version": "8.0.6", - "resolved": "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-8.0.6.tgz", - "integrity": "sha512-NsqlG34WSO44NOHVT6m6ZWpN80fK2l5Zt73PMDXBJKT6fO6GPnI0NBi6xIVFr/naej3ukVUlIFQGxb7JGaa9Zg==", + "version": "9.0.7", + "resolved": "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-9.0.7.tgz", + "integrity": "sha512-xkbOxiCQGNFi1Cn48Mv/OVpmomEQx8st7uu5kG7TBXUC9Mi0Sy406N6iYJkhJQP64GG13LVWB4nmxlmNs3+7Wg==", "license": "MIT", "dependencies": { - "@colordx/core": "^5.6.0", - "cssnano-utils": "^6.0.4", - "postcss-value-parser": "^4.2.0" + "@colordx/core": "^7.3.0", + "@csstools/css-tokenizer": "^4.0.2", + "cssnano-utils": "^8.2.1" }, "engines": { - "node": "^22.11.0 || ^24.11.0 || >=26.0" + "node": "^22.22.3 || ^24.15.0 || >=26.0" }, "peerDependencies": { - "postcss": "^8.5.26" + "postcss": "^8.5.29" } }, "node_modules/postcss-minify-params": { - "version": "8.0.4", - "resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-8.0.4.tgz", - "integrity": "sha512-TDx9O/ni7KazRK32pVvoo3MjExyVxWE0949vB2XZ0oHnxdAtQFHa9oG21wWkvsOL3osjiOVejwDLe6a0d0EuSw==", + "version": "9.0.7", + "resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-9.0.7.tgz", + "integrity": "sha512-jBYKPaUr6+mosG/hnVFPmbkYB7eGosw3vOuK7K1eCihIa28jQjZVL4Su50/gA3612gHpE86DctsQXOdeqxiTqQ==", "license": "MIT", "dependencies": { - "browserslist": "^4.28.8", - "cssnano-utils": "^6.0.4", - "postcss-value-parser": "^4.2.0" + "browserslist": "^4.29.3", + "cssnano-utils": "^8.2.1" }, "engines": { - "node": "^22.11.0 || ^24.11.0 || >=26.0" + "node": "^22.22.3 || ^24.15.0 || >=26.0" }, "peerDependencies": { - "postcss": "^8.5.26" + "postcss": "^8.5.29" } }, "node_modules/postcss-minify-selectors": { - "version": "8.0.5", - "resolved": "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-8.0.5.tgz", - "integrity": "sha512-i+PlhVCaPa7xevjhpActH2PrP27kDNSuuFf/ZGk8YrIcd1pd3Mfcfiv8H7dbY1/vz7U+QparioAtJaFV+IEwQg==", + "version": "9.0.7", + "resolved": "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-9.0.7.tgz", + "integrity": "sha512-2KWzOYuNbRXQEN1iSBljqmvHs4ZOtAJPop7Uq5OpwF8v1z92EtmcK3aZUJx4NKcoGwa1FQoPtMEvlvnOJQXRww==", "license": "MIT", "dependencies": { - "browserslist": "^4.28.8", + "browserslist": "^4.29.3", "caniuse-api": "^4.0.0", "cssesc": "^3.0.0", - "postcss-selector-parser": "^7.1.5" + "cssnano-utils": "^8.2.1" }, "engines": { - "node": "^22.11.0 || ^24.11.0 || >=26.0" + "node": "^22.22.3 || ^24.15.0 || >=26.0" }, "peerDependencies": { - "postcss": "^8.5.26" + "postcss": "^8.5.29" } }, "node_modules/postcss-normalize-charset": { - "version": "8.0.5", - "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-8.0.5.tgz", - "integrity": "sha512-l0L1rIe9YHbLaf4xfL2pkkQV3gq/QIy8D0bjsaHYdGVgR+eQk7IBp3dUdIKe6XuG7DzFiCwJn248nvAFAi5CjA==", + "version": "9.0.6", + "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-9.0.6.tgz", + "integrity": "sha512-H336j3ofJ5nM6TLW1nbKHj3zIMtGkycfS0KetU1cmcTtTwQ0f9CvdLVZUmBja6+IO3xuqMogjkYtB91K2slshQ==", "license": "MIT", "engines": { - "node": "^22.11.0 || ^24.11.0 || >=26.0" + "node": "^22.22.3 || ^24.15.0 || >=26.0" }, "peerDependencies": { - "postcss": "^8.5.26" + "postcss": "^8.5.29" } }, "node_modules/postcss-normalize-display-values": { - "version": "8.0.4", - "resolved": "https://registry.npmjs.org/postcss-normalize-display-values/-/postcss-normalize-display-values-8.0.4.tgz", - "integrity": "sha512-F8DGtzelJDV6S5mhcugc+EJ8sXI+kFv2PBedkfMWqd8mvTE4qyy3kva6AQwjy2+L2lZ9TGTkSumdq+n3EhGeAw==", + "version": "9.0.6", + "resolved": "https://registry.npmjs.org/postcss-normalize-display-values/-/postcss-normalize-display-values-9.0.6.tgz", + "integrity": "sha512-7NEn6H4nF+UHfjjlpr+OTJW9O9qqtcOebGM/4sHvpM5KSYOGgJcDznvwgklgeoXAyjxxkIRfKGFMB/6Hv7tH8Q==", "license": "MIT", "dependencies": { - "postcss-value-parser": "^4.2.0" + "@csstools/css-tokenizer": "^4.0.2", + "cssnano-utils": "^8.2.1" }, "engines": { - "node": "^22.11.0 || ^24.11.0 || >=26.0" + "node": "^22.22.3 || ^24.15.0 || >=26.0" }, "peerDependencies": { - "postcss": "^8.5.26" + "postcss": "^8.5.29" } }, "node_modules/postcss-normalize-positions": { - "version": "8.0.4", - "resolved": "https://registry.npmjs.org/postcss-normalize-positions/-/postcss-normalize-positions-8.0.4.tgz", - "integrity": "sha512-anOMhqe6Z0OP4jvchK1v1hIG08kO7rsp8uHrxT2+XaKp8wbvVcUO3QHUzRmQDazWUPtRzy1h2UlixyJenF42sg==", + "version": "9.0.7", + "resolved": "https://registry.npmjs.org/postcss-normalize-positions/-/postcss-normalize-positions-9.0.7.tgz", + "integrity": "sha512-6VEVY9u7fhaqGydZ3rgvDfudHsHOEX3/gCTo7Csi7hssRvxjNWPqyw1aqa99ZfAgSyzF/fEZMQv5I5jLavAdVw==", "license": "MIT", "dependencies": { - "postcss-value-parser": "^4.2.0" + "cssnano-utils": "^8.2.1" }, "engines": { - "node": "^22.11.0 || ^24.11.0 || >=26.0" + "node": "^22.22.3 || ^24.15.0 || >=26.0" }, "peerDependencies": { - "postcss": "^8.5.26" + "postcss": "^8.5.29" } }, "node_modules/postcss-normalize-repeat-style": { - "version": "8.0.4", - "resolved": "https://registry.npmjs.org/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-8.0.4.tgz", - "integrity": "sha512-vY8+k+/bFT7B8gzlHVye8FxBTMEpUAQxAgb4UYFAGGsKmXfwEvc7VPs+kpFNrTeqVKAvVh8+VVhZ+f3S4TWJPg==", + "version": "9.0.6", + "resolved": "https://registry.npmjs.org/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-9.0.6.tgz", + "integrity": "sha512-Gcc59B9JjRpDyOe394L3oZdBMycmUB0nf60awo7fSxqyYX9vRzHaBUmNMnrhXqdzjyFXBuw7sdPaAlm+UR/h5A==", "license": "MIT", "dependencies": { - "postcss-value-parser": "^4.2.0" + "cssnano-utils": "^8.2.1" }, "engines": { - "node": "^22.11.0 || ^24.11.0 || >=26.0" + "node": "^22.22.3 || ^24.15.0 || >=26.0" }, "peerDependencies": { - "postcss": "^8.5.26" + "postcss": "^8.5.29" } }, "node_modules/postcss-normalize-string": { - "version": "8.0.4", - "resolved": "https://registry.npmjs.org/postcss-normalize-string/-/postcss-normalize-string-8.0.4.tgz", - "integrity": "sha512-dWnV1frIV1XUlSYzJudceAbQ7PGyho9EgVHQXMcAoog6Nwnet3G9rz8IXcCb0EF2eefmpYG9hDT9+yWJOxc5zw==", + "version": "9.0.6", + "resolved": "https://registry.npmjs.org/postcss-normalize-string/-/postcss-normalize-string-9.0.6.tgz", + "integrity": "sha512-jhSo3E2hh9WUkJTLT6D+5hBE3f9NXgQVgG9xDcpWpAb3/mTJL5EqKWJzbvn9tHozyp2kXr5ohOaNWzSl4MClnQ==", "license": "MIT", "dependencies": { - "postcss-value-parser": "^4.2.0" + "@csstools/css-tokenizer": "^4.0.2" }, "engines": { - "node": "^22.11.0 || ^24.11.0 || >=26.0" + "node": "^22.22.3 || ^24.15.0 || >=26.0" }, "peerDependencies": { - "postcss": "^8.5.26" + "postcss": "^8.5.29" } }, "node_modules/postcss-normalize-timing-functions": { - "version": "8.0.4", - "resolved": "https://registry.npmjs.org/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-8.0.4.tgz", - "integrity": "sha512-VANJsokAWdQ03ZJwmcdEKXJjVHRtMgalB/BgbCgN3CRTdbwB5TXlSDRmdShSCxpnZs8WLAm0mPa//p5o5D/ZTg==", + "version": "9.0.6", + "resolved": "https://registry.npmjs.org/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-9.0.6.tgz", + "integrity": "sha512-kq7Mxte8lvfJXWezPtFAYJyUe4BJi4aYIBTVPlWU7FgHL+ye/RYg325JIuddaLLCYtSZToPdXvvsFR4SRKhQMg==", "license": "MIT", "dependencies": { - "postcss-value-parser": "^4.2.0" + "@csstools/css-tokenizer": "^4.0.2", + "cssnano-utils": "^8.2.1" }, "engines": { - "node": "^22.11.0 || ^24.11.0 || >=26.0" + "node": "^22.22.3 || ^24.15.0 || >=26.0" }, "peerDependencies": { - "postcss": "^8.5.26" + "postcss": "^8.5.29" } }, "node_modules/postcss-normalize-unicode": { - "version": "8.0.4", - "resolved": "https://registry.npmjs.org/postcss-normalize-unicode/-/postcss-normalize-unicode-8.0.4.tgz", - "integrity": "sha512-SxVEoFdYed0bxxwZwAXnaAZ2lzNb5jtiQvYWyMIEqnGOzCuztWpVO8LvsqpOfUIacOb6oYaWi7WOAZR36OhYqw==", + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-unicode/-/postcss-normalize-unicode-9.1.0.tgz", + "integrity": "sha512-RuEXyPVtC+hyqpdfCiIZSBO1x2/kgTDqWJX0srNLjTTiikwbdQAM1B57241o5tAu7zqpwPwct16ChJqYQiFlyQ==", "license": "MIT", "dependencies": { - "browserslist": "^4.28.8", - "postcss-value-parser": "^4.2.0" + "@csstools/css-tokenizer": "^4.0.2", + "browserslist": "^4.29.3", + "cssnano-utils": "^8.2.1" }, "engines": { - "node": "^22.11.0 || ^24.11.0 || >=26.0" + "node": "^22.22.3 || ^24.15.0 || >=26.0" }, "peerDependencies": { - "postcss": "^8.5.26" + "postcss": "^8.5.29" } }, "node_modules/postcss-normalize-url": { - "version": "8.0.4", - "resolved": "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-8.0.4.tgz", - "integrity": "sha512-JKZW8KRHkLEYkUb/55n3knDUMZeyvxlTv7ZA82bcHiCjL81b0Yyf9fIUBAYKZ6ucDgKfCDM78xhXuteVlk/o9Q==", + "version": "9.0.6", + "resolved": "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-9.0.6.tgz", + "integrity": "sha512-fQyF4hY1BRjFw9IoShVwTEwVN4q8TsVT9d6GwHgv+DpUfEfYfsNHgtkjkgEA/GMhIeSLoOyCZYusiNDvy2Qa8A==", "license": "MIT", "dependencies": { - "postcss-value-parser": "^4.2.0" + "@csstools/css-tokenizer": "^4.0.2", + "cssnano-utils": "^8.2.1" }, "engines": { - "node": "^22.11.0 || ^24.11.0 || >=26.0" + "node": "^22.22.3 || ^24.15.0 || >=26.0" }, "peerDependencies": { - "postcss": "^8.5.26" + "postcss": "^8.5.29" } }, "node_modules/postcss-normalize-whitespace": { - "version": "8.0.5", - "resolved": "https://registry.npmjs.org/postcss-normalize-whitespace/-/postcss-normalize-whitespace-8.0.5.tgz", - "integrity": "sha512-xuFovful3vVIA9fWWmAL2pG1Qr95Udj2MrLgjU50C6r+Xnh3s6t2BN1xXQ8lnCOCAtzMvMpPWYlnAh4nSv5VEQ==", + "version": "9.0.7", + "resolved": "https://registry.npmjs.org/postcss-normalize-whitespace/-/postcss-normalize-whitespace-9.0.7.tgz", + "integrity": "sha512-nwKlSuaZ4x1Bfo6ajgCDvZDSX7wZv4vnRnHiLhcKaurP6/nD1zEtt7Dm/y9GvcmPqNHH8llwW8zJC8YEFTFcRA==", "license": "MIT", "dependencies": { - "postcss-value-parser": "^4.2.0" + "@csstools/css-tokenizer": "^4.0.2", + "cssnano-utils": "^8.2.1" }, "engines": { - "node": "^22.11.0 || ^24.11.0 || >=26.0" + "node": "^22.22.3 || ^24.15.0 || >=26.0" }, "peerDependencies": { - "postcss": "^8.5.26" + "postcss": "^8.5.29" } }, "node_modules/postcss-ordered-values": { - "version": "8.0.5", - "resolved": "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-8.0.5.tgz", - "integrity": "sha512-aDL7nPR7w1foUqo4rN6QY1Kr45+SYELnmUg5taZehh7FfFdxQmUjbuOOBuffIXjiHV45zqgFHCb4azsWW19jyw==", + "version": "9.0.8", + "resolved": "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-9.0.8.tgz", + "integrity": "sha512-gj+jUyii6pnKXuJVPl6Cbb7OfOSVhuKP5m8L5Vhyx+sxLPyhxazG20e3bkmnuDrU/EEwduUHFuJ94ZDE/JIhKQ==", "license": "MIT", "dependencies": { - "cssnano-utils": "^6.0.4", - "postcss-value-parser": "^4.2.0" + "cssnano-utils": "^8.2.1" }, "engines": { - "node": "^22.11.0 || ^24.11.0 || >=26.0" + "node": "^22.22.3 || ^24.15.0 || >=26.0" }, "peerDependencies": { - "postcss": "^8.5.26" + "postcss": "^8.5.29" } }, "node_modules/postcss-reduce-initial": { - "version": "8.0.5", - "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-8.0.5.tgz", - "integrity": "sha512-Bh5d7zjH1Ai9LzJ1qULUZIaC18cniD+wA6gFlOPglMxJRC7meXbuPvpJ/zHQGg5QKwQgit++XQhDTiEIhCk8Ow==", + "version": "9.0.7", + "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-9.0.7.tgz", + "integrity": "sha512-5mnNVdVCf7tI1j2uYtiopoLZ6Dm+M7y4Fz32STAtTEZa7LbOf44zYBYduwd952xXshTJkRHpT2Q2UmK+XLo5ew==", "license": "MIT", "dependencies": { - "browserslist": "^4.28.8", + "browserslist": "^4.29.3", "caniuse-api": "^4.0.0" }, "engines": { - "node": "^22.11.0 || ^24.11.0 || >=26.0" + "node": "^22.22.3 || ^24.15.0 || >=26.0" }, "peerDependencies": { - "postcss": "^8.5.26" + "postcss": "^8.5.29" } }, "node_modules/postcss-reduce-transforms": { - "version": "8.0.4", - "resolved": "https://registry.npmjs.org/postcss-reduce-transforms/-/postcss-reduce-transforms-8.0.4.tgz", - "integrity": "sha512-ZGU7/R0GmjE3x2mOGB0g9Ohx74X+JjYX62RB5IBYLOe6R0fuHGZ76HtTY2CdM5NE8Yey/ew5clEf8iZQnjDjWA==", + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/postcss-reduce-transforms/-/postcss-reduce-transforms-9.0.5.tgz", + "integrity": "sha512-i8WpWt778MTL3j2zoqhtVLQ54NsiCsUrNxpQh/JD+U+lH+BT4olrRUEofnlf0wGzd6U/s0TWqFr2FsSVxrIzzQ==", "license": "MIT", "dependencies": { - "postcss-value-parser": "^4.2.0" + "cssnano-utils": "^8.2.1" }, "engines": { - "node": "^22.11.0 || ^24.11.0 || >=26.0" + "node": "^22.22.3 || ^24.15.0 || >=26.0" }, "peerDependencies": { - "postcss": "^8.5.26" - } - }, - "node_modules/postcss-selector-parser": { - "version": "7.1.6", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.6.tgz", - "integrity": "sha512-7qASPzhKF2l2KLboRZux8CCTRMdGiV08vWmyKzPz22qZ7ZjQBOeY7rNzNoCLSUiftJ7HUq0GERHmxw/t0dCdMw==", - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" + "postcss": "^8.5.29" } }, "node_modules/postcss-svgo": { - "version": "8.0.6", - "resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-8.0.6.tgz", - "integrity": "sha512-auQHNmduFFHzz1sWYM3sroW7IDMzxshlLjnwAALfkrAgPegHFats+jMK6BRCcdIulxPmqXFSQvgoAX4vw8I59g==", + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-9.0.5.tgz", + "integrity": "sha512-JpqUBFOnJSQ4qhbk1JvigVPnhROL6zjDCUcPWf9PZ8qwntr3dM5xUgkr9gaDNkU5kaVXyOC8fUNUCpY7x4P09A==", "license": "MIT", "dependencies": { - "postcss-value-parser": "^4.2.0", + "@csstools/css-tokenizer": "^4.0.2", + "cssnano-utils": "^8.2.1", "svgo": "^4.1.0" }, "engines": { - "node": "^22.11.0 || ^24.11.0 || >=26.0" + "node": "^22.22.3 || ^24.15.0 || >=26.0" }, "peerDependencies": { - "postcss": "^8.5.26" + "postcss": "^8.5.29" } }, "node_modules/postcss-unique-selectors": { - "version": "8.0.4", - "resolved": "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-8.0.4.tgz", - "integrity": "sha512-Sby0EtmD1mlvcZSWP5eXjxhxVgvXE5QVRzd+8NH0IbF+lYQIM57ybwAvJYYtI/fexMgCWcRnDETovjKOcLJb8w==", + "version": "9.0.6", + "resolved": "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-9.0.6.tgz", + "integrity": "sha512-mhXCmLZbqDtzM2JbtjGP36555/aob2fbOXSW8cP0SiNtssVSKB8aG7EbiAhVZKV/toJLqKJm1ayWArvzc5+huA==", "license": "MIT", "dependencies": { - "postcss-selector-parser": "^7.1.5" + "@csstools/css-tokenizer": "^4.0.2" }, "engines": { - "node": "^22.11.0 || ^24.11.0 || >=26.0" + "node": "^22.22.3 || ^24.15.0 || >=26.0" }, "peerDependencies": { - "postcss": "^8.5.26" + "postcss": "^8.5.29" } }, "node_modules/postcss-value-parser": { @@ -9558,7 +9714,6 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/srvx/-/srvx-1.0.5.tgz", "integrity": "sha512-KvSKRpgPG/oaq3cyT614OQ2bAa7DynuGamivCZeoZzIUULOdbQz6wUtnnVuC4+DCx2Zglzo8v5gBYmWIWYhDxA==", - "devOptional": true, "license": "MIT", "bin": { "srvx": "bin/srvx.mjs" @@ -9736,19 +9891,19 @@ "license": "ISC" }, "node_modules/stylehacks": { - "version": "8.0.4", - "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-8.0.4.tgz", - "integrity": "sha512-irgZeyYBFVkb8k7yTRQ9No/bTniTGqzptGVMG1/Mj3N2YnI8nIozzY1pcok4i01NfrCSqvc0PdQB6O6kr6dJHw==", + "version": "9.0.8", + "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-9.0.8.tgz", + "integrity": "sha512-PIopN/yo+/Queh2lTvJLQuXJ5yrqxWk6fQTN10daJVjnEaqkM+i+IjQdc8Jzl2fnJ8Ngq1vb57vI0hoLkKs22w==", "license": "MIT", "dependencies": { - "browserslist": "^4.28.8", - "postcss-selector-parser": "^7.1.5" + "@csstools/css-tokenizer": "^4.0.2", + "browserslist": "^4.29.3" }, "engines": { - "node": "^22.11.0 || ^24.11.0 || >=26.0" + "node": "^22.22.3 || ^24.15.0 || >=26.0" }, "peerDependencies": { - "postcss": "^8.5.26" + "postcss": "^8.5.29" } }, "node_modules/supports-color": { @@ -9914,9 +10069,9 @@ "license": "MIT" }, "node_modules/tinyclip": { - "version": "0.1.15", - "resolved": "https://registry.npmjs.org/tinyclip/-/tinyclip-0.1.15.tgz", - "integrity": "sha512-uo33abH+Ays0xYaDysoBt494Hb3hsEczMpcC0MwFl773pazORx4fmvKhclhR1wonUbB6vvpRsvVMwnhfqeMc+A==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tinyclip/-/tinyclip-1.0.3.tgz", + "integrity": "sha512-nTv2raKFMzGG6JJp0lpNDsRPdfFE6UHlu2rVEjXo8vkx1ZS3LW2CVXqIaLu8xZlu+6tbFs9V58Qc/Jmk/Ii7wA==", "license": "MIT", "engines": { "node": "^16.14.0 || >= 17.3.0" @@ -10048,6 +10203,18 @@ "integrity": "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==", "license": "MIT" }, + "node_modules/uint8array-extras": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.6.0.tgz", + "integrity": "sha512-8iAasVS4wUx0gPLjH8Xtz2PeDzTSiy8QiLGu2DT3X5GU+egFEQhpnbtkehbAwjvDcxIERmCYcysiODFmE3Ud0Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/ultrahtml": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/ultrahtml/-/ultrahtml-1.7.0.tgz", @@ -10182,12 +10349,11 @@ } }, "node_modules/unimport": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/unimport/-/unimport-6.5.0.tgz", - "integrity": "sha512-t0KLJLRbebz3f7NrQe+vy2ObeugVeM1XqI2oeYnauddmHSglYj3U6bxOJ65IbXmOFkcAGFJK2OyGH7aqHPM6Ug==", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/unimport/-/unimport-7.0.2.tgz", + "integrity": "sha512-5yrZs6kHyx5h1ZgeLxK0EgS/JL7Uhwv9HgzQImW8JccGmbYWyXx7Jc4YhU5+sd1Yk31PlfLQhfbZ1oigXipo3A==", "license": "MIT", "dependencies": { - "acorn": "^8.18.0", "escape-string-regexp": "^5.0.0", "estree-walker": "^3.0.3", "local-pkg": "^1.2.1", @@ -10206,10 +10372,14 @@ "node": ">=18.12.0" }, "peerDependencies": { + "acorn": "^8.0.0", "oxc-parser": "*", "rolldown": "^1.0.0" }, "peerDependenciesMeta": { + "acorn": { + "optional": true + }, "oxc-parser": { "optional": true }, @@ -10522,9 +10692,9 @@ "license": "MIT" }, "node_modules/verkit": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/verkit/-/verkit-0.3.2.tgz", - "integrity": "sha512-zj/ob3UsvJGN0whEAKFp53REA5X66hvffVqoCtVQAakJKnKlH+/PcOfMoFwIG/o4rElqLv/ycAFlx8ZlXUorCg==", + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/verkit/-/verkit-0.5.0.tgz", + "integrity": "sha512-u4J1hr6Y+C/hit34GwL3eIcUf3wngT8TlYUjK7tk0jxydKLAq3sYvifjdTDA6U3KCKRcQCQDGf/srjxfZF3l7g==", "license": "MIT", "engines": { "node": ">=18.12.0" @@ -11009,21 +11179,6 @@ "integrity": "sha512-RutnB7X8c5hjq39NceArgXg28WZtZpGc3+J16ljMiYnFhKvd8hITxSWQSQ5bvldxMDU6gG5mkxl1MTQLXckVSQ==", "license": "MIT" }, - "node_modules/vue-router": { - "version": "4.6.4", - "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.6.4.tgz", - "integrity": "sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==", - "license": "MIT", - "dependencies": { - "@vue/devtools-api": "^6.6.4" - }, - "funding": { - "url": "https://github.com/sponsors/posva" - }, - "peerDependencies": { - "vue": "^3.5.0" - } - }, "node_modules/vue-tsc": { "version": "2.2.12", "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-2.2.12.tgz", diff --git a/src/Delibera.WebUI/package.json b/src/Delibera.WebUI/package.json index f88beef..1ab1589 100644 --- a/src/Delibera.WebUI/package.json +++ b/src/Delibera.WebUI/package.json @@ -6,7 +6,7 @@ "description": "Web UI for the Delibera multi-model AI council framework", "license": "MIT", "engines": { - "node": ">=22" + "node": ">=22.22.3" }, "scripts": { "dev": "nuxt dev", @@ -16,13 +16,13 @@ "start": "node .output/server/index.mjs", "postinstall": "nuxt prepare", "typecheck": "nuxt typecheck", + "pretest": "nuxt build", "test": "vitest run", "test:watch": "vitest" }, "dependencies": { - "nuxt": "^4.0.0", - "vue": "^3.5.0", - "vue-router": "^4.5.0" + "nuxt": "^4.6.1", + "vue": "^3.5.43" }, "devDependencies": { "@nuxt/test-utils": "^3.19.0", diff --git a/tests/Delibera.Server.Tests/Endpoints/RequestValidationTests.cs b/tests/Delibera.Server.Tests/Endpoints/RequestValidationTests.cs index 4a3496a..5b8fb25 100644 --- a/tests/Delibera.Server.Tests/Endpoints/RequestValidationTests.cs +++ b/tests/Delibera.Server.Tests/Endpoints/RequestValidationTests.cs @@ -1,6 +1,11 @@ +using System.Collections.Concurrent; using System.Net; using System.Net.Http.Json; +using System.Reflection; using System.Text.Json; +using Delibera.Server.Api.Contracts; +using Delibera.Server.Api.Filters; +using FluentValidation; using Microsoft.AspNetCore.Mvc.Testing; namespace Delibera.Server.Tests.Endpoints; @@ -168,4 +173,43 @@ public async Task ValidationProblem_Is_Reported_As_ProblemJson() response.Content.Headers.ContentType?.MediaType.Should().Be("application/problem+json", "a validation problem has its own media type in RFC 7807"); } + + [Fact] + public async Task Validator_Type_Is_Resolved_Once_Per_Contract_Not_Per_Request() + { + // ValidationFilter used to call typeof(IValidator<>).MakeGenericType(arg.GetType()) for + // every argument of every request. Building a closed generic type walks the generic + // parameters and allocates a runtime type, so that repeated never-changing work ran on + // a per-request path. The mapping is cached now. + // + // This asserts the mapping exists and is correct, rather than comparing cache sizes: + // the cache is static and shared, and xUnit runs test classes in parallel, so any count + // taken here would also grow from unrelated endpoints landing mid-test. + using var client = Client(); + + await client.PostAsJsonAsync("/api/v1/scenarios", + new { question = "warm up", members = new[] { new { role = "Architect" } } }); + for (var i = 0; i < 5; i++) + await client.PostAsJsonAsync("/api/v1/scenarios", + new { question = $"q{i}", members = new[] { new { role = "Architect" } } }); + + var cache = ValidatorTypeCache(); + cache.Should().ContainKey(typeof(ScenarioRequest), + "the closed validator type must be cached; if this is empty the per-request " + + "MakeGenericType call is back"); + + var cached = cache[typeof(ScenarioRequest)]; + cached.GetGenericTypeDefinition().Should().Be(typeof(IValidator<>)); + cached.GetGenericArguments().Should().ContainSingle().Which.Should().Be(typeof(ScenarioRequest), + "the cached type must be closed over the argument type the request actually bound"); + } + + private static ConcurrentDictionary ValidatorTypeCache() + { + var field = typeof(ValidationFilter) + .GetField("ValidatorTypes", BindingFlags.NonPublic | BindingFlags.Static); + field.Should().NotBeNull("ValidationFilter must keep the validator-type cache this test " + + "asserts on; renaming it is fine, deleting the cache is not"); + return (ConcurrentDictionary)field!.GetValue(null)!; + } } diff --git a/tests/Delibera.Server.Tests/Sse/SseHeartbeatTests.cs b/tests/Delibera.Server.Tests/Sse/SseHeartbeatTests.cs index 39f77c4..c2d617e 100644 --- a/tests/Delibera.Server.Tests/Sse/SseHeartbeatTests.cs +++ b/tests/Delibera.Server.Tests/Sse/SseHeartbeatTests.cs @@ -131,6 +131,95 @@ public async Task Cancelled_Client_Does_Not_Produce_A_Second_Terminal_Event() "the client is gone; writing another event would only throw"); } + /// + /// A buffering proxy once passed every other test in this file while delivering each + /// round 150–200 seconds late. The other tests here cannot see that failure: they hand + /// the writer a and read everything back after the debate is + /// over, so a frame held in a buffer and a frame written promptly look identical. + /// + /// This one watches the wire instead. The terminal event is withheld, so the debate + /// cannot finish on its own; the round must arrive anyway, within the timeout. If it + /// only shows up when the writer returns, the frame was sitting in a buffer — which is + /// precisely the defect being guarded against. + /// + /// + [Fact] + public async Task Round_Is_Flushed_While_The_Debate_Is_Still_Running() + { + var orchestrator = ChannelOrchestrator.Open(); + orchestrator.Publish("d1", new DebateRoundEvent.RoundCompleted("d1", Round(1))); + + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); + var ctx = new DefaultHttpContext(); + var wire = new SignalStream(); + ctx.Response.Body = wire; + + // This never returns on its own: the orchestrator channel is left open and no terminal + // event is published, so the write task is still running when the round must arrive. + var write = SseDebateStreamWriter.WriteAsync(Record(), orchestrator, ctx, cts.Token, FastHeartbeat); + + var arrived = await Task.WhenAny(wire.SawRound, Task.Delay(TimeSpan.FromSeconds(3))); + + arrived.Should().BeSameAs(wire.SawRound, + "a round that only reaches the client when the stream ends is indistinguishable from " + + "buffering, which is how this failure shipped in the first place"); + + await cts.CancelAsync(); + await write; + } + + /// + /// Write-only stream that completes a task the first time a round frame lands on it. + /// + private sealed class SignalStream : Stream + { + private readonly TaskCompletionSource _sawRound = + new(TaskCreationOptions.RunContinuationsAsynchronously); + private readonly StringBuilder _received = new(); + + public Task SawRound => _sawRound.Task; + + public override bool CanRead => false; + public override bool CanSeek => false; + public override bool CanWrite => true; + public override long Length => 0; + public override long Position { get => 0; set => throw new NotSupportedException(); } + + public override void Write(byte[] buffer, int offset, int count) => + Observe(buffer.AsSpan(offset, count)); + + public override void Write(ReadOnlySpan buffer) => Observe(buffer); + + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken ct) + { + Observe(buffer.AsSpan(offset, count)); + return Task.CompletedTask; + } + + public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken ct = default) + { + Observe(buffer.Span); + return ValueTask.CompletedTask; + } + + private void Observe(ReadOnlySpan chunk) + { + var text = Encoding.UTF8.GetString(chunk); + lock (_received) + { + _received.Append(text); + if (text.Contains("event: debate-round", StringComparison.Ordinal)) + _sawRound.TrySetResult(); + } + } + + public override void Flush() { } + public override Task FlushAsync(CancellationToken ct) => Task.CompletedTask; + public override int Read(byte[] buffer, int offset, int count) => throw new NotSupportedException(); + public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); + public override void SetLength(long value) => throw new NotSupportedException(); + } + /// /// Channel-backed orchestrator. A real channel is used rather than a hand-rolled /// iterator so the test exercises the same shape the production stream has. From 644d26270a6cadc6329cf111ee66267cd6e1ec52 Mon Sep 17 00:00:00 2001 From: Viktor Buzin Date: Sat, 10 Oct 2026 21:49:44 +0300 Subject: [PATCH 04/13] webui: a typecheck that actually checks, and a proxy that releases streams The typecheck has been reporting green while checking less than it should. `npm run typecheck` printed "[Vue] Load plugin failed: vue-router/volar/sfc-route-blocks" on every run and still exited 0, so CI saw a pass. The plugin comes from vue-router, and package.json pinned vue-router@^4 - a different major from the one nuxt depends on, for a package no source file imports. Two defects sat behind that one message: vue-tsc@2 cannot load a vue-router@5 plugin at all, and the plugin also has to resolve from the project root, which it does not while it only exists nested under node_modules/nuxt. vue-router is now pinned to the major nuxt actually wants, and the toolchain moved with it: nuxt 4.6.1, vue-tsc 3.3, vitest 5, @nuxt/test-utils 4, happy-dom 20. The check is clean for the first time. Node is pinned to 22.23.3 - .nvmrc, plus one ARG across the Dockerfile's three stages - because nuxt@4.6.1 declares an exact engines range, ^22.22.3 || ^24.15.0 || >=26.0.0, that the floating node:22-alpine tag only satisfied by luck. The proxy runs every upstream call with timeout: 0, so nothing else ever ended those requests: closing the tab or navigating away left the server-side SSE connection running for the rest of the debate, one leaked stream per piece of user navigation. It now aborts its fetch when the response closes before it was written, and swallows the resulting read error, because a client hanging up is not an upstream fault worth reporting to nobody. Measured on the fixture, the upstream socket closes 6ms after the client goes away with the fix and 2699ms without it - the second being the moment the fake upstream ends its own response, which is exactly the point: a connection the proxy never cancelled closes anyway. The regression test therefore asserts the timing, not the count, since the count passes either way. Three quieter fixes. Cancel reported neither of its two outcomes, because both branches were the same line. Changing a filter did not re-query, leaving the previous results under controls claiming the new selection. And npm test failed on a clean checkout, because the SSE suite needs the built server that only CI happened to build first. Live rounds now reach a screen reader through a polite live region, each route titles itself, and buttons, inputs and rows gained hover states and a visible focus ring - the browser default being near-invisible against these dark surfaces. The server route stays on h3 rather than the new nuxt/server surface: that one omits the response-writing helpers this proxy is built on, and Nuxt's RequestEvent is not assignable to h3's H3Event. The reasoning is recorded at the import so it is not re-attempted. Co-Authored-By: Claude Opus 4.8 (1M context) --- CHANGELOG.md | 68 + src/Delibera.WebUI/.nvmrc | 2 +- src/Delibera.WebUI/Dockerfile | 28 +- src/Delibera.WebUI/app/app.vue | 7 + .../app/components/RoundTimeline.vue | 37 +- src/Delibera.WebUI/app/error.vue | 28 +- src/Delibera.WebUI/app/pages/debates/[id].vue | 56 +- src/Delibera.WebUI/app/pages/debates/new.vue | 22 + src/Delibera.WebUI/app/pages/index.vue | 37 +- src/Delibera.WebUI/package-lock.json | 1876 +++++------------ src/Delibera.WebUI/package.json | 15 +- .../server/api/delibera/[...path].ts | 46 +- src/Delibera.WebUI/server/api/health.get.ts | 3 + src/Delibera.WebUI/server/utils/delibera.ts | 2 - src/Delibera.WebUI/test/sse-streaming.test.ts | 80 + 15 files changed, 885 insertions(+), 1422 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5b42ec1..1935024 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -131,6 +131,74 @@ pipeline over HTTP — that a default host emits no `Access-Control-Allow-Origin configured origin gets its own origin echoed back rather than a wildcard, that an unlisted origin gets nothing, that the preflight is answered, and that `X-Correlation-Id` is exposed. +### Fixed — the Web UI typecheck was passing while silently checking less + +`npm run typecheck` printed `[Vue] Load plugin failed: vue-router/volar/sfc-route-blocks` on every +run and exited **0**, so CI reported green. The plugin Nuxt writes into the generated tsconfig +comes from `vue-router`, and the app pinned `vue-router@^4` — a different major from the one +`nuxt` itself depends on, for a package no source file imports. Two defects sat behind the one +message: `vue-tsc@2` cannot load a `vue-router@5` plugin at all, and once that is upgraded the +plugin must also be resolvable from the project root, which it is not while it only exists +nested under `node_modules/nuxt`. Nuxt is now `4.6.1`, `vue-router@^5.4.0` matching it, +`vue-tsc@3.3`, `vitest@5`, `@nuxt/test-utils@4`, and the check is clean for the first time. + +### Changed — Node is pinned to an exact patch, because the requirement is an exact range + +`nuxt@4.6.1` declares `engines.node: ^22.22.3 || ^24.15.0 || >=26.0.0`, so the floating +`node:22-alpine` and `.nvmrc: 22` that worked by accident stop working the moment the tag moves +against us. Both are now `22.23.3`, the Dockerfile resolving one `ARG NODE_VERSION` across its +three stages. Node 24 ("Krypton") satisfies the range too and is the natural move before 22 +("Jod") leaves maintenance in April 2027 — but that also moves the bundled npm from 10.x to +11.x, which deserves its own change. + +### Fixed — the proxy kept upstream SSE connections open after the browser left + +Every upstream call runs with `timeout: 0`, because a debate legitimately takes 150–200 s and its +stream stays open longer still, so nothing else ever ended those requests. Closing the tab, +navigating away or letting `EventSource` reconnect left the server-side connection running for +the remainder of the debate — one leaked stream per piece of user navigation, invisible from a UI +that had simply gone. The proxy now aborts its own fetch when the response closes before it was +fully written, and the resulting read error is swallowed rather than thrown: a client hanging up +is not an upstream fault worth reporting to nobody. + +Measured on the test fixture, the upstream socket closes **6 ms** after the client disconnects +with the fix and **2699 ms** without it — the second being the moment the fake upstream ends its +own response, which is exactly the point: a connection the proxy never cancelled closes anyway. +The regression test asserts the timing, not the count, because the count passes either way. + +### Fixed — three Web UI behaviours that were silently wrong + +- **Cancel reported itself without saying anything.** Both branches of `onCancel` called + `loadRecord()`, so "cancelled" and "had already finished" produced identical silence and an + unexplained status change. They now say which one happened. +- **Changing a filter did not re-query.** The selects only changed what was *selected*; the list + kept rendering the previous filter's results under controls claiming the new one, and paging + was never reset. A filter change now reloads from page 1. +- **`npm test` failed on a clean checkout.** The SSE suite runs the built Nitro server, and CI + only passed because its `build` step happens to run first. `pretest` now builds. + +### Added — the live view announces itself, and the pages identify themselves + +Rounds arriving over SSE were invisible to a screen reader: the count in the heading changed +silently, and the reader was most likely on the verdict panel. A polite live region now announces +each round — polite, because rounds arrive as a steady drip and interrupting whatever is being +read for each one would make the page unusable rather than live. Every tab was titled "Delibera — +AI council"; each route now titles itself, and a debate names itself by its label once loaded. +Buttons, inputs and rows gained hover states and a visible `:focus-visible` ring, the browser +default being near-invisible against these dark surfaces, and the error page now shows the status +message and clears the error through `clearError` instead of relying on a link that renders +without the router state it needs. + +### Changed — the server route stays on `h3`, and says why + +Nuxt 4.6 added `nuxt/server`, a server-agnostic surface that re-declares the request helpers +against Nuxt's own `RequestEvent` and omits the response-writing helpers, so a non-Nitro builder +can satisfy it. This route is exactly the case it cannot serve: forwarding an upstream body +verbatim is `sendWebResponse`, streaming SSE chunk by chunk is `sendStream`, and Nuxt's +`RequestEvent` is not assignable to h3's `H3Event` — mixing the surfaces fails typecheck, not at +runtime. The imports stay on `h3` with the reasoning recorded at the import, so the next person +does not spend the same afternoon on it. An unused `H3Event` type import went with it. + ## [10.5.2] - 2026-10-09 **Contains behaviour changes.** Two are breaking for existing deployments: published ports now diff --git a/src/Delibera.WebUI/.nvmrc b/src/Delibera.WebUI/.nvmrc index 8fdd954..22a0c16 100644 --- a/src/Delibera.WebUI/.nvmrc +++ b/src/Delibera.WebUI/.nvmrc @@ -1 +1 @@ -22 \ No newline at end of file +22.23.3 \ No newline at end of file diff --git a/src/Delibera.WebUI/Dockerfile b/src/Delibera.WebUI/Dockerfile index f36d1e8..29588c0 100644 --- a/src/Delibera.WebUI/Dockerfile +++ b/src/Delibera.WebUI/Dockerfile @@ -7,15 +7,27 @@ # dependency layer is cached on the lockfile alone. # ──────────────────────────────────────────────────────────────────────────────── +# Node is pinned to an exact patch, and the pin is load-bearing rather than cosmetic: +# nuxt@4.6.1 declares `engines.node: ^22.22.3 || ^24.15.0 || >=26.0.0`, so a floating +# `node:22-alpine` tag only works by luck — the tag tracks the newest 22.x, and the build +# would start failing the moment 22.22.3 were pulled or a lower tag were cached. +# +# Keep in step with src/Delibera.WebUI/.nvmrc and the `engines.node` field in package.json. +# Node 24 ("Krypton", the current LTS) satisfies the range too and is the natural next move +# before 22 ("Jod") leaves maintenance in April 2027 — but moving to it also moves the +# bundled npm from 10.x to 11.x, which is a separate change to make on purpose. +ARG NODE_VERSION=22.23.3 + # ── Stage 1: dependencies ───────────────────────────────────────────────────── # Keyed on package.json + lockfile only, so a source edit does not re-run npm ci. -FROM node:22-alpine AS deps +FROM node:${NODE_VERSION}-alpine AS deps WORKDIR /app -# npm is pinned deliberately. node:22-alpine ships npm 12.x while a developer workstation -# commonly has 10.x, and the two resolve some transitive dependencies differently — a lockfile -# written by npm 10 fails `npm ci` under npm 12 with EUSAGE ("lock file's cac@7.0.1 does not -# satisfy cac@6.7.14"). Pinning the installer is what keeps the image build reproducible. +# npm is pinned deliberately. The base image ships npm 10.9.9 while a developer workstation +# commonly has a different major, and the two resolve some transitive dependencies +# differently — a lockfile written by npm 10 fails `npm ci` under npm 12 with EUSAGE +# ("lock file's cac@7.0.1 does not satisfy cac@6.7.14"). Pinning the installer is what keeps +# the image build reproducible even if the base tag is ever bumped. # # Bump this together with the .nvmrc / local npm, never on its own. RUN npm install -g npm@10.9.4 @@ -26,7 +38,7 @@ COPY package.json package-lock.json ./ RUN npm ci --no-audit --no-fund # ── Stage 2: build ──────────────────────────────────────────────────────────── -FROM node:22-alpine AS build +FROM node:${NODE_VERSION}-alpine AS build WORKDIR /app ENV NODE_ENV=production @@ -39,7 +51,7 @@ COPY . . RUN npm run build # ── Stage 3: runtime ────────────────────────────────────────────────────────── -FROM node:22-alpine AS runtime +FROM node:${NODE_VERSION}-alpine AS runtime WORKDIR /app ENV NODE_ENV=production @@ -47,7 +59,7 @@ ENV NITRO_HOST=0.0.0.0 ENV NITRO_PORT=3000 ENV NPM_CONFIG_UPDATE_NOTIFIER=false -# node:22-alpine already ships an unprivileged `node` user (uid/gid 1000). Unlike the +# The node image already ships an unprivileged `node` user (uid/gid 1000). Unlike the # Debian-based .NET image, alpine has adduser but the base user is already correct — using # it avoids creating a second account with the same uid. USER node diff --git a/src/Delibera.WebUI/app/app.vue b/src/Delibera.WebUI/app/app.vue index 2e0564a..76466bc 100644 --- a/src/Delibera.WebUI/app/app.vue +++ b/src/Delibera.WebUI/app/app.vue @@ -116,6 +116,13 @@ const healthTitle = computed(() => color: #fff; } +/* The nav is the only always-visible route control in the app, so a keyboard user crossing + the page meets it first. The browser default outline is near-invisible on #151922. */ +.app__nav a:focus-visible { + outline: 2px solid #7fb0ff; + outline-offset: 2px; +} + .app__health { font-size: 0.72rem; padding: 0.2rem 0.55rem; diff --git a/src/Delibera.WebUI/app/components/RoundTimeline.vue b/src/Delibera.WebUI/app/components/RoundTimeline.vue index c7e4697..226863c 100644 --- a/src/Delibera.WebUI/app/components/RoundTimeline.vue +++ b/src/Delibera.WebUI/app/components/RoundTimeline.vue @@ -2,6 +2,17 @@

Rounds ({{ rounds.length }})

+ +

{{ liveMessage }}

+

No rounds yet. A debate typically takes 150–200 seconds to complete.

@@ -46,10 +57,34 @@ \ No newline at end of file diff --git a/src/Delibera.WebUI/app/pages/debates/[id].vue b/src/Delibera.WebUI/app/pages/debates/[id].vue index 99deb52..ff725f8 100644 --- a/src/Delibera.WebUI/app/pages/debates/[id].vue +++ b/src/Delibera.WebUI/app/pages/debates/[id].vue @@ -38,6 +38,11 @@

{{ streamError }}

+ +

{{ notice }}

+ @@ -63,12 +68,32 @@ const debateId = computed(() => String(route.params.id ?? '')) const record = ref(null) const loadError = ref(null) +/** Outcome of the last action the user took, when it succeeded but is worth spelling out. */ +const notice = ref(null) const cancelling = ref(false) const { rounds, errorMessage: streamError, connected, polling, start } = useDebateStream() const exportHref = computed(() => exportUrl(debateId.value)) +// The debate's own label is the most specific thing this page can be called, and it only +// arrives after the first render — so the title fills in rather than committing to the +// opaque id up front. Long labels are cut: a tab strip is not a place for a full question. +const TITLE_MAX = 60 + +useHead({ + title: computed(() => { + const current = record.value + const specific = current?.label?.trim() || current?.templateId?.trim() + if (specific) { + const shortened = + specific.length > TITLE_MAX ? `${specific.slice(0, TITLE_MAX - 1)}…` : specific + return `${shortened} — Delibera` + } + return `Debate ${debateId.value.slice(0, 8)} — Delibera` + }), +}) + /** * Backfills rounds from the paginated endpoint when the timeline is still empty. * @@ -119,15 +144,18 @@ async function onStreamFinished(): Promise { async function onCancel(): Promise { cancelling.value = true + notice.value = null try { const outcome = await cancel(debateId.value) - // "already finished" is a success from the caller's point of view, not an error — the - // server answers 404 for a debate that has already reached a terminal state. - if (outcome === 'already-finished') { - await loadRecord() - } else { - await loadRecord() - } + // Both outcomes mean "this debate will not run any longer", but they are not the same + // event and the user pressed a button called Cancel: telling them a debate ended a + // second earlier is different information from confirming their own cancellation, and + // silently reloading the record conflates the two into an unexplained status change. + notice.value = + outcome === 'already-finished' + ? 'This debate had already reached a final state, so there was nothing to cancel.' + : 'Cancellation requested. The debate stops after the round in flight.' + await loadRecord() } catch (error) { loadError.value = error as ParsedApiError } finally { @@ -201,6 +229,20 @@ onMounted(async () => { cursor: not-allowed; } +/* See the note in app/pages/index.vue. */ +.btn:hover:not(:disabled) { + border-color: #4a5a7a; +} + +.btn--danger:hover:not(:disabled) { + border-color: #8c3a3a; +} + +.btn:focus-visible { + outline: 2px solid #7fb0ff; + outline-offset: 2px; +} + .alert { padding: 0.8rem 1rem; border-radius: 8px; diff --git a/src/Delibera.WebUI/app/pages/debates/new.vue b/src/Delibera.WebUI/app/pages/debates/new.vue index 3192eb9..577ae96 100644 --- a/src/Delibera.WebUI/app/pages/debates/new.vue +++ b/src/Delibera.WebUI/app/pages/debates/new.vue @@ -98,6 +98,8 @@ import type { ParsedApiError } from '~/composables/useApiError' const { create, templates: fetchTemplates } = useDebates() const router = useRouter() +useHead({ title: 'New debate — Delibera' }) + const templates = ref([]) const submitting = ref(false) const clientError = ref(null) @@ -262,6 +264,26 @@ onMounted(async () => { cursor: not-allowed; } +/* See the note in app/pages/index.vue: pointer hover without leaving a ring behind, keyboard + focus that is actually visible against these dark surfaces. */ +.btn:hover:not(:disabled) { + border-color: #4a5a7a; +} + +.btn--primary:hover:not(:disabled) { + border-color: #5a8fe0; +} + +.input:hover { + border-color: #3a445c; +} + +.input:focus-visible, +.btn:focus-visible { + outline: 2px solid #7fb0ff; + outline-offset: 2px; +} + .alert { padding: 0.8rem 1rem; border-radius: 8px; diff --git a/src/Delibera.WebUI/app/pages/index.vue b/src/Delibera.WebUI/app/pages/index.vue index 6b444f7..18e97e3 100644 --- a/src/Delibera.WebUI/app/pages/index.vue +++ b/src/Delibera.WebUI/app/pages/index.vue @@ -31,7 +31,7 @@ No debates yet. Create one from a registered template to get started.

-
    +
    • @@ -69,6 +69,10 @@ import { describeStatus, type ParsedApiError } from '~/composables/useApiError' const { list, templates: fetchTemplates } = useDebates() +// Without this the tab reads "Delibera — AI council" on every screen, which identifies the +// product but not the page — and with three routes open there is nothing to tell them apart. +useHead({ title: 'Debates — Delibera' }) + const debates = ref([]) const templates = ref([]) const loading = ref(false) @@ -107,6 +111,17 @@ async function refresh(): Promise { await load() } +// Changing a filter re-queries on the spot. +// +// Without this the selects only changed what was *selected*: the list kept rendering the +// previous filter's results while the controls above it claimed the new one, and the only +// way to reconcile the two was to guess that Refresh was now the missing step. Paging is +// reset as well, because page 3 of a filter the user has just replaced is a page that has +// no meaning under the new selection. +watch([templateId, status], () => { + void refresh() +}) + function goTo(next: number): void { page.value = next void load() @@ -174,6 +189,26 @@ onMounted(async () => { cursor: not-allowed; } +/* Hover and keyboard focus. + `:focus-visible` rather than `:focus`, so a pointer click does not leave a ring behind on + every row while keyboard navigation still shows exactly where the reader is. The app has + no :focus styling at all elsewhere, and the browser default is a thin outline that all but + disappears against these dark surfaces. */ +.btn:hover:not(:disabled) { + border-color: #4a5a7a; +} + +.input:hover { + border-color: #3a445c; +} + +.input:focus-visible, +.btn:focus-visible, +.row:focus-visible { + outline: 2px solid #7fb0ff; + outline-offset: 2px; +} + .list { list-style: none; margin: 0; diff --git a/src/Delibera.WebUI/package-lock.json b/src/Delibera.WebUI/package-lock.json index ad5f3d0..576c859 100644 --- a/src/Delibera.WebUI/package-lock.json +++ b/src/Delibera.WebUI/package-lock.json @@ -11,15 +11,16 @@ "license": "MIT", "dependencies": { "nuxt": "^4.6.1", - "vue": "^3.5.43" + "vue": "^3.5.43", + "vue-router": "^5.4.0" }, "devDependencies": { - "@nuxt/test-utils": "^3.19.0", + "@nuxt/test-utils": "^4.3.3", "@vue/test-utils": "^2.4.6", - "happy-dom": "^17.0.0", - "typescript": "^5.7.0", - "vitest": "^3.2.0", - "vue-tsc": "^2.2.0" + "happy-dom": "^20.14.6", + "typescript": "^5.9.3", + "vitest": "^5.0.3", + "vue-tsc": "^3.3.13" }, "engines": { "node": ">=22.22.3" @@ -429,26 +430,31 @@ } }, "node_modules/@clack/core": { - "version": "1.0.0-alpha.7", - "resolved": "https://registry.npmjs.org/@clack/core/-/core-1.0.0-alpha.7.tgz", - "integrity": "sha512-3vdh6Ar09D14rVxJZIm3VQJkU+ZOKKT5I5cC0cOVazy70CNyYYjiwRj9unwalhESndgxx6bGc/m6Hhs4EKF5XQ==", - "dev": true, + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@clack/core/-/core-1.5.1.tgz", + "integrity": "sha512-iHTrHA8MtVuLl2TfZySmcKv1qO2PoyC9Z7pfSDozEuV5vtY3/wcOPKJXlqJ5Oq2Cx5DDGQGAMVx6HZfRRoVEbQ==", "license": "MIT", "dependencies": { - "picocolors": "^1.0.0", + "fast-wrap-ansi": "^0.2.0", "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 20.12.0" } }, "node_modules/@clack/prompts": { - "version": "1.0.0-alpha.9", - "resolved": "https://registry.npmjs.org/@clack/prompts/-/prompts-1.0.0-alpha.9.tgz", - "integrity": "sha512-sKs0UjiHFWvry4SiRfBi5Qnj0C/6AYx8aKkFPZQSuUZXgAram25ZDmhQmP7vj1aFyLpfHWtLQjWvOvcat0TOLg==", - "dev": true, + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@clack/prompts/-/prompts-1.8.1.tgz", + "integrity": "sha512-dlT1m5e/0yUL0kRNcQn7yGLVThkgbB0Ga/1AmfDDC/8ik6AIiSf2QLQO2zPYvefsHP0aFgxO93cVLCCfDp7kzQ==", "license": "MIT", "dependencies": { - "@clack/core": "1.0.0-alpha.7", - "picocolors": "^1.0.0", + "@clack/core": "1.5.1", + "fast-string-width": "^3.0.2", + "fast-wrap-ansi": "^0.2.0", "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 20.12.0" } }, "node_modules/@cloudflare/kv-asset-handler": { @@ -502,77 +508,6 @@ "unplugin": "^3.3.0" } }, - "node_modules/@dxup/nuxt/node_modules/@nuxt/kit": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/@nuxt/kit/-/kit-4.6.0.tgz", - "integrity": "sha512-UXTajJ+MFJgwV9UL1CTNNd2PSw5PC3Pjb2rM7FsSYcr92ySQKXEslBnUKbvnXT+gG3NbZ/EF70+WenoNIHX2tw==", - "license": "MIT", - "dependencies": { - "consola": "^3.4.2", - "defu": "^6.1.7", - "destr": "^2.0.5", - "errx": "^0.2.2", - "exsolve": "^1.1.3", - "ignore": "^7.0.12", - "klona": "^2.0.6", - "microdiff": "^1.6.0", - "nostics": "^1.3.0", - "package-manager-detector": "^1.8.0", - "pathe": "^2.0.3", - "rc9": "^3.1.0", - "scule": "^1.3.0", - "std-env": "^4.3.0", - "tinyexec": "^1.3.1", - "tinyglobby": "^0.2.17", - "unctx": "^3.0.1", - "verkit": "^0.5.0" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "peerDependencies": { - "@nuxt/schema": "^3.0.0 || ^4.0.0 || ^5.0.0-0", - "confbox": ">=0.1.0", - "dotenv": "*", - "giget": ">=3.1.0", - "jiti": "^2.0.0", - "mlly": "^1.2.0", - "oxc-parser": ">=0.56.0", - "rolldown": ">=1.0.0" - }, - "peerDependenciesMeta": { - "@nuxt/schema": { - "optional": true - }, - "confbox": { - "optional": true - }, - "dotenv": { - "optional": true - }, - "giget": { - "optional": true - }, - "jiti": { - "optional": true - }, - "mlly": { - "optional": true - }, - "oxc-parser": { - "optional": true - }, - "rolldown": { - "optional": true - } - } - }, - "node_modules/@dxup/nuxt/node_modules/errx": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/errx/-/errx-0.2.2.tgz", - "integrity": "sha512-HaPN6cxm6iZ9bIbQQ5orChcw7SY5smUzM08ZMc6OtV4UQhrrc3Ms1G8Ww2n/9lXkWqiioNTssF8bCvchrSbFEQ==", - "license": "MIT" - }, "node_modules/@dxup/nuxt/node_modules/magic-string": { "version": "1.4.3", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.4.3.tgz", @@ -582,38 +517,6 @@ "@jridgewell/sourcemap-codec": "^1.6.0" } }, - "node_modules/@dxup/nuxt/node_modules/std-env": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.3.0.tgz", - "integrity": "sha512-OtU/EgQ1kIm5KwqQpBC6ZEMXrZRui11w8zgfTWp8cdO9B8OaPsbA8bTHO2P+HNo1VlUTGMVBwPhydu6poeXiag==", - "license": "MIT" - }, - "node_modules/@dxup/nuxt/node_modules/unctx": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/unctx/-/unctx-3.0.1.tgz", - "integrity": "sha512-5RAt2etv7g362RXyd33R82gm9u/kbtQlpoaOs9Bgm4E32GRMJ0xjbZyvdxUTmiuHk3V1IQb1aUNrp5IWY+JaWw==", - "license": "MIT", - "peerDependencies": { - "magic-string": ">=0.30.21", - "oxc-parser": ">=0.140.0", - "rolldown": "^1.1.5", - "unplugin": "^3.3.0" - }, - "peerDependenciesMeta": { - "magic-string": { - "optional": true - }, - "oxc-parser": { - "optional": true - }, - "rolldown": { - "optional": true - }, - "unplugin": { - "optional": true - } - } - }, "node_modules/@dxup/nuxt/node_modules/unplugin": { "version": "3.4.0", "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-3.4.0.tgz", @@ -1403,34 +1306,6 @@ } } }, - "node_modules/@nuxt/cli/node_modules/@clack/core": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/@clack/core/-/core-1.5.1.tgz", - "integrity": "sha512-iHTrHA8MtVuLl2TfZySmcKv1qO2PoyC9Z7pfSDozEuV5vtY3/wcOPKJXlqJ5Oq2Cx5DDGQGAMVx6HZfRRoVEbQ==", - "license": "MIT", - "dependencies": { - "fast-wrap-ansi": "^0.2.0", - "sisteransi": "^1.0.5" - }, - "engines": { - "node": ">= 20.12.0" - } - }, - "node_modules/@nuxt/cli/node_modules/@clack/prompts": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/@clack/prompts/-/prompts-1.8.1.tgz", - "integrity": "sha512-dlT1m5e/0yUL0kRNcQn7yGLVThkgbB0Ga/1AmfDDC/8ik6AIiSf2QLQO2zPYvefsHP0aFgxO93cVLCCfDp7kzQ==", - "license": "MIT", - "dependencies": { - "@clack/core": "1.5.1", - "fast-string-width": "^3.0.2", - "fast-wrap-ansi": "^0.2.0", - "sisteransi": "^1.0.5" - }, - "engines": { - "node": ">= 20.12.0" - } - }, "node_modules/@nuxt/cli/node_modules/confbox": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.3.1.tgz", @@ -1450,12 +1325,6 @@ "node": ">=12.20.0" } }, - "node_modules/@nuxt/cli/node_modules/std-env": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.3.0.tgz", - "integrity": "sha512-OtU/EgQ1kIm5KwqQpBC6ZEMXrZRui11w8zgfTWp8cdO9B8OaPsbA8bTHO2P+HNo1VlUTGMVBwPhydu6poeXiag==", - "license": "MIT" - }, "node_modules/@nuxt/devalue": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/@nuxt/devalue/-/devalue-2.0.2.tgz", @@ -1528,10 +1397,29 @@ "vite": ">=6.0" } }, - "node_modules/@nuxt/devtools-kit/node_modules/@nuxt/kit": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/@nuxt/kit/-/kit-4.6.0.tgz", - "integrity": "sha512-UXTajJ+MFJgwV9UL1CTNNd2PSw5PC3Pjb2rM7FsSYcr92ySQKXEslBnUKbvnXT+gG3NbZ/EF70+WenoNIHX2tw==", + "node_modules/@nuxt/devtools-wizard": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/@nuxt/devtools-wizard/-/devtools-wizard-3.4.2.tgz", + "integrity": "sha512-U2G8fw7KW1rdECyJDho5N14z8hSllpTk430KG1QPLjrv5w3pDksZ+YcpITd0u762Vw3gq3Arpg65Bk6U4ua6aQ==", + "license": "MIT", + "dependencies": { + "@clack/prompts": "^1.7.0", + "consola": "^3.4.2", + "diff": "^8.0.4", + "execa": "^8.0.1", + "magicast": "^0.5.4", + "pathe": "^2.0.3", + "pkg-types": "^2.3.1", + "semver": "^7.8.5" + }, + "bin": { + "devtools-wizard": "cli.mjs" + } + }, + "node_modules/@nuxt/kit": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/@nuxt/kit/-/kit-4.6.1.tgz", + "integrity": "sha512-ld5+/o4EMqyw6Qy+SdEYkezfprS87+p+xOBhnQgXwyrg1EdtILfLeudQ+hX6UyCnsoF6o0SOw3pjN1P4Tg9O0w==", "license": "MIT", "dependencies": { "consola": "^3.4.2", @@ -1543,7 +1431,7 @@ "klona": "^2.0.6", "microdiff": "^1.6.0", "nostics": "^1.3.0", - "package-manager-detector": "^1.8.0", + "package-manager-detector": "^1.9.0", "pathe": "^2.0.3", "rc9": "^3.1.0", "scule": "^1.3.0", @@ -1593,19 +1481,7 @@ } } }, - "node_modules/@nuxt/devtools-kit/node_modules/errx": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/errx/-/errx-0.2.2.tgz", - "integrity": "sha512-HaPN6cxm6iZ9bIbQQ5orChcw7SY5smUzM08ZMc6OtV4UQhrrc3Ms1G8Ww2n/9lXkWqiioNTssF8bCvchrSbFEQ==", - "license": "MIT" - }, - "node_modules/@nuxt/devtools-kit/node_modules/std-env": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.3.0.tgz", - "integrity": "sha512-OtU/EgQ1kIm5KwqQpBC6ZEMXrZRui11w8zgfTWp8cdO9B8OaPsbA8bTHO2P+HNo1VlUTGMVBwPhydu6poeXiag==", - "license": "MIT" - }, - "node_modules/@nuxt/devtools-kit/node_modules/unctx": { + "node_modules/@nuxt/kit/node_modules/unctx": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/unctx/-/unctx-3.0.1.tgz", "integrity": "sha512-5RAt2etv7g362RXyd33R82gm9u/kbtQlpoaOs9Bgm4E32GRMJ0xjbZyvdxUTmiuHk3V1IQb1aUNrp5IWY+JaWw==", @@ -1631,216 +1507,34 @@ } } }, - "node_modules/@nuxt/devtools-wizard": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/@nuxt/devtools-wizard/-/devtools-wizard-3.4.2.tgz", - "integrity": "sha512-U2G8fw7KW1rdECyJDho5N14z8hSllpTk430KG1QPLjrv5w3pDksZ+YcpITd0u762Vw3gq3Arpg65Bk6U4ua6aQ==", - "license": "MIT", - "dependencies": { - "@clack/prompts": "^1.7.0", - "consola": "^3.4.2", - "diff": "^8.0.4", - "execa": "^8.0.1", - "magicast": "^0.5.4", - "pathe": "^2.0.3", - "pkg-types": "^2.3.1", - "semver": "^7.8.5" - }, - "bin": { - "devtools-wizard": "cli.mjs" - } - }, - "node_modules/@nuxt/devtools-wizard/node_modules/@clack/core": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/@clack/core/-/core-1.5.1.tgz", - "integrity": "sha512-iHTrHA8MtVuLl2TfZySmcKv1qO2PoyC9Z7pfSDozEuV5vtY3/wcOPKJXlqJ5Oq2Cx5DDGQGAMVx6HZfRRoVEbQ==", - "license": "MIT", - "dependencies": { - "fast-wrap-ansi": "^0.2.0", - "sisteransi": "^1.0.5" - }, - "engines": { - "node": ">= 20.12.0" - } - }, - "node_modules/@nuxt/devtools-wizard/node_modules/@clack/prompts": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/@clack/prompts/-/prompts-1.8.1.tgz", - "integrity": "sha512-dlT1m5e/0yUL0kRNcQn7yGLVThkgbB0Ga/1AmfDDC/8ik6AIiSf2QLQO2zPYvefsHP0aFgxO93cVLCCfDp7kzQ==", - "license": "MIT", - "dependencies": { - "@clack/core": "1.5.1", - "fast-string-width": "^3.0.2", - "fast-wrap-ansi": "^0.2.0", - "sisteransi": "^1.0.5" - }, - "engines": { - "node": ">= 20.12.0" - } - }, - "node_modules/@nuxt/devtools/node_modules/@nuxt/kit": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/@nuxt/kit/-/kit-4.6.0.tgz", - "integrity": "sha512-UXTajJ+MFJgwV9UL1CTNNd2PSw5PC3Pjb2rM7FsSYcr92ySQKXEslBnUKbvnXT+gG3NbZ/EF70+WenoNIHX2tw==", + "node_modules/@nuxt/nitro-server": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/@nuxt/nitro-server/-/nitro-server-4.6.1.tgz", + "integrity": "sha512-ecMNTxJ44uany4cjA58EW5AMbqcG0XgqP0d0HJmZ7z3CupPCc+qmd9InvzsX53uU7ZVzAkWmQ8K+0lrzl9Xp3A==", "license": "MIT", "dependencies": { + "@nuxt/kit": "4.6.1", + "@unhead/vue": "^3.4.2", "consola": "^3.4.2", "defu": "^6.1.7", "destr": "^2.0.5", + "devalue": "^6.0.2", "errx": "^0.2.2", + "escape-string-regexp": "^5.0.0", "exsolve": "^1.1.3", - "ignore": "^7.0.12", + "flru": "^1.0.2", + "h3": "^1.15.11", + "impound": "^1.2.0", "klona": "^2.0.6", - "microdiff": "^1.6.0", + "mocked-exports": "^0.1.1", + "my-bad": "^0.2.11", + "nitropack": "^2.13.4", "nostics": "^1.3.0", - "package-manager-detector": "^1.8.0", + "ohash": "^2.0.12", "pathe": "^2.0.3", - "rc9": "^3.1.0", - "scule": "^1.3.0", + "rou3": "^0.9.2", "std-env": "^4.3.0", - "tinyexec": "^1.3.1", - "tinyglobby": "^0.2.17", - "unctx": "^3.0.1", - "verkit": "^0.5.0" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "peerDependencies": { - "@nuxt/schema": "^3.0.0 || ^4.0.0 || ^5.0.0-0", - "confbox": ">=0.1.0", - "dotenv": "*", - "giget": ">=3.1.0", - "jiti": "^2.0.0", - "mlly": "^1.2.0", - "oxc-parser": ">=0.56.0", - "rolldown": ">=1.0.0" - }, - "peerDependenciesMeta": { - "@nuxt/schema": { - "optional": true - }, - "confbox": { - "optional": true - }, - "dotenv": { - "optional": true - }, - "giget": { - "optional": true - }, - "jiti": { - "optional": true - }, - "mlly": { - "optional": true - }, - "oxc-parser": { - "optional": true - }, - "rolldown": { - "optional": true - } - } - }, - "node_modules/@nuxt/devtools/node_modules/errx": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/errx/-/errx-0.2.2.tgz", - "integrity": "sha512-HaPN6cxm6iZ9bIbQQ5orChcw7SY5smUzM08ZMc6OtV4UQhrrc3Ms1G8Ww2n/9lXkWqiioNTssF8bCvchrSbFEQ==", - "license": "MIT" - }, - "node_modules/@nuxt/devtools/node_modules/std-env": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.3.0.tgz", - "integrity": "sha512-OtU/EgQ1kIm5KwqQpBC6ZEMXrZRui11w8zgfTWp8cdO9B8OaPsbA8bTHO2P+HNo1VlUTGMVBwPhydu6poeXiag==", - "license": "MIT" - }, - "node_modules/@nuxt/devtools/node_modules/unctx": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/unctx/-/unctx-3.0.1.tgz", - "integrity": "sha512-5RAt2etv7g362RXyd33R82gm9u/kbtQlpoaOs9Bgm4E32GRMJ0xjbZyvdxUTmiuHk3V1IQb1aUNrp5IWY+JaWw==", - "license": "MIT", - "peerDependencies": { - "magic-string": ">=0.30.21", - "oxc-parser": ">=0.140.0", - "rolldown": "^1.1.5", - "unplugin": "^3.3.0" - }, - "peerDependenciesMeta": { - "magic-string": { - "optional": true - }, - "oxc-parser": { - "optional": true - }, - "rolldown": { - "optional": true - }, - "unplugin": { - "optional": true - } - } - }, - "node_modules/@nuxt/kit": { - "version": "3.21.11", - "resolved": "https://registry.npmjs.org/@nuxt/kit/-/kit-3.21.11.tgz", - "integrity": "sha512-0Xi3tgwN77w43Q8GCPIrvWmF1J7Peehkts44E0uKNIml9lB8WoUn8YxyUjxBv47XtVR86NWoWALAT+/IEMHJEA==", - "license": "MIT", - "dependencies": { - "c12": "^3.3.4", - "consola": "^3.4.2", - "defu": "^6.1.7", - "destr": "^2.0.5", - "errx": "^0.1.2", - "exsolve": "^1.1.1", - "ignore": "^7.0.6", - "jiti": "^2.7.0", - "klona": "^2.0.6", - "knitwork": "^1.3.0", - "mlly": "^1.8.2", - "ohash": "^2.0.11", - "pathe": "^2.0.3", - "pkg-types": "^2.3.1", - "rc9": "^3.0.1", - "scule": "^1.3.0", - "semver": "^7.8.5", - "tinyglobby": "^0.2.17", - "ufo": "^1.6.4", - "unctx": "^2.5.0", - "untyped": "^2.0.0" - }, - "engines": { - "node": ">=18.12.0" - } - }, - "node_modules/@nuxt/nitro-server": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/@nuxt/nitro-server/-/nitro-server-4.6.1.tgz", - "integrity": "sha512-ecMNTxJ44uany4cjA58EW5AMbqcG0XgqP0d0HJmZ7z3CupPCc+qmd9InvzsX53uU7ZVzAkWmQ8K+0lrzl9Xp3A==", - "license": "MIT", - "dependencies": { - "@nuxt/kit": "4.6.1", - "@unhead/vue": "^3.4.2", - "consola": "^3.4.2", - "defu": "^6.1.7", - "destr": "^2.0.5", - "devalue": "^6.0.2", - "errx": "^0.2.2", - "escape-string-regexp": "^5.0.0", - "exsolve": "^1.1.3", - "flru": "^1.0.2", - "h3": "^1.15.11", - "impound": "^1.2.0", - "klona": "^2.0.6", - "mocked-exports": "^0.1.1", - "my-bad": "^0.2.11", - "nitropack": "^2.13.4", - "nostics": "^1.3.0", - "ohash": "^2.0.12", - "pathe": "^2.0.3", - "rou3": "^0.9.2", - "std-env": "^4.3.0", - "ufo": "^1.6.4", + "ufo": "^1.6.4", "unctx": "^3.0.1", "unrouting": "^0.2.3", "unstorage": "^1.17.5", @@ -1869,89 +1563,12 @@ } } }, - "node_modules/@nuxt/nitro-server/node_modules/@nuxt/kit": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/@nuxt/kit/-/kit-4.6.1.tgz", - "integrity": "sha512-ld5+/o4EMqyw6Qy+SdEYkezfprS87+p+xOBhnQgXwyrg1EdtILfLeudQ+hX6UyCnsoF6o0SOw3pjN1P4Tg9O0w==", - "license": "MIT", - "dependencies": { - "consola": "^3.4.2", - "defu": "^6.1.7", - "destr": "^2.0.5", - "errx": "^0.2.2", - "exsolve": "^1.1.3", - "ignore": "^7.0.12", - "klona": "^2.0.6", - "microdiff": "^1.6.0", - "nostics": "^1.3.0", - "package-manager-detector": "^1.9.0", - "pathe": "^2.0.3", - "rc9": "^3.1.0", - "scule": "^1.3.0", - "std-env": "^4.3.0", - "tinyexec": "^1.3.1", - "tinyglobby": "^0.2.17", - "unctx": "^3.0.1", - "verkit": "^0.5.0" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "peerDependencies": { - "@nuxt/schema": "^3.0.0 || ^4.0.0 || ^5.0.0-0", - "confbox": ">=0.1.0", - "dotenv": "*", - "giget": ">=3.1.0", - "jiti": "^2.0.0", - "mlly": "^1.2.0", - "oxc-parser": ">=0.56.0", - "rolldown": ">=1.0.0" - }, - "peerDependenciesMeta": { - "@nuxt/schema": { - "optional": true - }, - "confbox": { - "optional": true - }, - "dotenv": { - "optional": true - }, - "giget": { - "optional": true - }, - "jiti": { - "optional": true - }, - "mlly": { - "optional": true - }, - "oxc-parser": { - "optional": true - }, - "rolldown": { - "optional": true - } - } - }, - "node_modules/@nuxt/nitro-server/node_modules/errx": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/errx/-/errx-0.2.2.tgz", - "integrity": "sha512-HaPN6cxm6iZ9bIbQQ5orChcw7SY5smUzM08ZMc6OtV4UQhrrc3Ms1G8Ww2n/9lXkWqiioNTssF8bCvchrSbFEQ==", - "license": "MIT" - }, "node_modules/@nuxt/nitro-server/node_modules/rou3": { "version": "0.9.2", "resolved": "https://registry.npmjs.org/rou3/-/rou3-0.9.2.tgz", "integrity": "sha512-3SOzvaAg8rkHrXtRjpCvCvbyO5to9oOO27Z/XqHEYXfMRVSw/qMIVdmaOk9W2lcRLtR6dlqTjo9hDeJk70QBYQ==", "license": "MIT" }, - "node_modules/@nuxt/nitro-server/node_modules/std-env": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.3.0.tgz", - "integrity": "sha512-OtU/EgQ1kIm5KwqQpBC6ZEMXrZRui11w8zgfTWp8cdO9B8OaPsbA8bTHO2P+HNo1VlUTGMVBwPhydu6poeXiag==", - "license": "MIT" - }, "node_modules/@nuxt/nitro-server/node_modules/unctx": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/unctx/-/unctx-3.0.1.tgz", @@ -1995,12 +1612,6 @@ "node": "^14.18.0 || >=16.10.0" } }, - "node_modules/@nuxt/schema/node_modules/std-env": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.3.0.tgz", - "integrity": "sha512-OtU/EgQ1kIm5KwqQpBC6ZEMXrZRui11w8zgfTWp8cdO9B8OaPsbA8bTHO2P+HNo1VlUTGMVBwPhydu6poeXiag==", - "license": "MIT" - }, "node_modules/@nuxt/telemetry": { "version": "2.9.1", "resolved": "https://registry.npmjs.org/@nuxt/telemetry/-/telemetry-2.9.1.tgz", @@ -2022,61 +1633,50 @@ "@nuxt/kit": ">=3.0.0" } }, - "node_modules/@nuxt/telemetry/node_modules/std-env": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.3.0.tgz", - "integrity": "sha512-OtU/EgQ1kIm5KwqQpBC6ZEMXrZRui11w8zgfTWp8cdO9B8OaPsbA8bTHO2P+HNo1VlUTGMVBwPhydu6poeXiag==", - "license": "MIT" - }, "node_modules/@nuxt/test-utils": { - "version": "3.23.0", - "resolved": "https://registry.npmjs.org/@nuxt/test-utils/-/test-utils-3.23.0.tgz", - "integrity": "sha512-NZKWSwvfIiTO2qhMoJHVbUQLgJMe96J9ccLhPPqN5+a/XzISZ027LG9wWVp1tC5oB0qQ3eUDhrxmq6Lj8EQLMQ==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@nuxt/test-utils/-/test-utils-4.3.3.tgz", + "integrity": "sha512-0Nxp6e+VJROAMGuyurTX3+ON+l3y79TuNDsciy+7TfCPx4GcitTrYsxjomi65jLVi0H32aEb2sloh/qmiwj6aA==", "dev": true, "license": "MIT", "dependencies": { - "@clack/prompts": "1.0.0-alpha.9", - "@nuxt/kit": "^3.20.2", - "c12": "^3.3.3", + "@clack/prompts": "1.8.1", + "@nuxt/devtools-kit": "^3.4.2", + "@nuxt/kit": "^4.5.2", "consola": "^3.4.2", - "defu": "^6.1.4", - "destr": "^2.0.5", - "estree-walker": "^3.0.3", - "exsolve": "^1.0.8", + "defu": "^6.1.7", + "exsolve": "^1.1.1", "fake-indexeddb": "^6.2.5", "get-port-please": "^3.2.0", - "h3": "^1.15.4", - "h3-next": "npm:h3@^2.0.1-rc.7", - "local-pkg": "^1.1.2", - "magic-string": "^0.30.21", - "node-fetch-native": "^1.6.7", - "node-mock-http": "^1.0.4", - "nypm": "^0.6.2", + "magic-string": "^1.4.2", "ofetch": "^1.5.1", + "oxc-walker": "^1.1.1", + "package-manager-detector": "^1.8.0", "pathe": "^2.0.3", - "perfect-debounce": "^2.0.0", + "perfect-debounce": "^2.1.0", "radix3": "^1.1.2", - "scule": "^1.3.0", - "std-env": "^3.10.0", - "tinyexec": "^1.0.2", - "ufo": "^1.6.1", - "unplugin": "^2.3.11", - "vitest-environment-nuxt": "^1.0.1", - "vue": "^3.5.26" + "std-env": "^4.2.0", + "tinyexec": "^1.3.1", + "ufo": "^1.6.4", + "unplugin": "^3.4.0", + "vitest-environment-nuxt": "2.0.0" }, "engines": { - "node": "^20.11.1 || ^22.0.0 || >=24.0.0" + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" }, "peerDependencies": { - "@cucumber/cucumber": "^10.3.1 || >=11.0.0", - "@jest/globals": "^29.5.0 || >=30.0.0", + "@cucumber/cucumber": ">=11.0.0", + "@jest/globals": ">=30.0.0", "@playwright/test": "^1.43.1", - "@testing-library/vue": "^7.0.0 || ^8.0.1", + "@testing-library/vue": "^8.0.1", "@vue/test-utils": "^2.4.2", - "happy-dom": "*", - "jsdom": "*", + "h3": "^1.15.0 || ^2.0.0-0", + "happy-dom": ">=20.0.11", + "jsdom": ">=27.4.0", + "nuxt": "^3.21.0 || ^4.0.0 || ^5.0.0-0", "playwright-core": "^1.43.1", - "vitest": "^3.2.0" + "vitest": "^4.0.2 || ^5.0.0", + "vue": "^3.5.0" }, "peerDependenciesMeta": { "@cucumber/cucumber": { @@ -2097,12 +1697,18 @@ "@vue/test-utils": { "optional": true }, + "h3": { + "optional": true + }, "happy-dom": { "optional": true }, "jsdom": { "optional": true }, + "nuxt": { + "optional": true + }, "playwright-core": { "optional": true }, @@ -2111,6 +1717,75 @@ } } }, + "node_modules/@nuxt/test-utils/node_modules/magic-string": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.4.3.tgz", + "integrity": "sha512-z12OxmaPGE0F4xlpGdjuAUckipLpQlTAuAmyGhNoD7ODpYUzvDfvtxUbjM/jwznyP178oju/KDdyi220wNJ0RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.6.0" + } + }, + "node_modules/@nuxt/test-utils/node_modules/unplugin": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-3.4.0.tgz", + "integrity": "sha512-9skdIFlCsPdFV7wUfZxNsFInlW+7nJmGu2gkTu0OUhF56aXGsHab9x52/QhdJ4lC7ZDPWTxbiC4ANqVlsuaW3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "picomatch": "^4.0.7", + "webpack-virtual-modules": "^0.6.2" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@farmfe/core": "*", + "@rsbuild/core": "*", + "@rspack/core": "*", + "bun-types-no-globals": "*", + "esbuild": "*", + "rolldown": "*", + "rollup": "*", + "unloader": "*", + "vite": "*", + "webpack": "*" + }, + "peerDependenciesMeta": { + "@farmfe/core": { + "optional": true + }, + "@rsbuild/core": { + "optional": true + }, + "@rspack/core": { + "optional": true + }, + "bun-types-no-globals": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "rolldown": { + "optional": true + }, + "rollup": { + "optional": true + }, + "unloader": { + "optional": true + }, + "vite": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, "node_modules/@nuxt/vite-builder": { "version": "4.6.1", "resolved": "https://registry.npmjs.org/@nuxt/vite-builder/-/vite-builder-4.6.1.tgz", @@ -2172,77 +1847,6 @@ } } }, - "node_modules/@nuxt/vite-builder/node_modules/@nuxt/kit": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/@nuxt/kit/-/kit-4.6.1.tgz", - "integrity": "sha512-ld5+/o4EMqyw6Qy+SdEYkezfprS87+p+xOBhnQgXwyrg1EdtILfLeudQ+hX6UyCnsoF6o0SOw3pjN1P4Tg9O0w==", - "license": "MIT", - "dependencies": { - "consola": "^3.4.2", - "defu": "^6.1.7", - "destr": "^2.0.5", - "errx": "^0.2.2", - "exsolve": "^1.1.3", - "ignore": "^7.0.12", - "klona": "^2.0.6", - "microdiff": "^1.6.0", - "nostics": "^1.3.0", - "package-manager-detector": "^1.9.0", - "pathe": "^2.0.3", - "rc9": "^3.1.0", - "scule": "^1.3.0", - "std-env": "^4.3.0", - "tinyexec": "^1.3.1", - "tinyglobby": "^0.2.17", - "unctx": "^3.0.1", - "verkit": "^0.5.0" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "peerDependencies": { - "@nuxt/schema": "^3.0.0 || ^4.0.0 || ^5.0.0-0", - "confbox": ">=0.1.0", - "dotenv": "*", - "giget": ">=3.1.0", - "jiti": "^2.0.0", - "mlly": "^1.2.0", - "oxc-parser": ">=0.56.0", - "rolldown": ">=1.0.0" - }, - "peerDependenciesMeta": { - "@nuxt/schema": { - "optional": true - }, - "confbox": { - "optional": true - }, - "dotenv": { - "optional": true - }, - "giget": { - "optional": true - }, - "jiti": { - "optional": true - }, - "mlly": { - "optional": true - }, - "oxc-parser": { - "optional": true - }, - "rolldown": { - "optional": true - } - } - }, - "node_modules/@nuxt/vite-builder/node_modules/errx": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/errx/-/errx-0.2.2.tgz", - "integrity": "sha512-HaPN6cxm6iZ9bIbQQ5orChcw7SY5smUzM08ZMc6OtV4UQhrrc3Ms1G8Ww2n/9lXkWqiioNTssF8bCvchrSbFEQ==", - "license": "MIT" - }, "node_modules/@nuxt/vite-builder/node_modules/npm-run-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-6.0.0.tgz", @@ -2261,46 +1865,14 @@ }, "node_modules/@nuxt/vite-builder/node_modules/path-key": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", - "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@nuxt/vite-builder/node_modules/std-env": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.3.0.tgz", - "integrity": "sha512-OtU/EgQ1kIm5KwqQpBC6ZEMXrZRui11w8zgfTWp8cdO9B8OaPsbA8bTHO2P+HNo1VlUTGMVBwPhydu6poeXiag==", - "license": "MIT" - }, - "node_modules/@nuxt/vite-builder/node_modules/unctx": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/unctx/-/unctx-3.0.1.tgz", - "integrity": "sha512-5RAt2etv7g362RXyd33R82gm9u/kbtQlpoaOs9Bgm4E32GRMJ0xjbZyvdxUTmiuHk3V1IQb1aUNrp5IWY+JaWw==", - "license": "MIT", - "peerDependencies": { - "magic-string": ">=0.30.21", - "oxc-parser": ">=0.140.0", - "rolldown": "^1.1.5", - "unplugin": "^3.3.0" + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "license": "MIT", + "engines": { + "node": ">=12" }, - "peerDependenciesMeta": { - "magic-string": { - "optional": true - }, - "oxc-parser": { - "optional": true - }, - "rolldown": { - "optional": true - }, - "unplugin": { - "optional": true - } + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/@nuxt/vite-builder/node_modules/unicorn-magic": { @@ -2396,115 +1968,12 @@ "vite": "^8.3.2" } }, - "node_modules/@nuxt/vite-server/node_modules/@nuxt/kit": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/@nuxt/kit/-/kit-4.6.1.tgz", - "integrity": "sha512-ld5+/o4EMqyw6Qy+SdEYkezfprS87+p+xOBhnQgXwyrg1EdtILfLeudQ+hX6UyCnsoF6o0SOw3pjN1P4Tg9O0w==", - "license": "MIT", - "dependencies": { - "consola": "^3.4.2", - "defu": "^6.1.7", - "destr": "^2.0.5", - "errx": "^0.2.2", - "exsolve": "^1.1.3", - "ignore": "^7.0.12", - "klona": "^2.0.6", - "microdiff": "^1.6.0", - "nostics": "^1.3.0", - "package-manager-detector": "^1.9.0", - "pathe": "^2.0.3", - "rc9": "^3.1.0", - "scule": "^1.3.0", - "std-env": "^4.3.0", - "tinyexec": "^1.3.1", - "tinyglobby": "^0.2.17", - "unctx": "^3.0.1", - "verkit": "^0.5.0" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "peerDependencies": { - "@nuxt/schema": "^3.0.0 || ^4.0.0 || ^5.0.0-0", - "confbox": ">=0.1.0", - "dotenv": "*", - "giget": ">=3.1.0", - "jiti": "^2.0.0", - "mlly": "^1.2.0", - "oxc-parser": ">=0.56.0", - "rolldown": ">=1.0.0" - }, - "peerDependenciesMeta": { - "@nuxt/schema": { - "optional": true - }, - "confbox": { - "optional": true - }, - "dotenv": { - "optional": true - }, - "giget": { - "optional": true - }, - "jiti": { - "optional": true - }, - "mlly": { - "optional": true - }, - "oxc-parser": { - "optional": true - }, - "rolldown": { - "optional": true - } - } - }, - "node_modules/@nuxt/vite-server/node_modules/errx": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/errx/-/errx-0.2.2.tgz", - "integrity": "sha512-HaPN6cxm6iZ9bIbQQ5orChcw7SY5smUzM08ZMc6OtV4UQhrrc3Ms1G8Ww2n/9lXkWqiioNTssF8bCvchrSbFEQ==", - "license": "MIT" - }, "node_modules/@nuxt/vite-server/node_modules/rou3": { "version": "0.9.2", "resolved": "https://registry.npmjs.org/rou3/-/rou3-0.9.2.tgz", "integrity": "sha512-3SOzvaAg8rkHrXtRjpCvCvbyO5to9oOO27Z/XqHEYXfMRVSw/qMIVdmaOk9W2lcRLtR6dlqTjo9hDeJk70QBYQ==", "license": "MIT" }, - "node_modules/@nuxt/vite-server/node_modules/std-env": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.3.0.tgz", - "integrity": "sha512-OtU/EgQ1kIm5KwqQpBC6ZEMXrZRui11w8zgfTWp8cdO9B8OaPsbA8bTHO2P+HNo1VlUTGMVBwPhydu6poeXiag==", - "license": "MIT" - }, - "node_modules/@nuxt/vite-server/node_modules/unctx": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/unctx/-/unctx-3.0.1.tgz", - "integrity": "sha512-5RAt2etv7g362RXyd33R82gm9u/kbtQlpoaOs9Bgm4E32GRMJ0xjbZyvdxUTmiuHk3V1IQb1aUNrp5IWY+JaWw==", - "license": "MIT", - "peerDependencies": { - "magic-string": ">=0.30.21", - "oxc-parser": ">=0.140.0", - "rolldown": "^1.1.5", - "unplugin": "^3.3.0" - }, - "peerDependenciesMeta": { - "magic-string": { - "optional": true - }, - "oxc-parser": { - "optional": true - }, - "rolldown": { - "optional": true - }, - "unplugin": { - "optional": true - } - } - }, "node_modules/@one-ini/wasm": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/@one-ini/wasm/-/wasm-0.2.1.tgz", @@ -3421,12 +2890,39 @@ "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", "license": "MIT" }, + "node_modules/@types/node": { + "version": "26.6.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.5.tgz", + "integrity": "sha512-uMzsv9stDdofIZBx4UGh4Z9ZgXnGC9seRDscilBcTHAeOFk67MZg/k9bWVfO+bk4qZmG+l52hRnLwUiH2LnJTg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "undici-types": "~8.9.0" + } + }, "node_modules/@types/resolve": { "version": "1.20.2", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==", "license": "MIT" }, + "node_modules/@types/whatwg-mimetype": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/whatwg-mimetype/-/whatwg-mimetype-3.0.2.tgz", + "integrity": "sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/ws": { + "version": "8.18.2", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.2.tgz", + "integrity": "sha512-67MQl+fpWKVTT1NYdnmo3U4sc/xPo/zQBncVnI74qmQa0z/b+1g6iYqNmGCPbxO+zz2aklb08a0oHfegiVd0/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@unhead/bundler": { "version": "3.4.2", "resolved": "https://registry.npmjs.org/@unhead/bundler/-/bundler-3.4.2.tgz", @@ -3699,139 +3195,79 @@ "vue": "^3.0.0" } }, - "node_modules/@vitest/expect": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.7.tgz", - "integrity": "sha512-E8eBXaKibuvH2pSZErOjdVb5vF4PbKYcrnluBTYxEk1l/VhhwZg1kZQsdtjq+CsF5CFydf2Rdkz7jDHKSisi3w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/chai": "^5.2.2", - "@vitest/spy": "3.2.7", - "@vitest/utils": "3.2.7", - "chai": "^5.2.0", - "tinyrainbow": "^2.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/pretty-format": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.7.tgz", - "integrity": "sha512-KUHlwqVu0sRlhCdyPdQ/wBoTfRahjUky1MubOmYw9fWfIZy1gNoHpuaaQBPAaMaVYdQYHJLurzj8ECCj5OwTqA==", + "node_modules/@vitest/mocker": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-5.0.3.tgz", + "integrity": "sha512-T8sWAIbkSyAjkwTcaEc3Iu0o9A27X1/kdXrizhZkGuSKScRQtRzclfAMpOTcGdXCsqxeWlpGy3XjqaW8CpLORg==", "dev": true, "license": "MIT", "dependencies": { - "tinyrainbow": "^2.0.0" + "@jridgewell/trace-mapping": "0.3.31", + "@vitest/spy": "5.0.3", + "estree-walker": "^3.0.3", + "magic-string": "^1.2.3" }, "funding": { "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/runner": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.7.tgz", - "integrity": "sha512-sB9y4ovltoQP+WaUPwmSxO9WIg9Ig694Di5PalVPsYHklAdE027mehpWF2SQSVq+k6sFgaivbTjTJwZLSHbedA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/utils": "3.2.7", - "pathe": "^2.0.3", - "strip-literal": "^3.0.0" }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/runner/node_modules/js-tokens": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", - "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@vitest/runner/node_modules/strip-literal": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", - "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", - "dev": true, - "license": "MIT", - "dependencies": { - "js-tokens": "^9.0.1" + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, - "funding": { - "url": "https://github.com/sponsors/antfu" + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } } }, - "node_modules/@vitest/snapshot": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.7.tgz", - "integrity": "sha512-7C+MwShwtBSI5Buwoyg3s/iY1eHL9PKAf+O1wVh/TdnjXUtkoL/9YQtre90i4MtNXM6edP1wJ2zOBpfCyhIS7g==", + "node_modules/@vitest/mocker/node_modules/magic-string": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.4.3.tgz", + "integrity": "sha512-z12OxmaPGE0F4xlpGdjuAUckipLpQlTAuAmyGhNoD7ODpYUzvDfvtxUbjM/jwznyP178oju/KDdyi220wNJ0RQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "3.2.7", - "magic-string": "^0.30.17", - "pathe": "^2.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" + "@jridgewell/sourcemap-codec": "^1.6.0" } }, "node_modules/@vitest/spy": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.7.tgz", - "integrity": "sha512-Q2eQGI6d2L/hBtZ0qNuKcAGid68XK6cv1xsoaIma6PaJhHPoqcEJhYpXZ/5myCMqkNgtP6UKuBhbc0nHKnrkuQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "tinyspy": "^4.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/utils": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.7.tgz", - "integrity": "sha512-x6BDOd7dyo3PFLY3I9/HJ25X/6OurhGXk2/B9gOZNPF7XDVjeBK4k01lQE5uvDpbuheErh91qYuE1E2OEjK3Rw==", + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-5.0.3.tgz", + "integrity": "sha512-XhFysQTB8AZ+P4gMi+Lpo99vg2AZi0qKpaB9yXQl37+CaMEAPO3iH/wGVnSyL5MPERiLezpqTVtrR6UZH5GCXg==", "dev": true, "license": "MIT", - "dependencies": { - "@vitest/pretty-format": "3.2.7", - "loupe": "^3.1.4", - "tinyrainbow": "^2.0.0" - }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@volar/language-core": { - "version": "2.4.15", - "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.15.tgz", - "integrity": "sha512-3VHw+QZU0ZG9IuQmzT68IyN4hZNd9GchGPhbD9+pa8CVv7rnoOZwo7T8weIbrRmihqy3ATpdfXFnqRrfPVK6CA==", + "version": "2.4.28", + "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.28.tgz", + "integrity": "sha512-w4qhIJ8ZSitgLAkVay6AbcnC7gP3glYM3fYwKV3srj8m494E3xtrCv6E+bWviiK/8hs6e6t1ij1s2Endql7vzQ==", "devOptional": true, "license": "MIT", "dependencies": { - "@volar/source-map": "2.4.15" + "@volar/source-map": "2.4.28" } }, "node_modules/@volar/source-map": { - "version": "2.4.15", - "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.15.tgz", - "integrity": "sha512-CPbMWlUN6hVZJYGcU/GSoHu4EnCHiLaXI9n8c9la6RaI9W5JHX+NqG+GSQcB0JdC2FIBLdZJwGsfKyBB71VlTg==", + "version": "2.4.28", + "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.28.tgz", + "integrity": "sha512-yX2BDBqJkRXfKw8my8VarTyjv48QwxdJtvRgUpNE5erCsgEUdI2DsLbpa+rOQVAJYshY99szEcRDmyHbF10ggQ==", "devOptional": true, "license": "MIT" }, "node_modules/@volar/typescript": { - "version": "2.4.15", - "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.15.tgz", - "integrity": "sha512-2aZ8i0cqPGjXb4BhkMsPYDkkuc2ZQ6yOpqwAuNwUoncELqoy5fRgOQtLR9gB0g902iS0NAkvpIzs27geVyVdPg==", + "version": "2.4.28", + "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.28.tgz", + "integrity": "sha512-Ja6yvWrbis2QtN4ClAKreeUZPVYMARDYZl9LMEv1iQ1QdepB6wn0jTRxA9MftYmYa4DQ4k/DaSZpFPUfxl8giw==", "devOptional": true, "license": "MIT", "dependencies": { - "@volar/language-core": "2.4.15", + "@volar/language-core": "2.4.28", "path-browserify": "^1.0.1", "vscode-uri": "^3.0.8" } @@ -3975,15 +3411,13 @@ "@vue/shared": "3.5.43" } }, - "node_modules/@vue/compiler-vue2": { - "version": "2.7.16", - "resolved": "https://registry.npmjs.org/@vue/compiler-vue2/-/compiler-vue2-2.7.16.tgz", - "integrity": "sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==", - "devOptional": true, + "node_modules/@vue/devtools-api": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-8.2.1.tgz", + "integrity": "sha512-6u4vXBlIBAC1wMplIZgpyPn7uh/s4Bf6F5bMzvLv+EdJ0aHs/+4B7Ygv864EStQSjRbsRzTko/kUG1A1IejQ3A==", "license": "MIT", "dependencies": { - "de-indent": "^1.0.2", - "he": "^1.2.0" + "@vue/devtools-kit": "^8.2.1" } }, "node_modules/@vue/devtools-core": { @@ -4033,61 +3467,19 @@ "license": "MIT" }, "node_modules/@vue/language-core": { - "version": "2.2.12", - "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-2.2.12.tgz", - "integrity": "sha512-IsGljWbKGU1MZpBPN+BvPAdr55YPkj2nB/TBNGNC32Vy2qLG25DYu/NBN2vNtZqdRbTRjaoYrahLrToim2NanA==", + "version": "3.3.13", + "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-3.3.13.tgz", + "integrity": "sha512-axA0Xemn5LuUKuKWob0/kVq7xQ2mJJM6VqA9Exvt35nM9yRC31goWVve/G/Zb2ZCLsXC8FHYjjfXlnshknSCew==", "devOptional": true, "license": "MIT", "dependencies": { - "@volar/language-core": "2.4.15", + "@volar/language-core": "2.4.28", "@vue/compiler-dom": "^3.5.0", - "@vue/compiler-vue2": "^2.7.16", "@vue/shared": "^3.5.0", - "alien-signals": "^1.0.3", - "minimatch": "^9.0.3", + "alien-signals": "^3.2.1", "muggle-string": "^0.4.1", - "path-browserify": "^1.0.1" - }, - "peerDependencies": { - "typescript": "*" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@vue/language-core/node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/@vue/language-core/node_modules/brace-expansion": { - "version": "2.1.7", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.7.tgz", - "integrity": "sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/@vue/language-core/node_modules/minimatch": { - "version": "9.0.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", - "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", - "devOptional": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.2" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "path-browserify": "^1.0.1", + "picomatch": "^4.0.4" } }, "node_modules/@vue/reactivity": { @@ -4212,9 +3604,9 @@ } }, "node_modules/alien-signals": { - "version": "1.0.13", - "resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-1.0.13.tgz", - "integrity": "sha512-OGj9yyTnJEttvzhTUWuscOvtqxq5vrhF7vL9oS0xJ2mK0ItPYP1/y+vCFebfxoEyAz0++1AIwJ5CMr+Fk3nDmg==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-3.2.1.tgz", + "integrity": "sha512-I8FjmltrfnDFoZedi5CG8DghVYNhzb/Ijluz7tCSJH0xpd0484Kowhbb1XDYOxfJpU1p5wnM2X54dA+IfGyD1g==", "devOptional": true, "license": "MIT" }, @@ -4812,6 +4204,19 @@ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "license": "MIT" }, + "node_modules/buffer-image-size": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/buffer-image-size/-/buffer-image-size-0.6.4.tgz", + "integrity": "sha512-nEh+kZOPY1w+gcCMobZ6ETUp9WfibndnosbpwB1iJk/8Gt5ZF2bhS6+B6bPYz424KtwsR6Rflc3tCz1/ghX2dQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + }, + "engines": { + "node": ">=4.0" + } + }, "node_modules/bundle-name": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.1.tgz", @@ -4848,21 +4253,11 @@ }, "peerDependencies": { "magicast": "*" - }, - "peerDependenciesMeta": { - "magicast": { - "optional": true - } - } - }, - "node_modules/cac": { - "version": "6.7.14", - "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", - "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" + }, + "peerDependenciesMeta": { + "magicast": { + "optional": true + } } }, "node_modules/caniuse-api": { @@ -4896,32 +4291,15 @@ "license": "CC-BY-4.0" }, "node_modules/chai": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", - "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.3.0.tgz", + "integrity": "sha512-XWAtwJ6OHO+tj0EKCs0Y2UamnyOxseZWltU4x2U2wh8g4AigdjwvtUjvLP2tqkA/avxHEtzxNaqGq/YGNwckKg==", "dev": true, "license": "MIT", - "dependencies": { - "assertion-error": "^2.0.1", - "check-error": "^2.1.1", - "deep-eql": "^5.0.1", - "loupe": "^3.1.0", - "pathval": "^2.0.0" - }, "engines": { "node": ">=18" } }, - "node_modules/check-error": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", - "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 16" - } - }, "node_modules/chokidar": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", @@ -5415,13 +4793,6 @@ } } }, - "node_modules/de-indent": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz", - "integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==", - "devOptional": true, - "license": "MIT" - }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -5439,16 +4810,6 @@ } } }, - "node_modules/deep-eql": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", - "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/deepmerge": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", @@ -5729,9 +5090,9 @@ } }, "node_modules/errx": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/errx/-/errx-0.1.2.tgz", - "integrity": "sha512-chfpPHmCerdo/rXr/nNvPZRkV4WwDRwzwnsJ0Uzz3tVi8Z41tDctRjduYy1138ii77AFlts1qvWtX3g/Acg91Q==", + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/errx/-/errx-0.2.2.tgz", + "integrity": "sha512-HaPN6cxm6iZ9bIbQQ5orChcw7SY5smUzM08ZMc6OtV4UQhrrc3Ms1G8Ww2n/9lXkWqiioNTssF8bCvchrSbFEQ==", "license": "MIT" }, "node_modules/es-errors": { @@ -6291,36 +5652,6 @@ "uncrypto": "^0.1.3" } }, - "node_modules/h3-next": { - "name": "h3", - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/h3/-/h3-2.0.2.tgz", - "integrity": "sha512-+/zvHSNldYpNURuUUG+y1KzdASUhNnSd1VpqxYFVY40ov3YzuBnCETaUQaT+QbKkoxhh9LRa6AEQdB14LALwdA==", - "dev": true, - "license": "MIT", - "dependencies": { - "rou3": "^1.0.0", - "srvx": "^1.0.5" - }, - "bin": { - "h3": "bin/h3.mjs" - }, - "engines": { - "node": ">=20.19.0" - }, - "peerDependencies": { - "crossws": "^0.4.12", - "ocache": ">=0.3.0" - }, - "peerDependenciesMeta": { - "crossws": { - "optional": true - }, - "ocache": { - "optional": true - } - } - }, "node_modules/h3/node_modules/crossws": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/crossws/-/crossws-0.3.5.tgz", @@ -6331,14 +5662,19 @@ } }, "node_modules/happy-dom": { - "version": "17.6.3", - "resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-17.6.3.tgz", - "integrity": "sha512-UVIHeVhxmxedbWPCfgS55Jg2rDfwf2BCKeylcPSqazLz5w3Kri7Q4xdBJubsr/+VUzFLh0VjIvh13RaDA2/Xug==", + "version": "20.14.6", + "resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-20.14.6.tgz", + "integrity": "sha512-5DDsf4QmEIlRL6NkCv/fxSjYHHkZceL/IaYQdJc2h3G7DRTEai++VTjbfL0LB9geW2VrA3+vrjF3Efx+OTtX0Q==", "dev": true, "license": "MIT", "dependencies": { - "webidl-conversions": "^7.0.0", - "whatwg-mimetype": "^3.0.0" + "@types/node": ">=20.0.0", + "@types/whatwg-mimetype": "^3.0.2", + "@types/ws": "^8.18.1", + "buffer-image-size": "^0.6.4", + "entities": "^7.0.1", + "whatwg-mimetype": "^3.0.0", + "ws": "^8.21.0" }, "engines": { "node": ">=20.0.0" @@ -6356,16 +5692,6 @@ "node": ">= 0.4" } }, - "node_modules/he": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", - "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", - "devOptional": true, - "license": "MIT", - "bin": { - "he": "bin/he" - } - }, "node_modules/hookable": { "version": "6.1.2", "resolved": "https://registry.npmjs.org/hookable/-/hookable-6.1.2.tgz", @@ -7208,12 +6534,6 @@ } } }, - "node_modules/listhen/node_modules/std-env": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.3.0.tgz", - "integrity": "sha512-OtU/EgQ1kIm5KwqQpBC6ZEMXrZRui11w8zgfTWp8cdO9B8OaPsbA8bTHO2P+HNo1VlUTGMVBwPhydu6poeXiag==", - "license": "MIT" - }, "node_modules/listhen/node_modules/tinyclip": { "version": "0.1.15", "resolved": "https://registry.npmjs.org/tinyclip/-/tinyclip-0.1.15.tgz", @@ -7255,13 +6575,6 @@ "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", "license": "MIT" }, - "node_modules/loupe": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", - "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", - "dev": true, - "license": "MIT" - }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -7524,12 +6837,6 @@ } } }, - "node_modules/my-bad/node_modules/errx": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/errx/-/errx-0.2.2.tgz", - "integrity": "sha512-HaPN6cxm6iZ9bIbQQ5orChcw7SY5smUzM08ZMc6OtV4UQhrrc3Ms1G8Ww2n/9lXkWqiioNTssF8bCvchrSbFEQ==", - "license": "MIT" - }, "node_modules/nanoid": { "version": "3.3.20", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.20.tgz", @@ -7668,12 +6975,6 @@ "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==", "license": "MIT" }, - "node_modules/nitropack/node_modules/std-env": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.3.0.tgz", - "integrity": "sha512-OtU/EgQ1kIm5KwqQpBC6ZEMXrZRui11w8zgfTWp8cdO9B8OaPsbA8bTHO2P+HNo1VlUTGMVBwPhydu6poeXiag==", - "license": "MIT" - }, "node_modules/nitropack/node_modules/unimport": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/unimport/-/unimport-6.5.0.tgz", @@ -8003,98 +7304,12 @@ } } }, - "node_modules/nuxt/node_modules/@nuxt/kit": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/@nuxt/kit/-/kit-4.6.1.tgz", - "integrity": "sha512-ld5+/o4EMqyw6Qy+SdEYkezfprS87+p+xOBhnQgXwyrg1EdtILfLeudQ+hX6UyCnsoF6o0SOw3pjN1P4Tg9O0w==", - "license": "MIT", - "dependencies": { - "consola": "^3.4.2", - "defu": "^6.1.7", - "destr": "^2.0.5", - "errx": "^0.2.2", - "exsolve": "^1.1.3", - "ignore": "^7.0.12", - "klona": "^2.0.6", - "microdiff": "^1.6.0", - "nostics": "^1.3.0", - "package-manager-detector": "^1.9.0", - "pathe": "^2.0.3", - "rc9": "^3.1.0", - "scule": "^1.3.0", - "std-env": "^4.3.0", - "tinyexec": "^1.3.1", - "tinyglobby": "^0.2.17", - "unctx": "^3.0.1", - "verkit": "^0.5.0" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "peerDependencies": { - "@nuxt/schema": "^3.0.0 || ^4.0.0 || ^5.0.0-0", - "confbox": ">=0.1.0", - "dotenv": "*", - "giget": ">=3.1.0", - "jiti": "^2.0.0", - "mlly": "^1.2.0", - "oxc-parser": ">=0.56.0", - "rolldown": ">=1.0.0" - }, - "peerDependenciesMeta": { - "@nuxt/schema": { - "optional": true - }, - "confbox": { - "optional": true - }, - "dotenv": { - "optional": true - }, - "giget": { - "optional": true - }, - "jiti": { - "optional": true - }, - "mlly": { - "optional": true - }, - "oxc-parser": { - "optional": true - }, - "rolldown": { - "optional": true - } - } - }, - "node_modules/nuxt/node_modules/@vue/devtools-api": { - "version": "8.2.1", - "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-8.2.1.tgz", - "integrity": "sha512-6u4vXBlIBAC1wMplIZgpyPn7uh/s4Bf6F5bMzvLv+EdJ0aHs/+4B7Ygv864EStQSjRbsRzTko/kUG1A1IejQ3A==", - "license": "MIT", - "dependencies": { - "@vue/devtools-kit": "^8.2.1" - } - }, - "node_modules/nuxt/node_modules/confbox": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.3.1.tgz", - "integrity": "sha512-cKUSoKa8YxFZZSmraVi7onONx3amu77ngK3kGpsYHDH7drPwCRkQE1RYMPlLRrMtnciRj274XNRxcHxnKmDSnA==", - "license": "MIT" - }, "node_modules/nuxt/node_modules/cookie-es": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/cookie-es/-/cookie-es-3.1.1.tgz", "integrity": "sha512-UaXxwISYJPTr9hwQxMFYZ7kNhSXboMXP+Z3TRX6f1/NyaGPfuNUZOWP1pUEb75B2HjfklIYLVRfWiFZJyC6Npg==", "license": "MIT" }, - "node_modules/nuxt/node_modules/errx": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/errx/-/errx-0.2.2.tgz", - "integrity": "sha512-HaPN6cxm6iZ9bIbQQ5orChcw7SY5smUzM08ZMc6OtV4UQhrrc3Ms1G8Ww2n/9lXkWqiioNTssF8bCvchrSbFEQ==", - "license": "MIT" - }, "node_modules/nuxt/node_modules/iron-webcrypto": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/iron-webcrypto/-/iron-webcrypto-2.0.0.tgz", @@ -8122,12 +7337,6 @@ "integrity": "sha512-3SOzvaAg8rkHrXtRjpCvCvbyO5to9oOO27Z/XqHEYXfMRVSw/qMIVdmaOk9W2lcRLtR6dlqTjo9hDeJk70QBYQ==", "license": "MIT" }, - "node_modules/nuxt/node_modules/std-env": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.3.0.tgz", - "integrity": "sha512-OtU/EgQ1kIm5KwqQpBC6ZEMXrZRui11w8zgfTWp8cdO9B8OaPsbA8bTHO2P+HNo1VlUTGMVBwPhydu6poeXiag==", - "license": "MIT" - }, "node_modules/nuxt/node_modules/unctx": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/unctx/-/unctx-3.0.1.tgz", @@ -8212,54 +7421,6 @@ } } }, - "node_modules/nuxt/node_modules/vue-router": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-5.4.0.tgz", - "integrity": "sha512-pFvcYcxinDHjZUlf7AzFBrT8r8Xqvv4uMHoJnjc+EqPawXQvHTkF8S4tfSmQLpriVHUGfkaKlI7Et0KRjTMTdw==", - "license": "MIT", - "dependencies": { - "@vue-macros/common": "^3.1.3", - "@vue/devtools-api": "^8.1.5", - "ast-walker-scope": "^0.10.0", - "chokidar": "^5.0.0", - "confbox": "^0.3.1", - "local-pkg": "^1.2.1", - "magic-string": "^1.4.3", - "mlly": "^1.8.2", - "muggle-string": "^0.4.1", - "nostics": "^1.3.0", - "pathe": "^2.0.3", - "picomatch": "^4.0.7", - "scule": "^1.3.0", - "tinyglobby": "^0.2.17", - "unplugin": "^3.4.0", - "unplugin-utils": "^0.3.2" - }, - "funding": { - "url": "https://github.com/sponsors/posva" - }, - "peerDependencies": { - "@pinia/colada": ">=0.21.2", - "@vue/compiler-sfc": "^3.5.34 || ^4.0.0", - "pinia": "^3.0.4 || ^4.0.2", - "vite": "^7.3.0 || ^8.0.0", - "vue": "^3.5.34 || ^4.0.0" - }, - "peerDependenciesMeta": { - "@pinia/colada": { - "optional": true - }, - "@vue/compiler-sfc": { - "optional": true - }, - "pinia": { - "optional": true - }, - "vite": { - "optional": true - } - } - }, "node_modules/nypm": { "version": "0.6.10", "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.10.tgz", @@ -8468,16 +7629,6 @@ "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", "license": "MIT" }, - "node_modules/pathval": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", - "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14.16" - } - }, "node_modules/perfect-debounce": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-2.1.0.tgz", @@ -9393,16 +8544,6 @@ "node": ">= 12" } }, - "node_modules/rou3": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/rou3/-/rou3-1.0.0.tgz", - "integrity": "sha512-ySeaQkSrua+ut9g+65I7FHqOFBaY8tV0FmccXItoq0q21se32Q3u4YiW7/RqXSosWgYJaaKwRHXy3BST9PeCNQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=20.19.0" - } - }, "node_modules/run-applescript": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", @@ -9596,13 +8737,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/siginfo": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", - "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", - "dev": true, - "license": "ISC" - }, "node_modules/signal-exit": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", @@ -9722,13 +8856,6 @@ "node": ">=20.16.0" } }, - "node_modules/stackback": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", - "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", - "dev": true, - "license": "MIT" - }, "node_modules/standard-as-callback": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", @@ -9745,10 +8872,9 @@ } }, "node_modules/std-env": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", - "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", - "dev": true, + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.3.0.tgz", + "integrity": "sha512-OtU/EgQ1kIm5KwqQpBC6ZEMXrZRui11w8zgfTWp8cdO9B8OaPsbA8bTHO2P+HNo1VlUTGMVBwPhydu6poeXiag==", "license": "MIT" }, "node_modules/streamx": { @@ -10062,11 +9188,14 @@ "license": "MIT" }, "node_modules/tinybench": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", - "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-6.2.1.tgz", + "integrity": "sha512-IAbQaPJIIdhVxi0mqy9lez1wEwje0UQf9F1vwtbelEG5Nbgn0nPOxCvU+mNeNS9gyJTAhrHp74CbUaSynQpZcQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } }, "node_modules/tinyclip": { "version": "1.0.3", @@ -10102,36 +9231,6 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, - "node_modules/tinypool": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", - "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.0.0 || >=20.0.0" - } - }, - "node_modules/tinyrainbow": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", - "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/tinyspy": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.6.tgz", - "integrity": "sha512-u8KszXvGfU68hVcZpRHKG28T0krMuv2G5nDhiHaMLen/gIuFEgIJhaJuO69qjnXg5paSrbPMFfx3brNuN8eVSg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -10248,6 +9347,13 @@ "node": ">=22.19.0" } }, + "node_modules/undici-types": { + "version": "8.9.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.9.0.tgz", + "integrity": "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==", + "devOptional": true, + "license": "MIT" + }, "node_modules/unenv": { "version": "2.0.0-rc.24", "resolved": "https://registry.npmjs.org/unenv/-/unenv-2.0.0-rc.24.tgz", @@ -10909,65 +10015,72 @@ } }, "node_modules/vitest": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.7.tgz", - "integrity": "sha512-KrxIJ62Fd89gfysR4WotlgZABiz2dqFPgqGzX7s+CwsqLFomRH7777ZcrOD6+WVAh7khPQP41A+BKbpcJFrdEg==", + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-5.0.3.tgz", + "integrity": "sha512-xMw97S3rjdtj5dkVat7jCsqWBpvchs3RlpQctUqwJD0KkERk40vz2fJ77lDwW/Vzh/pk18eItYAzkodhSes3jQ==", "dev": true, "license": "MIT", "dependencies": { "@types/chai": "^5.2.2", - "@vitest/expect": "3.2.7", - "@vitest/mocker": "3.2.7", - "@vitest/pretty-format": "^3.2.7", - "@vitest/runner": "3.2.7", - "@vitest/snapshot": "3.2.7", - "@vitest/spy": "3.2.7", - "@vitest/utils": "3.2.7", - "chai": "^5.2.0", - "debug": "^4.4.1", - "expect-type": "^1.2.1", - "magic-string": "^0.30.17", - "pathe": "^2.0.3", - "picomatch": "^4.0.2", - "std-env": "^3.9.0", - "tinybench": "^2.9.0", - "tinyexec": "^0.3.2", - "tinyglobby": "^0.2.14", - "tinypool": "^1.1.1", - "tinyrainbow": "^2.0.0", - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", - "vite-node": "3.2.4", - "why-is-node-running": "^2.3.0" + "@vitest/mocker": "5.0.3", + "chai": "^6.2.2", + "es-module-lexer": "^2.3.2", + "expect-type": "^1.4.0", + "magic-string": "^1.2.3", + "obug": "^2.1.4", + "picomatch": "^4.0.7", + "std-env": "^4.2.0", + "tinybench": "^6.1.4", + "tinyexec": "^1.3.0", + "tinyglobby": "^0.2.17", + "why-is-node-running": "3.2.1" }, "bin": { "vitest": "vitest.mjs" }, "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + "node": "^22.12.0 || ^24.0.0 || >=26.0.0" }, "funding": { "url": "https://opencollective.com/vitest" }, "peerDependencies": { "@edge-runtime/vm": "*", - "@types/debug": "^4.1.12", - "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", - "@vitest/browser": "3.2.7", - "@vitest/ui": "3.2.7", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "5.0.3", + "@vitest/browser-preview": "5.0.3", + "@vitest/browser-webdriverio": "^5.0.0-beta.5 || >=5.0.0", + "@vitest/coverage-istanbul": "5.0.3", + "@vitest/coverage-v8": "5.0.3", + "@vitest/ui": "5.0.3", "happy-dom": "*", - "jsdom": "*" + "jsdom": "*", + "vite": "^6.4.0 || ^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "@edge-runtime/vm": { "optional": true }, - "@types/debug": { + "@opentelemetry/api": { "optional": true }, "@types/node": { "optional": true }, - "@vitest/browser": { + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { "optional": true }, "@vitest/ui": { @@ -10978,156 +10091,30 @@ }, "jsdom": { "optional": true - } - } - }, - "node_modules/vitest-environment-nuxt": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/vitest-environment-nuxt/-/vitest-environment-nuxt-1.0.1.tgz", - "integrity": "sha512-eBCwtIQriXW5/M49FjqNKfnlJYlG2LWMSNFsRVKomc8CaMqmhQPBS5LZ9DlgYL9T8xIVsiA6RZn2lk7vxov3Ow==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nuxt/test-utils": ">=3.13.1" - } - }, - "node_modules/vitest/node_modules/@vitest/mocker": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.7.tgz", - "integrity": "sha512-Trr0hYO9CM3Wj6ksWHRhK9IZpIY6wTMO5u/MqXurMxT57sWBaOPEtP3Oq60ihZuh5JsiagKfz95OcxdEP6dBrA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/spy": "3.2.7", - "estree-walker": "^3.0.3", - "magic-string": "^0.30.17" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "msw": "^2.4.9", - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" - }, - "peerDependenciesMeta": { - "msw": { - "optional": true }, "vite": { - "optional": true + "optional": false } } }, - "node_modules/vitest/node_modules/es-module-lexer": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", - "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", - "dev": true, - "license": "MIT" - }, - "node_modules/vitest/node_modules/tinyexec": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", - "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", - "dev": true, - "license": "MIT" - }, - "node_modules/vitest/node_modules/vite": { - "version": "7.3.7", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.7.tgz", - "integrity": "sha512-AHEzTORMbi7/7aXp7w7KZ5+DCUdSeLsJz0grY8xHpc9DHdYb0vQu6DqJfm2tOw3gDExNgPgnJTXxM6syYFHaCA==", + "node_modules/vitest-environment-nuxt": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/vitest-environment-nuxt/-/vitest-environment-nuxt-2.0.0.tgz", + "integrity": "sha512-zEGFRiCAaRR3fHnqISHKMNTRvCzkQEI1XyFeqNgR2IBD0oYkfZ1rUHwi7C+h3Cns3KPykfB0av1B3MtLEbChDw==", "dev": true, "license": "MIT", "dependencies": { - "esbuild": "^0.27.0 || ^0.28.0", - "fdir": "^6.5.0", - "picomatch": "^4.0.3", - "postcss": "^8.5.6", - "rollup": "^4.43.0", - "tinyglobby": "^0.2.15" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^20.19.0 || >=22.12.0", - "jiti": ">=1.21.0", - "less": "^4.0.0", - "lightningcss": "^1.21.0", - "sass": "^1.70.0", - "sass-embedded": "^1.70.0", - "stylus": ">=0.54.8", - "sugarss": "^5.0.0", - "terser": "^5.16.0", - "tsx": "^4.8.1", - "yaml": "^2.4.2" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "jiti": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true - } + "@nuxt/test-utils": "^4.0.0" } }, - "node_modules/vitest/node_modules/vite-node": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", - "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", + "node_modules/vitest/node_modules/magic-string": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.4.3.tgz", + "integrity": "sha512-z12OxmaPGE0F4xlpGdjuAUckipLpQlTAuAmyGhNoD7ODpYUzvDfvtxUbjM/jwznyP178oju/KDdyi220wNJ0RQ==", "dev": true, "license": "MIT", "dependencies": { - "cac": "^6.7.14", - "debug": "^4.4.1", - "es-module-lexer": "^1.7.0", - "pathe": "^2.0.3", - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" - }, - "bin": { - "vite-node": "vite-node.mjs" - }, - "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" + "@jridgewell/sourcemap-codec": "^1.6.0" } }, "node_modules/vscode-uri": { @@ -11179,15 +10166,136 @@ "integrity": "sha512-RutnB7X8c5hjq39NceArgXg28WZtZpGc3+J16ljMiYnFhKvd8hITxSWQSQ5bvldxMDU6gG5mkxl1MTQLXckVSQ==", "license": "MIT" }, + "node_modules/vue-router": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-5.4.0.tgz", + "integrity": "sha512-pFvcYcxinDHjZUlf7AzFBrT8r8Xqvv4uMHoJnjc+EqPawXQvHTkF8S4tfSmQLpriVHUGfkaKlI7Et0KRjTMTdw==", + "license": "MIT", + "dependencies": { + "@vue-macros/common": "^3.1.3", + "@vue/devtools-api": "^8.1.5", + "ast-walker-scope": "^0.10.0", + "chokidar": "^5.0.0", + "confbox": "^0.3.1", + "local-pkg": "^1.2.1", + "magic-string": "^1.4.3", + "mlly": "^1.8.2", + "muggle-string": "^0.4.1", + "nostics": "^1.3.0", + "pathe": "^2.0.3", + "picomatch": "^4.0.7", + "scule": "^1.3.0", + "tinyglobby": "^0.2.17", + "unplugin": "^3.4.0", + "unplugin-utils": "^0.3.2" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "@pinia/colada": ">=0.21.2", + "@vue/compiler-sfc": "^3.5.34 || ^4.0.0", + "pinia": "^3.0.4 || ^4.0.2", + "vite": "^7.3.0 || ^8.0.0", + "vue": "^3.5.34 || ^4.0.0" + }, + "peerDependenciesMeta": { + "@pinia/colada": { + "optional": true + }, + "@vue/compiler-sfc": { + "optional": true + }, + "pinia": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/vue-router/node_modules/confbox": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.3.1.tgz", + "integrity": "sha512-cKUSoKa8YxFZZSmraVi7onONx3amu77ngK3kGpsYHDH7drPwCRkQE1RYMPlLRrMtnciRj274XNRxcHxnKmDSnA==", + "license": "MIT" + }, + "node_modules/vue-router/node_modules/magic-string": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.4.3.tgz", + "integrity": "sha512-z12OxmaPGE0F4xlpGdjuAUckipLpQlTAuAmyGhNoD7ODpYUzvDfvtxUbjM/jwznyP178oju/KDdyi220wNJ0RQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.6.0" + } + }, + "node_modules/vue-router/node_modules/unplugin": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-3.4.0.tgz", + "integrity": "sha512-9skdIFlCsPdFV7wUfZxNsFInlW+7nJmGu2gkTu0OUhF56aXGsHab9x52/QhdJ4lC7ZDPWTxbiC4ANqVlsuaW3w==", + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "picomatch": "^4.0.7", + "webpack-virtual-modules": "^0.6.2" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@farmfe/core": "*", + "@rsbuild/core": "*", + "@rspack/core": "*", + "bun-types-no-globals": "*", + "esbuild": "*", + "rolldown": "*", + "rollup": "*", + "unloader": "*", + "vite": "*", + "webpack": "*" + }, + "peerDependenciesMeta": { + "@farmfe/core": { + "optional": true + }, + "@rsbuild/core": { + "optional": true + }, + "@rspack/core": { + "optional": true + }, + "bun-types-no-globals": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "rolldown": { + "optional": true + }, + "rollup": { + "optional": true + }, + "unloader": { + "optional": true + }, + "vite": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, "node_modules/vue-tsc": { - "version": "2.2.12", - "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-2.2.12.tgz", - "integrity": "sha512-P7OP77b2h/Pmk+lZdJ0YWs+5tJ6J2+uOQPo7tlBnY44QqQSPYvS0qVT4wqDJgwrZaLe47etJLLQRFia71GYITw==", + "version": "3.3.13", + "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-3.3.13.tgz", + "integrity": "sha512-jE4/bXdOMSHVxEGgQCqNllJy+9N3Gx6tfSAsg0yhFK/MAN6eXqISihupnDEKSCt12VVnjN2gI3AqLYcqoh5x/A==", "devOptional": true, "license": "MIT", "dependencies": { - "@volar/typescript": "2.4.15", - "@vue/language-core": "2.2.12" + "@volar/typescript": "2.4.28", + "@vue/language-core": "3.3.13" }, "bin": { "vue-tsc": "bin/vue-tsc.js" @@ -11196,16 +10304,6 @@ "typescript": ">=5.0.0" } }, - "node_modules/webidl-conversions": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", - "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=12" - } - }, "node_modules/webpack-virtual-modules": { "version": "0.6.2", "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz", @@ -11254,20 +10352,16 @@ } }, "node_modules/why-is-node-running": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", - "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-3.2.1.tgz", + "integrity": "sha512-Tb2FUhB4vUsGQlfSquQLYkApkuPAFQXGFzxWKHHumVz2dK+X1RUm/HnID4+TfIGYJ1kTcwOaCk/buYCEJr6YjQ==", "dev": true, "license": "MIT", - "dependencies": { - "siginfo": "^2.0.0", - "stackback": "0.0.2" - }, "bin": { "why-is-node-running": "cli.js" }, "engines": { - "node": ">=8" + "node": ">=20.11" } }, "node_modules/wrap-ansi": { diff --git a/src/Delibera.WebUI/package.json b/src/Delibera.WebUI/package.json index 1ab1589..297f74a 100644 --- a/src/Delibera.WebUI/package.json +++ b/src/Delibera.WebUI/package.json @@ -22,14 +22,15 @@ }, "dependencies": { "nuxt": "^4.6.1", - "vue": "^3.5.43" + "vue": "^3.5.43", + "vue-router": "^5.4.0" }, "devDependencies": { - "@nuxt/test-utils": "^3.19.0", + "@nuxt/test-utils": "^4.3.3", "@vue/test-utils": "^2.4.6", - "happy-dom": "^17.0.0", - "typescript": "^5.7.0", - "vitest": "^3.2.0", - "vue-tsc": "^2.2.0" + "happy-dom": "^20.14.6", + "typescript": "^5.9.3", + "vitest": "^5.0.3", + "vue-tsc": "^3.3.13" } -} \ No newline at end of file +} diff --git a/src/Delibera.WebUI/server/api/delibera/[...path].ts b/src/Delibera.WebUI/server/api/delibera/[...path].ts index 50554ba..44546d2 100644 --- a/src/Delibera.WebUI/server/api/delibera/[...path].ts +++ b/src/Delibera.WebUI/server/api/delibera/[...path].ts @@ -5,6 +5,18 @@ * cross-origin call would be blocked. Same-origin proxying is the fix, not a convenience. */ +// Imported from `h3`, NOT from `nuxt/server` (Nuxt 4.6+). +// +// `nuxt/server` is a deliberate server-agnostic surface: it re-declares the request helpers +// against Nuxt's own `RequestEvent` and omits the response-writing helpers entirely, so that a +// non-Nitro server builder can satisfy it. That is the wrong trade for THIS route. A BFF whose +// job is forwarding an upstream body verbatim (`sendWebResponse`) or streaming SSE chunk by +// chunk (`sendStream`) needs exactly the Nitro-bound helpers `nuxt/server` abstracts away, and +// Nuxt's `RequestEvent` is not assignable to h3's `H3Event` — mixing the two surfaces fails +// typecheck with `TS2345`, not at runtime. The app is Nitro-pinned anyway +// (`nitro.preset: 'node-server'` in nuxt.config.ts). +// +// Revisit only if the proxy can stop writing responses directly. import { defineEventHandler, getQuery, @@ -80,6 +92,24 @@ export default defineEventHandler(async (event) => { const hasBody = method !== 'GET' && method !== 'HEAD' const upstreamBody = hasBody ? encodeUpstreamBody(await readBody(event)) : undefined + // Cancel the upstream call when the browser goes away. + // + // `timeout: 0` above means nothing else ever ends this request, and a debate runs for + // 150–200 s with an SSE stream open for longer still. Navigating off the page, closing the + // tab, or simply having EventSource reconnect all end the client side while the upstream + // connection stays open and burns a server-side stream — one leaked connection per round of + // user navigation, for the whole life of the debate. + // + // `res.close` is the correct hook rather than `req.close`: the request stream ends as soon + // as the body is consumed, which for a body-less GET is immediately, so listening there + // would abort every call in this route. The guard on `writableEnded` keeps the normal + // completion path — where close fires *after* the response was fully written — from + // cancelling a request that already succeeded. + const abort = new AbortController() + event.node.res.on('close', () => { + if (!event.node.res.writableEnded) abort.abort() + }) + // A debate legitimately runs 150–200 s and an SSE stream stays open far longer, so // Nitro's default fetch timeout would cut both off mid-flight. `timeout` is a Nitro // extension to RequestInit, hence the widened type rather than a bare `as RequestInit` @@ -87,6 +117,7 @@ export default defineEventHandler(async (event) => { const upstreamInit = { method, body: upstreamBody, + signal: abort.signal, headers: buildUpstreamHeaders( { method, @@ -102,6 +133,12 @@ export default defineEventHandler(async (event) => { try { upstream = await fetch(target, upstreamInit) } catch (error) { + // The client hanging up is not an upstream failure. Reporting 502 here would describe a + // problem the user cannot act on — and the response they would receive is one nobody is + // left to read, because the socket they asked to close is the one this would be written + // to. + if (abort.signal.aborted) return + setResponseStatus(event, 502) return { title: 'Delibera API is unreachable', @@ -124,7 +161,14 @@ export default defineEventHandler(async (event) => { setResponseHeader(event, 'Content-Type', 'text/event-stream') setResponseHeader(event, 'Cache-Control', 'no-cache') setResponseHeader(event, 'X-Accel-Buffering', 'no') - await sendStream(event, upstream.body) + try { + await sendStream(event, upstream.body) + } catch (error) { + // Same reasoning as the fetch guard: an aborted stream means the browser navigated + // away or reconnected, and the resulting read error on `upstream.body` is the abort + // working as intended rather than a proxy fault worth surfacing. + if (!abort.signal.aborted) throw error + } return } diff --git a/src/Delibera.WebUI/server/api/health.get.ts b/src/Delibera.WebUI/server/api/health.get.ts index ec82e99..3f4d3ca 100644 --- a/src/Delibera.WebUI/server/api/health.get.ts +++ b/src/Delibera.WebUI/server/api/health.get.ts @@ -6,6 +6,9 @@ * honest instead of guessing at a shape the server never produces. */ +// From `h3`, not `nuxt/server` — see the note in server/api/delibera/[...path].ts. This route +// only reads a body-less GET and returns a plain object, but the `setResponseStatus` call below +// resolves to the Nitro auto-import, which is h3's, so the handler event must stay h3-typed. import { defineEventHandler } from 'h3' import { resolveApiBase, resolveUpstreamPath } from '../utils/delibera' diff --git a/src/Delibera.WebUI/server/utils/delibera.ts b/src/Delibera.WebUI/server/utils/delibera.ts index 4b46d87..32bb3de 100644 --- a/src/Delibera.WebUI/server/utils/delibera.ts +++ b/src/Delibera.WebUI/server/utils/delibera.ts @@ -12,8 +12,6 @@ * a proxy or ingress. The UI composes its own URLs from same-origin paths instead. */ -import type { H3Event } from 'h3' - /** Client-facing prefix that maps onto the server's `/api/v1` group. */ export const PROXY_PREFIX = '/api/delibera' export const API_VERSION_PREFIX = '/api/v1' diff --git a/src/Delibera.WebUI/test/sse-streaming.test.ts b/src/Delibera.WebUI/test/sse-streaming.test.ts index a766b22..f4d242b 100644 --- a/src/Delibera.WebUI/test/sse-streaming.test.ts +++ b/src/Delibera.WebUI/test/sse-streaming.test.ts @@ -39,6 +39,23 @@ let bff: ReturnType | undefined /** Bodies the upstream actually received, so POST forwarding can be asserted. */ const receivedBodies: string[] = [] +/** + * SSE connections opened against the `abort-probe` path, and how many of those the upstream + * saw close. Scoped to one path so the assertion cannot be satisfied by another test's + * connection draining. + */ +const probe = { opened: 0, closed: 0, lastClosedAt: 0 } + +/** Polls a predicate until it holds, so the test does not depend on a fixed sleep. */ +async function waitFor(predicate: () => boolean, timeoutMs: number): Promise { + const deadline = Date.now() + timeoutMs + while (Date.now() < deadline) { + if (predicate()) return true + await new Promise((r) => setTimeout(r, 100)) + } + return predicate() +} + /** * Readiness probe. * @@ -116,6 +133,17 @@ describe('SSE proxy streaming', () => { 'X-Accel-Buffering': 'no', }) + // Counted here, before any write, so `opened` proves the connection reached the + // upstream. `close` fires on both a normal end and an aborted one; only the probe path + // is counted, which is what makes the disconnect assertion unambiguous. + if (req.url?.includes('abort-probe')) { + probe.opened += 1 + res.on('close', () => { + probe.closed += 1 + probe.lastClosedAt = Date.now() + }) + } + setTimeout(() => { res.write( `event: debate-round\ndata: ${JSON.stringify({ @@ -239,4 +267,56 @@ describe('SSE proxy streaming', () => { expect(received).not.toContain('[object Object]') expect(JSON.parse(received!)).toEqual(payload) }, 20_000) + + // Regression guard for leaked server-side streams. + // + // The proxy runs every upstream call with `timeout: 0` because a debate legitimately runs + // for 150–200 s, so nothing else ever ended these requests. Closing the tab, navigating + // away or letting EventSource reconnect left the upstream connection open for the rest of + // the debate — one leaked stream per piece of user navigation, invisible from the UI, which + // had simply gone. + // + // What is asserted is the UPSTREAM side: the proxy cancelling its fetch is what closes the + // server's socket, and that is the observable consequence. Without the fix this counter + // stays at zero while every other test in this file still passes. + it('releases the upstream connection when the client disconnects', async () => { + const before = { ...probe } + + const response = await fetch( + `http://127.0.0.1:${BFF_PORT}/api/delibera/debates/abort-probe/stream`, + ) + expect(response.status).toBe(200) + + const reader = response.body!.getReader() + // Read one chunk first, so the stream is demonstrably established end-to-end and the + // disconnect below is a deliberate mid-stream hang-up rather than a race against setup. + await reader.read() + + expect( + await waitFor(() => probe.opened > before.opened, 5_000), + 'the upstream SSE connection was never opened', + ).toBe(true) + + // The client goes away. + const cancelledAt = Date.now() + await reader.cancel() + + expect( + await waitFor(() => probe.closed > before.closed, COMPLETED_AT_MS), + 'the upstream connection outlived the client entirely', + ).toBe(true) + + // The count alone is not enough, and this is why. The upstream eventually ends its own + // response after COMPLETED_AT_MS, and a connection the proxy never cancelled closes at + // exactly that moment anyway — measured at 2698 ms after the cancel on this fixture. So + // "the connection closed" passes either way, and the timing is the only thing that + // distinguishes a proxy that released the stream from one that merely outlived it. + // + // With the abort wired up the same measurement is 6 ms. The threshold sits at half the + // upstream's own lifetime, which is where the two outcomes are furthest apart. + const elapsed = probe.lastClosedAt - cancelledAt + expect(elapsed, `upstream closed ${elapsed}ms after the client disconnected`).toBeLessThan( + COMPLETED_AT_MS / 2, + ) + }, 20_000) }) \ No newline at end of file From 6e529d05e505d9f11e38285358214e687fe95fa8 Mon Sep 17 00:00:00 2001 From: Viktor Buzin Date: Sat, 10 Oct 2026 21:51:19 +0300 Subject: [PATCH 05/13] site: use the square logo in the site header The header showed the horizontal lockup. Switching it to img/delibera-square-1024x1024.png means cropping to the scales mark - x 244..778, y 170..581 - because the full lockup is 662px tall inside a 1024px canvas and the wordmark caps would render at about 3px at header size. The hero keeps the wide lockup. The crop is a wrapper with negative offsets rather than a rewritten PNG: the Pages build runs on ubuntu, where System.Drawing does not exist, so cropping at sync-assets time would work on a Windows dev machine and fail in CI. Two things the measurements turned up: - the first crop clipped the base of the scales and the left pan, because their antialiased edge pixels sit just above a strict threshold; the source background is a flat 253-254, so the threshold is 251 and the bounds carry a pixel of slack; - keeping the scale as its own custom property does not work. `calc(30px / 405)` is a *length*, not a unitless number, so multiplying it by another length is invalid at computed-value time and the box silently collapses to 0px. Dividing numbers first and multiplying by the height last stays valid. All three brand PNGs are Format24bppRgb with an opaque near-white background, so there is no alpha to blend. The mark is dark navy, which means the chip carries an explicit white plate in both themes - on a dark header the mark would otherwise be invisible. typecheck, build and verify green. --- site/README.md | 17 +++++++++++ site/app/globals.css | 62 ++++++++++++++++++++++++++++++++++++-- site/components/layout.tsx | 16 +++++----- 3 files changed, 85 insertions(+), 10 deletions(-) diff --git a/site/README.md b/site/README.md index e84ff68..af10b81 100644 --- a/site/README.md +++ b/site/README.md @@ -56,6 +56,23 @@ every template change — and this project documents cases where a claim drifted code actually did. If the parse finds no templates, the page says so instead of showing a stale list. +## Design notes + +### Header logo + +The header uses `img/delibera-square-1024x1024.png` cropped to the scales mark +(`x 244..778, y 170..581`). The full lockup — mark, `DELIBERA` wordmark and tagline — is 662px +tall in a 1024px canvas, so at header size the wordmark caps render at about 3px. The hero keeps +the wide horizontal lockup, where there is room for it. + +The crop is a wrapper with negative offsets rather than a rewritten PNG: the Pages build runs on +ubuntu, where `System.Drawing` does not exist, so cropping at `sync-assets` time would work on a +Windows dev machine and fail in CI. + +All three brand PNGs are `Format24bppRgb` with an opaque near-white background (253–254) — there is +no alpha channel to blend. The mark is dark navy, so the logo chip carries an explicit white plate +in both themes; on a dark header the mark would otherwise be invisible. + ## Commands ```bash diff --git a/site/app/globals.css b/site/app/globals.css index 484c01f..6c8ab04 100644 --- a/site/app/globals.css +++ b/site/app/globals.css @@ -129,9 +129,65 @@ a:hover { font-size: 1rem; } -.site-header__brand img { - height: 28px; - width: auto; +/* + * Header logo. + * + * The asset is a 1024x1024 canvas whose scales mark occupies x 244..778, y 170..581, with + * the DELIBERA wordmark and the tagline in separate bands below it (measured from the file; + * the bands are 172-230, 242-576, 644-756, 788-833). At header size the full 662px lockup + * would render the wordmark caps at roughly 3px — a smudge, not a logo — so the header crops + * to the mark alone and lets the hero keep the wide lockup where there is room for it. + * + * The bounds carry a pixel of slack on each side. Measured with a strict threshold the crop + * clipped the base of the scales and the left pan, because their antialiased edge pixels sit + * just above the cut-off; the source background is a flat 253-254, so the measurement + * threshold is 251. + * + * The crop is a wrapper with negative offsets rather than a rewritten PNG, because the Pages + * build runs on ubuntu where System.Drawing does not exist; rewriting the image at + * `sync-assets` time would work on this machine and fail in CI. + */ +.site-header__logo { + --logo-source-w: 1024; + --logo-mark-x: 244; + --logo-mark-y: 170; + --logo-mark-w: 535; + --logo-mark-h: 412; + --logo-mark-height: 30px; + + position: relative; + overflow: hidden; + flex: none; + width: calc(var(--logo-mark-w) / var(--logo-mark-h) * var(--logo-mark-height)); + height: var(--logo-mark-height); + border-radius: 6px; + + /* The PNG has no alpha channel and its background is opaque near-white, while the mark + itself is dark navy. On a dark header it would otherwise read as an invisible glyph on + a dark field, so the plate is explicit and identical in both themes — a deliberate chip + rather than an accident of the file. */ + background: #ffffff; +} + +/* + * The scale factor is folded into each expression rather than kept as its own custom + * property: `calc(30px / 405)` is a *length*, not a unitless number, so multiplying it by + * another length is invalid at computed-value time and silently collapses the box to 0px. + * Dividing numbers first and multiplying by the height last stays valid in every consumer. + */ +.site-header__logo img { + position: absolute; + top: calc(var(--logo-mark-y) / var(--logo-mark-h) * var(--logo-mark-height) * -1); + left: calc(var(--logo-mark-x) / var(--logo-mark-h) * var(--logo-mark-height) * -1); + width: calc(var(--logo-source-w) / var(--logo-mark-h) * var(--logo-mark-height)); + height: auto; + max-width: none; +} + +@media (prefers-color-scheme: dark) { + .site-header__logo { + box-shadow: 0 0 0 1px var(--border); + } } .site-nav { diff --git a/site/components/layout.tsx b/site/components/layout.tsx index 23cd471..dadd4f4 100644 --- a/site/components/layout.tsx +++ b/site/components/layout.tsx @@ -16,13 +16,15 @@ export function SiteHeader({ current }: { current?: string }) {
      - {/* eslint-disable-next-line @next/next/no-img-element */} - Delibera + + {/* eslint-disable-next-line @next/next/no-img-element */} + Delibera +