From 954a610c4edbb0470f281a6afa4860eb791240ef Mon Sep 17 00:00:00 2001 From: woksin Date: Tue, 25 Aug 2026 12:36:40 +0200 Subject: [PATCH 1/6] Establish an exact public documentation allowlist --- .github/workflows/docs-site.yml | 267 +- .gitignore | 9 +- web/README.md | 157 +- web/astro.config.mjs | 179 +- web/package-lock.json | 7760 +++++++++++++++++ web/package.json | 11 +- web/public-surface.json | 140 + web/scripts/check-public-surface.mjs | 191 + web/scripts/emit-doc-artifacts.mjs | 32 +- web/scripts/sync-public-content.mjs | 277 + web/src/content/docs/404.md | 22 - web/src/content/docs/adopting-cratis.mdx | 91 - web/src/content/docs/ai.mdx | 141 - .../content/docs/ai/cratis-maintainers.mdx | 113 - web/src/content/docs/ai/ecosystems.mdx | 75 - web/src/content/docs/ai/getting-started.mdx | 106 - .../docs/ai/trust-and-distribution.mdx | 98 - web/src/content/docs/api-reference.md | 36 - .../docs/architecture/CodeAnalysis/index.md | 42 - web/src/content/docs/architecture/index.md | 8 - web/src/content/docs/auth-and-compliance.mdx | 138 - web/src/content/docs/build-a-full-app.mdx | 148 - web/src/content/docs/code-analysis.mdx | 86 - web/src/content/docs/community.mdx | 72 - web/src/content/docs/compatibility.mdx | 51 - web/src/content/docs/cratis-stack.mdx | 171 - web/src/content/docs/event-modeling.mdx | 122 - web/src/content/docs/faq.mdx | 64 - web/src/content/docs/feedback.mdx | 66 - web/src/content/docs/glossary.md | 61 - web/src/content/docs/governance.mdx | 45 - web/src/content/docs/index.mdx | 255 - web/src/content/docs/learning-paths.mdx | 62 - web/src/content/docs/plugins.mdx | 123 - web/src/content/docs/production-readiness.mdx | 59 - web/src/content/docs/professional-help.mdx | 55 - web/src/content/docs/roadmap.mdx | 57 - web/src/content/docs/samples.mdx | 35 - .../docs/scenarios/camel-casing/index.md | 54 - .../docs/scenarios/chat/change-stream.md | 282 - .../content/docs/scenarios/chat/in-memory.md | 357 - .../docs/scenarios/chat/incremental-pushes.md | 386 - web/src/content/docs/scenarios/chat/index.md | 65 - .../content/docs/scenarios/chat/rabbitmq.md | 364 - web/src/content/docs/scenarios/index.md | 27 - .../scenarios/vertical-slices/automation.md | 222 - .../docs/scenarios/vertical-slices/index.md | 105 - .../scenarios/vertical-slices/state-change.md | 277 - .../scenarios/vertical-slices/state-view.md | 227 - .../scenarios/vertical-slices/translator.md | 239 - web/src/content/docs/security.mdx | 41 - web/src/content/docs/showcase.mdx | 33 - web/src/content/docs/specifications.mdx | 263 - web/src/content/docs/studio.mdx | 83 - web/src/content/docs/testing-with-cratis.mdx | 226 - web/src/content/docs/tools/lens.mdx | 72 - .../content/docs/tools/vscode-extension.mdx | 80 - web/src/content/docs/whats-new.mdx | 33 - web/src/content/docs/why-cratis.mdx | 219 - web/src/content/docs/work-with-us.mdx | 85 - web/src/public-pages/404.md | 21 + web/src/public-pages/index.mdx | 107 + 62 files changed, 8646 insertions(+), 6647 deletions(-) create mode 100644 web/package-lock.json create mode 100644 web/public-surface.json create mode 100644 web/scripts/check-public-surface.mjs create mode 100644 web/scripts/sync-public-content.mjs delete mode 100644 web/src/content/docs/404.md delete mode 100644 web/src/content/docs/adopting-cratis.mdx delete mode 100644 web/src/content/docs/ai.mdx delete mode 100644 web/src/content/docs/ai/cratis-maintainers.mdx delete mode 100644 web/src/content/docs/ai/ecosystems.mdx delete mode 100644 web/src/content/docs/ai/getting-started.mdx delete mode 100644 web/src/content/docs/ai/trust-and-distribution.mdx delete mode 100644 web/src/content/docs/api-reference.md delete mode 100644 web/src/content/docs/architecture/CodeAnalysis/index.md delete mode 100644 web/src/content/docs/architecture/index.md delete mode 100644 web/src/content/docs/auth-and-compliance.mdx delete mode 100644 web/src/content/docs/build-a-full-app.mdx delete mode 100644 web/src/content/docs/code-analysis.mdx delete mode 100644 web/src/content/docs/community.mdx delete mode 100644 web/src/content/docs/compatibility.mdx delete mode 100644 web/src/content/docs/cratis-stack.mdx delete mode 100644 web/src/content/docs/event-modeling.mdx delete mode 100644 web/src/content/docs/faq.mdx delete mode 100644 web/src/content/docs/feedback.mdx delete mode 100644 web/src/content/docs/glossary.md delete mode 100644 web/src/content/docs/governance.mdx delete mode 100644 web/src/content/docs/index.mdx delete mode 100644 web/src/content/docs/learning-paths.mdx delete mode 100644 web/src/content/docs/plugins.mdx delete mode 100644 web/src/content/docs/production-readiness.mdx delete mode 100644 web/src/content/docs/professional-help.mdx delete mode 100644 web/src/content/docs/roadmap.mdx delete mode 100644 web/src/content/docs/samples.mdx delete mode 100644 web/src/content/docs/scenarios/camel-casing/index.md delete mode 100644 web/src/content/docs/scenarios/chat/change-stream.md delete mode 100644 web/src/content/docs/scenarios/chat/in-memory.md delete mode 100644 web/src/content/docs/scenarios/chat/incremental-pushes.md delete mode 100644 web/src/content/docs/scenarios/chat/index.md delete mode 100644 web/src/content/docs/scenarios/chat/rabbitmq.md delete mode 100644 web/src/content/docs/scenarios/index.md delete mode 100644 web/src/content/docs/scenarios/vertical-slices/automation.md delete mode 100644 web/src/content/docs/scenarios/vertical-slices/index.md delete mode 100644 web/src/content/docs/scenarios/vertical-slices/state-change.md delete mode 100644 web/src/content/docs/scenarios/vertical-slices/state-view.md delete mode 100644 web/src/content/docs/scenarios/vertical-slices/translator.md delete mode 100644 web/src/content/docs/security.mdx delete mode 100644 web/src/content/docs/showcase.mdx delete mode 100644 web/src/content/docs/specifications.mdx delete mode 100644 web/src/content/docs/studio.mdx delete mode 100644 web/src/content/docs/testing-with-cratis.mdx delete mode 100644 web/src/content/docs/tools/lens.mdx delete mode 100644 web/src/content/docs/tools/vscode-extension.mdx delete mode 100644 web/src/content/docs/whats-new.mdx delete mode 100644 web/src/content/docs/why-cratis.mdx delete mode 100644 web/src/content/docs/work-with-us.mdx create mode 100644 web/src/public-pages/404.md create mode 100644 web/src/public-pages/index.mdx diff --git a/.github/workflows/docs-site.yml b/.github/workflows/docs-site.yml index 83507b57..92e84f11 100644 --- a/.github/workflows/docs-site.yml +++ b/.github/workflows/docs-site.yml @@ -1,47 +1,21 @@ -# Builds and deploys the Astro Starlight documentation site (Documentation/web). -# -# Content lives in each product repo's `Documentation/` folder. Chronicle client -# docs and snippets live in each client repo's `Documentation/` folder. This -# workflow checks each source repo out as a sibling of this repo, matching the -# local dev layout the converter expects (scripts/sync-content.mjs). -# It uses `docs-overhaul` for docs-overhaul validation and `main` for production -# builds and product-repo dispatches. -# -# The build runs on both `docs-overhaul` (validation) and `main`; product repos -# dispatch `build-docs` after docs changes on their `main` branches. The deploy job -# runs ONLY on `main`, so a docs-overhaul push validates the build without publishing. -# -# PREREQUISITES (one-time, manual): -# 1. Enable GitHub Pages for this repo with "GitHub Actions" as the source. -# 2. The checked-out product repos are public. If any become private, add a repo/org -# secret `DOCS_CHECKOUT_TOKEN`; the checkout steps fall back to `github.token`. -# 3. If serving under a sub-path (e.g. cratis.io/docs), set `base: '/docs'` in astro.config.mjs. - name: Documentation site on: + pull_request: + branches: [main] push: - branches: [docs-overhaul, main] + branches: [main] workflow_dispatch: repository_dispatch: types: [build-docs] permissions: contents: read - pages: write - id-token: write concurrency: - group: docs-site-${{ github.ref_name }} + group: docs-site-${{ github.event.pull_request.number || github.ref_name }} cancel-in-progress: true -# Product repos are checked out at docs-overhaul only for docs-overhaul validation. -# Main pushes, manual runs from main, and repository_dispatch runs use main. -env: - DOCS_REF: ${{ github.ref_name == 'docs-overhaul' && 'docs-overhaul' || 'main' }} - ELIXIR_VERSION: "1.19.5" - OTP_VERSION: "28.5" - jobs: build: runs-on: ubuntu-latest @@ -50,232 +24,93 @@ jobs: uses: actions/checkout@v4 with: path: Documentation + persist-credentials: false - - name: Checkout Samples - uses: actions/checkout@v4 - with: - repository: Cratis/Samples - ref: main - token: ${{ secrets.DOCS_CHECKOUT_TOKEN || github.token }} - path: Samples + - name: Resolve reviewed product revisions + id: revisions + working-directory: Documentation/web + env: + VALIDATE_REVIEWED_REVISIONS: ${{ github.event_name == 'pull_request' }} + run: | + if [ "$VALIDATE_REVIEWED_REVISIONS" != "true" ]; then + { + for name in chronicle arc components cli; do echo "$name=main"; done + } >> "$GITHUB_OUTPUT" + exit 0 + fi + + resolve() { + jq -er --arg repository "$1" \ + '[.routes[].source | select(.repository == $repository) | .revision] | unique | if length == 1 then .[0] else error("expected one revision") end' \ + public-surface.json + } + + { + echo "chronicle=$(resolve Chronicle)" + echo "arc=$(resolve Arc)" + echo "components=$(resolve Components)" + echo "cli=$(resolve cli)" + } >> "$GITHUB_OUTPUT" - name: Checkout Chronicle uses: actions/checkout@v4 with: repository: Cratis/Chronicle - ref: ${{ env.DOCS_REF }} - token: ${{ secrets.DOCS_CHECKOUT_TOKEN || github.token }} + ref: ${{ steps.revisions.outputs.chronicle }} path: Chronicle - - - name: Checkout Chronicle.Kotlin client docs - uses: actions/checkout@v4 - with: - repository: Cratis/Chronicle.Kotlin - ref: main - token: ${{ secrets.DOCS_CHECKOUT_TOKEN || github.token }} - path: Chronicle.Kotlin - - - name: Checkout Chronicle.Elixir client docs - uses: actions/checkout@v4 - with: - repository: Cratis/Chronicle.Elixir - ref: main - token: ${{ secrets.DOCS_CHECKOUT_TOKEN || github.token }} - path: Chronicle.Elixir - - - name: Checkout Chronicle.TypeScript client docs - uses: actions/checkout@v4 - with: - repository: Cratis/Chronicle.TypeScript - ref: main - token: ${{ secrets.DOCS_CHECKOUT_TOKEN || github.token }} - path: Chronicle.TypeScript - - - name: Checkout Chronicle.Mcp - uses: actions/checkout@v4 - with: - repository: Cratis/Chronicle.Mcp - ref: main - token: ${{ secrets.DOCS_CHECKOUT_TOKEN || github.token }} - path: Chronicle.Mcp + persist-credentials: false - name: Checkout Arc uses: actions/checkout@v4 with: repository: Cratis/Arc - ref: ${{ env.DOCS_REF }} - token: ${{ secrets.DOCS_CHECKOUT_TOKEN || github.token }} + ref: ${{ steps.revisions.outputs.arc }} path: Arc + persist-credentials: false - name: Checkout Components uses: actions/checkout@v4 with: repository: Cratis/Components - ref: ${{ env.DOCS_REF }} - token: ${{ secrets.DOCS_CHECKOUT_TOKEN || github.token }} + ref: ${{ steps.revisions.outputs.components }} path: Components + persist-credentials: false - - name: Checkout AuthProxy - uses: actions/checkout@v4 - with: - repository: Cratis/AuthProxy - ref: ${{ env.DOCS_REF }} - token: ${{ secrets.DOCS_CHECKOUT_TOKEN || github.token }} - path: AuthProxy - - - name: Checkout Fundamentals - uses: actions/checkout@v4 - with: - repository: Cratis/Fundamentals - ref: ${{ env.DOCS_REF }} - token: ${{ secrets.DOCS_CHECKOUT_TOKEN || github.token }} - path: Fundamentals - - - name: Checkout cli + - name: Checkout CLI uses: actions/checkout@v4 with: repository: Cratis/cli - ref: ${{ env.DOCS_REF }} - token: ${{ secrets.DOCS_CHECKOUT_TOKEN || github.token }} + ref: ${{ steps.revisions.outputs.cli }} path: cli + persist-credentials: false - - name: Checkout .github (Contributing) - uses: actions/checkout@v4 - with: - repository: Cratis/.github - ref: ${{ env.DOCS_REF }} - token: ${{ secrets.DOCS_CHECKOUT_TOKEN || github.token }} - path: .github - - - name: Checkout Architecture - uses: actions/checkout@v4 + - name: Setup Node + uses: actions/setup-node@v4 with: - repository: Cratis/Architecture - ref: main - token: ${{ secrets.DOCS_CHECKOUT_TOKEN || github.token }} - path: Architecture - - - name: Checkout Screenplay - uses: actions/checkout@v4 - with: - repository: Cratis/Screenplay - ref: main - token: ${{ secrets.DOCS_CHECKOUT_TOKEN || github.token }} - path: Screenplay - - - name: Checkout Prologue - uses: actions/checkout@v4 - with: - repository: Cratis/Prologue - ref: main - token: ${{ secrets.DOCS_CHECKOUT_TOKEN || github.token }} - path: Prologue - - - name: Checkout Prompter - uses: actions/checkout@v4 - with: - repository: Cratis/Prompter - ref: main - token: ${{ secrets.DOCS_CHECKOUT_TOKEN || github.token }} - path: Prompter - - - uses: actions/setup-node@v4 - with: - node-version: 23 # Components (Storybook build) requires node >=23 + node-version: 23 - name: Install dependencies working-directory: Documentation/web - run: npm ci || npm install - - - name: Setup JDK - uses: actions/setup-java@v4 - with: - java-version: '17' - distribution: 'temurin' - - - name: Setup Gradle - uses: gradle/actions/setup-gradle@v4 - with: - gradle-version: '8.13' - - - name: Setup Elixir and Erlang - uses: erlef/setup-beam@v1 - with: - elixir-version: ${{ env.ELIXIR_VERSION }} - otp-version: ${{ env.OTP_VERSION }} - - - name: Install Chronicle TypeScript dependencies - working-directory: Chronicle.TypeScript - run: | - corepack enable - yarn install --immutable - - - name: Install Chronicle Elixir dependencies - working-directory: Chronicle.Elixir/Source/chronicle - run: | - mix local.hex --force - mix local.rebar --force - mix deps.get - - - name: Build Components Storybook - working-directory: Components - run: | - corepack enable - if [ -f yarn.lock ]; then - yarn install --immutable - else - yarn install - fi - (cd Source && ../node_modules/.bin/storybook build -o ../../Documentation/web/public/storybook) - - - name: Build Arc React Storybook - working-directory: Arc/Source/JavaScript - run: | - corepack enable - if [ -f yarn.lock ]; then - yarn install --immutable - else - yarn install - fi - yarn workspace @cratis/arc.react exec storybook build -o ../../../../Documentation/web/public/storybook-arc - - - name: Setup .NET - uses: actions/setup-dotnet@v4 - with: - dotnet-version: '10.0.x' - - - name: Generate API reference (DocFX + TypeDoc) - working-directory: Documentation/web - run: | - dotnet tool install -g docfx || dotnet tool update -g docfx - npm run build:api - - - name: Lint docs - working-directory: Documentation/web - run: npm run sync && npm run lint:docs - - - name: Check Chronicle client docs - working-directory: Documentation/web - run: npm run chronicle-client-docs:check:ci - - - name: Build site - working-directory: Documentation/web - run: npm run build + run: npm ci - - name: Check internal links + - name: Validate and build the exact public surface working-directory: Documentation/web - run: npm run check:links + run: npm run check - name: Upload Pages artifact + if: ${{ github.event_name != 'pull_request' }} uses: actions/upload-pages-artifact@v3 with: path: Documentation/web/dist deploy: needs: build - # Publish to Pages only from main — docs-overhaul runs are build-only validation. - if: ${{ github.ref_name == 'main' }} + if: ${{ github.event_name != 'pull_request' && github.ref_name == 'main' }} runs-on: ubuntu-latest + permissions: + contents: read + pages: write + id-token: write environment: name: github-pages url: ${{ steps.deployment.outputs.page_url }} diff --git a/.gitignore b/.gitignore index 264c357a..ae931df3 100644 --- a/.gitignore +++ b/.gitignore @@ -283,6 +283,7 @@ __pycache__/ package-json.lock package-lock.json +!web/package-lock.json dist **/node_modules/ *.tsbuildinfo @@ -329,4 +330,10 @@ snippets.json # Storybook build output **/storybook-static/ -web/src/content/docs/architecture \ No newline at end of file + +# Exact public-surface projections (generated by sync-public-content.mjs) +web/src/content/docs/ +web/src/generated/topics.json +web/src/generated/public-routes.json +web/.public-approved/ +web/node_modules \ No newline at end of file diff --git a/web/README.md b/web/README.md index 2bcdd968..321cbba3 100644 --- a/web/README.md +++ b/web/README.md @@ -1,135 +1,96 @@ # Cratis documentation site -The Cratis documentation site, built with [Astro Starlight](https://starlight.astro.build/). It aggregates the documentation from each product repository (Chronicle, Arc, Components, CLI, Fundamentals) plus the Contributing guide, and presents them as one site. +The cratis.io site is built with Astro Starlight. Product repositories own their technical documentation; this repository owns the exact reviewed projection that may be rendered and deployed. -## Prerequisites +## Production boundary -- **Node.js 23 or newer** (CI builds on Node 23 because the Storybook toolchain requires it). -- **The product repositories checked out as siblings of this repo.** The site reads each product's `Documentation/` folder from a sibling clone, so your layout should be: +[`public-surface.json`](./public-surface.json) is the fail-closed deployment manifest. Every public route names: - ``` - / - ├── Documentation/ ← this repo (the site lives in Documentation/web) - ├── Chronicle/ - ├── Arc/ ← the ApplicationModel repo, cloned as "Arc" - ├── Components/ - ├── cli/ - ├── Fundamentals/ - ├── Samples/ ← sample catalog used by the /samples page - └── .github/ ← the Cratis/.github org repo (Contributing docs) - ``` +- one source repository and relative path; +- the exact SHA-256 of the reviewed source; +- the generated content path and public route; +- the Approved claim IDs used by the page; and +- whether search, machine exports, and sitemap inclusion are allowed. - Put each on the branch you want to preview. For the released site, use `main`: +`scripts/sync-public-content.mjs` rejects unknown repositories, unsafe paths, duplicate routes, stale hashes, Draft sources, unapproved claim IDs, and CLM-010 wording. It materializes only allowlisted content and static assets. - ```bash - for r in Chronicle Arc Components cli Fundamentals Samples .github; do (cd "$r" && git checkout main); done - ``` +The deployed site currently keeps Pagefind, `llms.txt`, `llms-full.txt`, page actions, raw Markdown mirrors, Storybooks, and generated API-reference sites disabled. Re-enable any of them only after their exact output tree is included in the same reviewed manifest boundary. - > If a product repo is missing as a sibling, the converter falls back to that product's git submodule inside this repo when one exists. Use sibling clones when you need to preview unmerged branch content. +## Required sibling layout -## Quick start +Production sync needs the allowlisted product repositories next to Documentation: -```bash -cd Documentation/web -npm install # first time only -npm run dev # converts content + starts the dev server at http://localhost:4321 +```text +/ +├── Documentation/ +├── Chronicle/ +├── Arc/ +├── Components/ +└── cli/ ``` -Open http://localhost:4321 — you should land on the Cratis home page. - -## How content is sourced - -Documentation **lives in each product repository's `Documentation/` folder** — that is the source of truth, kept next to the code it documents. A build step converts that DocFX-style Markdown into Starlight content. - -- `scripts/sync-content.mjs` reads the product `Documentation/` folders (resolved as siblings of this repo, e.g. `../Chronicle/Documentation`), converts them, and writes the result into `src/content/docs//`. -- `scripts/sync-samples.mjs` reads `Samples/samples.json` and writes `src/generated/samples.json`, which powers the cards and comparison table on `/samples/`. -- Those generated folders are **git-ignored** — never edit them by hand. Edit the source in the product repo and re-sync. -- Product sidebars are generated from each product's `toc.yml`; the topic model is written to `src/generated/topics.json` (also git-ignored) and imported by `astro.config.mjs`. -- Site-level pages that don't belong to a single product (the landing page, `why-cratis.mdx`, compatibility, community, feedback, and comparison pages) are authored directly in `src/content/docs/` and are tracked in git. +Use the branch whose source hashes are recorded in the manifest. Production uses merged `main` revisions. -The conversion handles: front matter (adds a `title`), DocFX alerts (`> [!NOTE]` → `:::note`), ``, `[!INCLUDE]`, and `.md`/`toc.yml` link fix-ups. +## Install and run -## Local development +Node.js 23 or newer is required. ```bash -# from this folder (Documentation/web) -npm install # first time only -npm run dev # sync content + start the dev server at http://localhost:4321 +cd Documentation/web +npm install +npm run dev ``` -`npm run dev` and `npm run build` both run `npm run sync` first (via the `predev`/`prebuild` hooks), so product content and the Samples catalog are always freshly synchronized. +The predev hook runs the exact production sync before starting Astro at `http://localhost:4321`. -To re-sync content without (re)starting the server — for example after editing a page in a product repo: +## Synchronization modes ```bash -npm run sync # all products -node scripts/sync-content.mjs chronicle # just one product +npm run sync # exact production manifest only +npm run sync:authoring # broad local authoring; never deploy this output ``` -## Verify the build +`sync:authoring` exists for working on product documentation before admission. Its generated pages, topics, and assets are not publication authority. + +## Verification -Before pushing, confirm the site builds cleanly: +Run the complete gate: ```bash -npm run build +npm run check ``` -A successful build: - -- converts all products (`[sync] chronicle: N pages ...` etc.), -- reports `0 broken toc entries dropped`, -- ends with `[build] N page(s) built` and `[build] Complete!`, -- generates `/llms.txt` and `/llms-full.txt`, and builds the Pagefind search index. - -Two QA scripts back this up: - -- `npm run lint:docs` — fails on non-descriptive link text (`[here]`, `[see documentation]`) and any leftover DocFX-isms (``, `[!INCLUDE]`, unconverted alerts). **Gates the build (0 errors required).** -- `npm run check:links` — verifies every internal Markdown link resolves to a real built page (Starlight does not). Run after a build. The site is currently at **zero broken internal links**, and this **gates the CI build** to keep it that way. - -`npm run check` runs build + lint + link-check together. Preview the production build locally with `npm run preview`. - -## Verify it works locally — checklist - -1. **Build + gates pass:** `npm run check` ends with `[build] Complete!`, `0 error(s)`, and `Checked … 0 broken`. Advisory style warnings may remain. -2. **Dev server serves:** `npm run dev`, open http://localhost:4321 — the landing page shows the hero and the C#/TypeScript tabs ("One feature, one slice, both ends type-safe"). -3. **Navigation:** the sidebar starts with *Why Cratis · Build a full-stack feature · Samples · API reference*, then each product (Chronicle, Arc, Components, CLI, Fundamentals, Contributing) with its sections. -4. **Search:** the top-bar search returns results (try "projection"). -5. **Diagrams render:** open *Chronicle → Architecture* — the Mermaid diagrams display. -6. **AI export:** http://localhost:4321/llms.txt lists the docs. - -## Adding or editing a page - -1. Edit (or add) the Markdown in the relevant product repo's `Documentation/` folder — for example `Chronicle/Documentation/concepts/my-page.md`. -2. Add it to that folder's `toc.yml` so it appears in the navigation. -3. Run `npm run dev` (or `npm run sync`) and check it locally. - -Follow the documentation conventions in `.ai/rules/documentation.md` (Diátaxis page types, why-first voice, descriptive link text, diagrams for concepts). - -## Working on the docs (AI assistants & contributors) - -Because the content is split across repos and the site has a few non-obvious build mechanisms, the operating knowledge is captured as **AI rules and skills** in the `Documentation` repo's `.ai/` folder. These are written once in `.ai/` and surfaced to GitHub Copilot (`.github/instructions/`, `.github/skills/`) and Claude Code (`.claude/rules/`, `.claude/skills/`) via symlinks. Product-doc rules are synced to the other Cratis repos; site-host rules are excluded by `.github/.copilot-sync-ignore` because only this repo owns `web/`. They're plain Markdown, so they double as human docs. +A successful check: -**Rules** (`.ai/rules/`): +- validates every source and static-file digest; +- materializes only the exact public routes; +- builds the Starlight site; +- verifies the claim-contained front page, title, description, canonical links, and complete Approved sentences; +- confirms CLM-010, Pagefind, machine exports, and raw Markdown mirrors are absent; +- compares internal links with the built route set; and +- runs documentation, prose, Markdown, and external-link checks. -- **`writing-cratis-docs.md`** — the content craft: the tour voice (Marten/Wolverine/Aspire style), Diátaxis page types, and how to use Starlight's authoring tools (``, ``, ``, diagrams) to achieve it. -- **`documentation-structure-and-formatting.md`** — the mechanical format so a page fits the site: frontmatter schema, heading/ToC structure, asides, code-fence languages, tables, links, file layout, trailing newline. -- **`editing-cratis-docs.md`** — where each page actually lives (which product repo), the edit → sync → verify loop, and the "never edit the generated folders" rule. -- **`astro-starlight-site.md`** — how the site is built (content conversion, navigation, the QA gate) and the hard-won gotchas. -- **`documentation-rendering-and-qa.md`** — the rendering pipeline (build-time Mermaid pre-rendering, `font-display: optional` fonts, GFM tables) and how to do headless visual/layout-shift QA. -- **`writing-correct-examples.md`** — verify every framework API against real source; the list of APIs the docs kept getting wrong. +The deployment workflow runs the same command. Pull requests validate only. Merges to `main` build and deploy the exact `dist` artifact to GitHub Pages. -**Skills** (`.ai/skills/`): +## Add or change a public page -- **`edit-cratis-docs`** — change/fix an existing page across the repos. -- **`add-cratis-docs-page`** — create a new page and wire it into the nav. -- **`qa-cratis-docs`** — screenshot pages in light/dark and diagnose layout-shift, using `scripts/screenshot.mjs`. +1. Edit the owning product documentation. +2. Complete product, claim, privacy/provenance, and public-sanitization review. +3. Add the exact source path, SHA-256, route, claim IDs, and output controls to `public-surface.json`. +4. Add navigation generated from the same manifest only. +5. Run `npm run check` from a clean sibling layout twice. +6. Merge the owning product source before the dependent Documentation manifest change. +7. Record the deployed URL/revision and withdrawal owner after publication. -For visual QA, `scripts/screenshot.mjs` drives the system Chrome to capture any page full-page in light **or** dark (no extra dependency): `node scripts/screenshot.mjs http://localhost:4321/chronicle/ /tmp/c.png dark`. +Do not edit `src/content/docs/`, `src/generated/`, or `.public-approved/` directly. They are disposable projections. -## Branding +## Site-owned pages and assets -The Cratis mark lives in `src/assets/cratis-mark-light.svg` and `src/assets/cratis-mark-dark.svg` (theme-adaptive). The accent color is set in `src/styles/cratis.css`. +- Reviewed site-owned source lives in `src/public-pages/`. +- Theme-adaptive marks live in `src/assets/` and are bundled by Astro. +- Exact static public files are copied from `public/` into `.public-approved/` only when their hashes appear in the manifest. +- Product source remains in each owning repository. -## API reference & Storybook +## Broad authoring pipeline -The .NET API reference (DocFX) and the Components Storybook are wired separately — see the Reference section. This keeps the "combine tooling" approach: Starlight for narrative docs, the right tool for each generated artifact. +`scripts/sync-content.mjs`, sample synchronization, Chronicle client-doc audits, Storybook builders, DocFX, and TypeDoc remain available for nondeployable authoring and validation. Their outputs must not be copied into the deployed public directory without explicit route/artifact admission. diff --git a/web/astro.config.mjs b/web/astro.config.mjs index f53b9ed3..7f04b8d5 100644 --- a/web/astro.config.mjs +++ b/web/astro.config.mjs @@ -5,165 +5,35 @@ import starlight from '@astrojs/starlight'; import mermaid from 'astro-mermaid'; import remarkGfm from 'remark-gfm'; import { remarkMermaidPrerender, closeBrowser } from './scripts/mermaid-prerender.mjs'; -import starlightLlmsTxt from 'starlight-llms-txt'; -import starlightPageActions from 'starlight-page-actions'; import starlightScrollToTop from 'starlight-scroll-to-top'; import starlightImageZoom from 'starlight-image-zoom'; import starlightSidebarTopics from 'starlight-sidebar-topics'; -// One topic per product, generated from each product's toc.yml by -// scripts/sync-content.mjs. starlight-sidebar-topics renders these as an icon -// rail at the top of the sidebar (the aspire.dev pattern). +// One topic per allowlisted product, generated from public-surface.json by +// sync-public-content.mjs. Production fails closed when this projection is missing. /** @typedef {{ id?: string, label: string, link?: string, icon?: string, items: any[] }} ProductTopic */ /** @type {ProductTopic[]} */ let productTopics; try { productTopics = JSON.parse(readFileSync(new URL('./src/generated/topics.json', import.meta.url), 'utf8')); -} catch { - productTopics = [ - { label: 'Chronicle', link: 'chronicle', icon: 'seti:db', items: [{ autogenerate: { directory: 'chronicle' } }] }, - { label: 'Arc', link: 'arc', icon: 'puzzle', items: [{ autogenerate: { directory: 'arc' } }] }, - { label: 'Components', link: 'components', icon: 'laptop', items: [{ autogenerate: { directory: 'components' } }] }, - ]; +} catch (error) { + throw new Error(`Approved public topics are missing. Run npm run sync before Astro: ${error}`); } -// Chronicle MCP and Prompter each ship a full toc.yml-driven sidebar of their own -// (generated like any other product topic), but in the nav they surface as -// sub-sections of the hand-authored "AI" topic below rather than getting their -// own icon-rail entry. Pull their generated topics out of the flat product list -// before building the rail. -const chronicleMcpTopic = productTopics.find((topic) => topic.id === 'chronicle-mcp'); -const prompterTopic = productTopics.find((topic) => topic.id === 'prompter'); -productTopics = productTopics.filter((topic) => topic.id !== 'chronicle-mcp' && topic.id !== 'prompter'); - -// The first topic gathers the site-level, cross-product pages (hand-authored in -// web/, not owned by any product): the "why", the capstone, samples, tools. const overviewTopic = { id: 'overview', - label: 'Cratis Stack', - link: 'cratis-stack', + label: 'Documentation', + link: '/', icon: 'open-book', - items: [ - { label: 'The Cratis Stack', slug: 'cratis-stack' }, - { label: 'Why developers choose Cratis', slug: 'why-cratis' }, - { label: 'Adopting Cratis', slug: 'adopting-cratis' }, - { - label: 'Scenarios', - collapsed: true, - items: [ - { label: 'Overview', slug: 'scenarios' }, - { label: 'Build a full-stack feature', slug: 'build-a-full-app' }, - { - label: 'Camel Casing', - collapsed: true, - items: [ - { label: 'Overview', slug: 'scenarios/camel-casing' }, - ], - }, - { - label: 'Vertical Slices', - collapsed: true, - items: [ - { label: 'Overview', slug: 'scenarios/vertical-slices' }, - { label: 'State Change', slug: 'scenarios/vertical-slices/state-change' }, - { label: 'State View', slug: 'scenarios/vertical-slices/state-view' }, - { label: 'Automation', slug: 'scenarios/vertical-slices/automation' }, - { label: 'Translator', slug: 'scenarios/vertical-slices/translator' }, - ], - }, - { - label: 'Real-Time Chat', - collapsed: true, - items: [ - { label: 'Overview', slug: 'scenarios/chat' }, - { label: 'In-Memory', slug: 'scenarios/chat/in-memory' }, - { label: 'With RabbitMQ', slug: 'scenarios/chat/rabbitmq' }, - { label: 'Frontend-Managed State', slug: 'scenarios/chat/change-stream' }, - { label: 'Incremental Pushes', slug: 'scenarios/chat/incremental-pushes' }, - ], - }, - ], - }, - { - label: 'Adopt and trust', - collapsed: true, - items: [ - { label: 'Learning paths', slug: 'learning-paths' }, - { label: 'FAQ', slug: 'faq' }, - { label: 'Version compatibility', slug: 'compatibility' }, - { label: 'Production readiness', slug: 'production-readiness' }, - { label: 'Roadmap', slug: 'roadmap' }, - { label: 'Governance', slug: 'governance' }, - { label: 'Security', slug: 'security' }, - { label: 'Professional help', slug: 'professional-help' }, - { label: 'Community and help', slug: 'community' }, - { label: 'Feedback and suggestions', slug: 'feedback' }, - ], - }, - { label: 'Studio', slug: 'studio', badge: { text: 'Soon', variant: 'tip' } }, - { label: 'Event Modeling', slug: 'event-modeling' }, - { label: 'Screenplay', link: '/screenplay/' }, - { label: 'Prologue', link: '/prologue/' }, - { - label: 'Testing', - collapsed: true, - items: [ - { label: 'Testing with Cratis', slug: 'testing-with-cratis' }, - { label: 'Specifications', slug: 'specifications' }, - ], - }, - { - label: 'Tools', - collapsed: true, - items: [ - { label: 'VS Code extension', slug: 'tools/vscode-extension' }, - { label: 'Lens', slug: 'tools/lens' }, - ], - }, - { label: 'Auth and compliance', slug: 'auth-and-compliance' }, - { label: 'Samples', slug: 'samples' }, - { label: 'Showcase and architectures', slug: 'showcase' }, - { label: "What's new", slug: 'whats-new' }, - { label: 'Glossary', slug: 'glossary' }, - { label: 'API reference', slug: 'api-reference' }, - ], + items: [], }; -// AI-related pages gathered under one topic: the hand-authored explainer pages -// plus Chronicle MCP's and Prompter's own generated sidebars, nested here as -// sub-sections rather than separate icon-rail topics. -const aiTopic = { - id: 'ai', - label: 'AI', - link: 'ai', - icon: 'star', - items: [ - { label: 'Getting started', slug: 'ai/getting-started' }, - { label: 'Ecosystem support', slug: 'ai/ecosystems' }, - { label: 'Cratis maintainers', slug: 'ai/cratis-maintainers' }, - { label: 'Trust and distribution', slug: 'ai/trust-and-distribution' }, - { label: 'Plugins', slug: 'plugins' }, - { label: 'Code analysis', slug: 'code-analysis' }, - chronicleMcpTopic - ? { label: chronicleMcpTopic.label, collapsed: true, items: chronicleMcpTopic.items } - : { label: 'Chronicle MCP server', link: '/chronicle-mcp/' }, - prompterTopic - ? { label: prompterTopic.label, collapsed: true, items: prompterTopic.items } - : { label: 'Prompter — docs assistant', link: '/prompter/' }, - ], -}; - -// Insert the AI topic right after CLI in the icon rail. -const cliIndex = productTopics.findIndex((topic) => topic.id === 'cli'); -const orderedProductTopics = cliIndex === -1 - ? [...productTopics, aiTopic] - : [...productTopics.slice(0, cliIndex + 1), aiTopic, ...productTopics.slice(cliIndex + 1)]; - -const topics = [overviewTopic, ...orderedProductTopics]; +const topics = [overviewTopic, ...productTopics]; // https://astro.build/config export default defineConfig({ site: 'https://cratis.io', + publicDir: './.public-approved', // NOTE: if the site is served under cratis.io/docs, set `base: '/docs'`. // GFM tables render in plain `.md`, but astro-mermaid injects plugins via the // (now-deprecated) `markdown.remarkPlugins` path, which leaves MDX's own `gfm` @@ -202,8 +72,11 @@ export default defineConfig({ }), starlight({ title: 'Cratis', + // Disable the generated machine index until public routes are controlled + // by an explicit Approved-claim allowlist. + pagefind: false, description: - 'Build event-sourced applications with Chronicle, Arc, and Components — the full-stack, type-safe Cratis platform.', + 'Canonical technical documentation for Chronicle, Arc, Components, and the Cratis CLI.', logo: { light: './src/assets/cratis-mark-light.svg', dark: './src/assets/cratis-mark-dark.svg', @@ -241,40 +114,22 @@ export default defineConfig({ { icon: 'discord', label: 'Discord', href: 'https://discord.gg/kt4AMpV8WV' }, { icon: 'youtube', label: 'YouTube', href: 'https://www.youtube.com/@CratisStack' }, ], - editLink: { - // Per-product content is generated from product repos; site-level pages live here. - baseUrl: 'https://github.com/cratis/Documentation/edit/main/web/', - }, plugins: [ // Product icon rail + per-product sidebar (the aspire.dev "topics" look). starlightSidebarTopics(topics, { // The splash homepage and 404 belong to no product. exclude: ['/', '/404'], - // Section-landing pages appear in the nav as collapsible groups, - // not listed leaves, so map every page slug to its topic by glob. topics: { - overview: ['/cratis-stack', '/why-cratis', '/adopting-cratis', '/scenarios', '/scenarios/**', '/learning-paths', '/faq', '/compatibility', '/production-readiness', '/roadmap', '/governance', '/security', '/work-with-us', '/professional-help', '/community', '/feedback', '/studio', '/event-modeling', '/testing-with-cratis', '/specifications', '/tools', '/tools/**', '/auth-and-compliance', '/build-a-full-app', '/samples', '/showcase', '/whats-new', '/glossary', '/api-reference'], - chronicle: ['/chronicle', '/chronicle/**'], - arc: ['/arc', '/arc/**'], - components: ['/components', '/components/**'], - authproxy: ['/authproxy', '/authproxy/**'], - cli: ['/cli', '/cli/**'], - ai: ['/ai', '/ai/**', '/plugins', '/code-analysis', '/chronicle-mcp', '/chronicle-mcp/**', '/prompter', '/prompter/**'], - fundamentals: ['/fundamentals', '/fundamentals/**'], - contributing: ['/contributing', '/contributing/**'], - architecture: ['/architecture', '/architecture/**'], - screenplay: ['/screenplay', '/screenplay/**'], - prologue: ['/prologue', '/prologue/**'], + chronicle: ['/chronicle', '/chronicle/architecture', '/chronicle/workbench'], + arc: ['/arc'], + components: ['/components'], + cli: ['/cli'], }, }), - // Per-page action row: Copy Markdown + Open in AI assistant + Share. - starlightPageActions(), // Floating "back to top" button (also on the splash homepage). starlightScrollToTop({ showTooltip: true, showOnHomepage: true }), // Click-to-zoom for screenshots and diagrams. starlightImageZoom(), - // Generates /llms.txt and /llms-full.txt so AI assistants can ground answers. - starlightLlmsTxt(), ], }), ], diff --git a/web/package-lock.json b/web/package-lock.json new file mode 100644 index 00000000..80feeb83 --- /dev/null +++ b/web/package-lock.json @@ -0,0 +1,7760 @@ +{ + "name": "web", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "web", + "version": "0.0.1", + "dependencies": { + "@astrojs/starlight": "^0.39.2", + "@astrojs/starlight-tailwind": "^5.0.0", + "@fontsource-variable/inter": "^5.2.8", + "@fontsource-variable/jetbrains-mono": "^5.2.8", + "astro": "^6.3.1", + "astro-mermaid": "^2.0.1", + "js-yaml": "^4.1.1", + "mermaid": "^11.15.0", + "sharp": "^0.34.5", + "starlight-image-zoom": "^0.14.2", + "starlight-scroll-to-top": "^1.0.1", + "starlight-sidebar-topics": "^0.7.1", + "starlight-typedoc": "^0.23.0", + "typedoc": "^0.28.19", + "typedoc-plugin-markdown": "^4.11.0" + } + }, + "node_modules/@antfu/install-pkg": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@antfu/install-pkg/-/install-pkg-1.1.0.tgz", + "integrity": "sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==", + "license": "MIT", + "dependencies": { + "package-manager-detector": "^1.3.0", + "tinyexec": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@astrojs/compiler": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@astrojs/compiler/-/compiler-4.0.0.tgz", + "integrity": "sha512-eouss7G8ygdZqHuke033VMcVw5HTZUu+PXd/h06DGDUg/jt5btPYPqh66ENWw/mU78rBrf/oeC4oqoBwMtDMNA==", + "license": "MIT" + }, + "node_modules/@astrojs/internal-helpers": { + "version": "0.10.4", + "resolved": "https://registry.npmjs.org/@astrojs/internal-helpers/-/internal-helpers-0.10.4.tgz", + "integrity": "sha512-nozZSy/mKYLqe4YrqbKtdOszedAfXYCtw3wZ0d+CAjz4GqQ4L9rl1ltIL5BlgwmYVinJg/RZ0MgGuWOdlyRZlA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.4", + "@types/mdast": "^4.0.4", + "js-yaml": "^4.3.0", + "picomatch": "^4.0.4", + "retext-smartypants": "^6.2.0", + "shiki": "^4.0.2", + "smol-toml": "^1.6.0", + "unified": "^11.0.5" + } + }, + "node_modules/@astrojs/markdown-remark": { + "version": "7.2.4", + "resolved": "https://registry.npmjs.org/@astrojs/markdown-remark/-/markdown-remark-7.2.4.tgz", + "integrity": "sha512-MvspGMynWKAjTe4/lTUdmBPHIFKNVLTCF6UlyWGogTGzNrTvjD+D4n48k7h8swxsEPKHK2TwxkZO7uoaCv1Pow==", + "license": "MIT", + "dependencies": { + "@astrojs/internal-helpers": "0.10.4", + "@astrojs/prism": "4.0.2", + "github-slugger": "^2.0.0", + "hast-util-from-html": "^2.0.3", + "hast-util-to-text": "^4.0.2", + "mdast-util-definitions": "^6.0.0", + "rehype-raw": "^7.0.0", + "rehype-stringify": "^10.0.1", + "remark-gfm": "^4.0.1", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.1.2", + "remark-smartypants": "^3.0.2", + "unified": "^11.0.5", + "unist-util-remove-position": "^5.0.0", + "unist-util-visit": "^5.1.0", + "unist-util-visit-parents": "^6.0.2", + "vfile": "^6.0.3" + } + }, + "node_modules/@astrojs/mdx": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@astrojs/mdx/-/mdx-5.0.6.tgz", + "integrity": "sha512-4dKe0ZMmqujofPNDHahzClkwinn9f8jHPcaXcgdGvPAlboD2mjzkUCofli2cBnxYAkdfhC6d50gBJ8i/cH8gHw==", + "license": "MIT", + "dependencies": { + "@astrojs/markdown-remark": "7.1.2", + "@mdx-js/mdx": "^3.1.1", + "acorn": "^8.16.0", + "es-module-lexer": "^2.0.0", + "estree-util-visit": "^2.0.0", + "hast-util-to-html": "^9.0.5", + "piccolore": "^0.1.3", + "rehype-raw": "^7.0.0", + "remark-gfm": "^4.0.1", + "remark-smartypants": "^3.0.2", + "source-map": "^0.7.6", + "unist-util-visit": "^5.1.0", + "vfile": "^6.0.3" + }, + "engines": { + "node": ">=22.12.0" + }, + "peerDependencies": { + "astro": "^6.0.0" + } + }, + "node_modules/@astrojs/mdx/node_modules/@astrojs/internal-helpers": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/@astrojs/internal-helpers/-/internal-helpers-0.9.1.tgz", + "integrity": "sha512-1pWuARqYom/TzuU3+0ZugsTrKlUydWKuULmDqSMTuonY+9IRDUEGKX/8PXQ1nBxRq3w85uGtd9q9SXfqEldMIQ==", + "license": "MIT", + "dependencies": { + "picomatch": "^4.0.4" + } + }, + "node_modules/@astrojs/mdx/node_modules/@astrojs/markdown-remark": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/@astrojs/markdown-remark/-/markdown-remark-7.1.2.tgz", + "integrity": "sha512-caXZ4Dc2St2dW8luEg22GlP0gupLdztCTQE4EzZOxW1pqWXz9mbeJEuHUkgDYcKWW8tjIHkydYDhWLVoxJ327Q==", + "license": "MIT", + "dependencies": { + "@astrojs/internal-helpers": "0.9.1", + "@astrojs/prism": "4.0.2", + "github-slugger": "^2.0.0", + "hast-util-from-html": "^2.0.3", + "hast-util-to-text": "^4.0.2", + "js-yaml": "^4.1.1", + "mdast-util-definitions": "^6.0.0", + "rehype-raw": "^7.0.0", + "rehype-stringify": "^10.0.1", + "remark-gfm": "^4.0.1", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.1.2", + "remark-smartypants": "^3.0.2", + "retext-smartypants": "^6.2.0", + "shiki": "^4.0.0", + "smol-toml": "^1.6.0", + "unified": "^11.0.5", + "unist-util-remove-position": "^5.0.0", + "unist-util-visit": "^5.1.0", + "unist-util-visit-parents": "^6.0.2", + "vfile": "^6.0.3" + } + }, + "node_modules/@astrojs/prism": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@astrojs/prism/-/prism-4.0.2.tgz", + "integrity": "sha512-KTivpmnz6lDsC6o9H4+DNm2SrE/GHzw8cNAvEJwAvUT+eoaEnn/4NtbDNfRRaxaJHdp15gf+tfHAWiXR4wB3BA==", + "license": "MIT", + "dependencies": { + "prismjs": "^1.30.0" + }, + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/@astrojs/sitemap": { + "version": "3.7.3", + "resolved": "https://registry.npmjs.org/@astrojs/sitemap/-/sitemap-3.7.3.tgz", + "integrity": "sha512-f8euLVsyeAmAkSm/1M2Kb8sL8byQmfgbvBNaHFItCheTj/IpiJYSEWVcqDHZ/yEHxiS7+w87mQkzwZaPHmk5GA==", + "license": "MIT", + "dependencies": { + "sitemap": "^9.0.0", + "stream-replace-string": "^2.0.0", + "zod": "^4.3.6" + } + }, + "node_modules/@astrojs/starlight": { + "version": "0.39.3", + "resolved": "https://registry.npmjs.org/@astrojs/starlight/-/starlight-0.39.3.tgz", + "integrity": "sha512-uvAweA2DwhmLgFVfBT9NqG38Ey14k1ck3+y78XNJbceT1pMdzxCCX69RoBajb1QzTJviufsXzSc1xswgRxJfig==", + "license": "MIT", + "dependencies": { + "@astrojs/markdown-remark": "^7.1.1", + "@astrojs/mdx": "^5.0.4", + "@astrojs/sitemap": "^3.7.2", + "@pagefind/default-ui": "^1.3.0", + "@types/hast": "^3.0.4", + "@types/js-yaml": "^4.0.9", + "@types/mdast": "^4.0.4", + "astro-expressive-code": "^0.42.0", + "bcp-47": "^2.1.0", + "hast-util-from-html": "^2.0.3", + "hast-util-select": "^6.0.4", + "hast-util-to-string": "^3.0.1", + "hastscript": "^9.0.1", + "i18next": "^26.0.7", + "js-yaml": "^4.1.1", + "klona": "^2.0.6", + "magic-string": "^0.30.21", + "mdast-util-directive": "^3.1.0", + "mdast-util-to-markdown": "^2.1.2", + "mdast-util-to-string": "^4.0.0", + "pagefind": "^1.5.2", + "rehype": "^13.0.2", + "rehype-format": "^5.0.1", + "remark-directive": "^4.0.0", + "ultrahtml": "^1.6.0", + "unified": "^11.0.5", + "unist-util-visit": "^5.1.0", + "vfile": "^6.0.3" + }, + "peerDependencies": { + "astro": "^6.0.0" + } + }, + "node_modules/@astrojs/starlight-tailwind": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@astrojs/starlight-tailwind/-/starlight-tailwind-5.0.0.tgz", + "integrity": "sha512-VivF+bWg++4ma/ffr5sgHsd/ONtGdVJIKAaRZ6jmL4yqxy7bviu59MGNi5aW3nd8psP9i/aivBTrpwGxRM1XyA==", + "license": "MIT", + "peerDependencies": { + "@astrojs/starlight": ">=0.38.0", + "tailwindcss": "^4.0.0" + } + }, + "node_modules/@astrojs/telemetry": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/@astrojs/telemetry/-/telemetry-3.3.2.tgz", + "integrity": "sha512-j8DNruA8ors99Al39RYZPJK4DC1bKkoNm93mAMuBhY9TCNC4R8n1q7ovFnJ5qhGh5Lsh7pa1gpQVpYpsJPeTHQ==", + "license": "MIT", + "dependencies": { + "ci-info": "^4.4.0", + "dset": "^3.1.4", + "is-docker": "^4.0.0", + "is-wsl": "^3.1.1", + "which-pm-runs": "^1.1.0" + }, + "engines": { + "node": "18.20.8 || ^20.3.0 || >=22.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz", + "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@braintree/sanitize-url": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-7.1.2.tgz", + "integrity": "sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA==", + "license": "MIT" + }, + "node_modules/@capsizecss/unpack": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@capsizecss/unpack/-/unpack-4.0.1.tgz", + "integrity": "sha512-CuNiSqg7+e1cO/GjffyMOm5Tt2jUF9CWHHnvQ/UkqvtkGfHdgwEC0wpmq7fkN3gxwpRnrAN0WzO3vREKmNolMQ==", + "license": "MIT", + "dependencies": { + "fontkitten": "^1.0.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@chevrotain/types": { + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-11.1.2.tgz", + "integrity": "sha512-U+HFai5+zmJCkK86QsaJtoITlboZHBqrVketcO2ROv865xfCMSFpELQoz1GkX5GzME8pTa+3kbKrZHQtI0gdbw==", + "license": "Apache-2.0" + }, + "node_modules/@clack/core": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/@clack/core/-/core-1.4.3.tgz", + "integrity": "sha512-/kr3UWNtdJfxZtPgDqUOmG2pvwlmcLGheex5yiZKdwbzZJxhV+HMNR9QNmyY5cGwTNV6LrR7Jtp+KjhUAP1qBQ==", + "license": "MIT", + "dependencies": { + "fast-wrap-ansi": "^0.2.0", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 20.12.0" + } + }, + "node_modules/@clack/prompts": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@clack/prompts/-/prompts-1.7.0.tgz", + "integrity": "sha512-y7/yvZ2TPAnR9+jnc00klvNNLkJiXFFrQA/hlLCcxA9a2A4zQIOimyFQ9XfwYKiGD1fb5GY8vbKIIgO8d5Tb2A==", + "license": "MIT", + "dependencies": { + "@clack/core": "1.4.3", + "fast-string-width": "^3.0.2", + "fast-wrap-ansi": "^0.2.0", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 20.12.0" + } + }, + "node_modules/@ctrl/tinycolor": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-4.2.0.tgz", + "integrity": "sha512-kzyuwOAQnXJNLS9PSyrk0CWk35nWJW/zl/6KvnTBMFK65gm7U1/Z5BqjxeapjZCIhQcM/DsrEmcbRwDyXyXK4A==", + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "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/@esbuild/aix-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@expressive-code/core": { + "version": "0.42.0", + "resolved": "https://registry.npmjs.org/@expressive-code/core/-/core-0.42.0.tgz", + "integrity": "sha512-MN11+9nfmaC7sYu2BZJXAXqwkBRt8t1xTSqP+Ti1NfTEskgl6xUnzDxoaiQkg0BMzpglA0pys4dpDKquP/cyIw==", + "license": "MIT", + "dependencies": { + "@ctrl/tinycolor": "^4.0.4", + "hast-util-select": "^6.0.2", + "hast-util-to-html": "^9.0.1", + "hast-util-to-text": "^4.0.1", + "hastscript": "^9.0.0", + "postcss": "^8.4.38", + "postcss-nested": "^6.0.1", + "unist-util-visit": "^5.0.0", + "unist-util-visit-parents": "^6.0.1" + } + }, + "node_modules/@expressive-code/plugin-frames": { + "version": "0.42.0", + "resolved": "https://registry.npmjs.org/@expressive-code/plugin-frames/-/plugin-frames-0.42.0.tgz", + "integrity": "sha512-XtkPm+941Uta7Y+81Acv+OA/20F1NJmJhCX6UYGKpqEIGqplNh3PTOhcURp6tcruhlzJcWcvpWy6Oigz3SrjqA==", + "license": "MIT", + "dependencies": { + "@expressive-code/core": "^0.42.0" + } + }, + "node_modules/@expressive-code/plugin-shiki": { + "version": "0.42.0", + "resolved": "https://registry.npmjs.org/@expressive-code/plugin-shiki/-/plugin-shiki-0.42.0.tgz", + "integrity": "sha512-PMKey/kLmewttAHQezL+Y5Fx3vVssfDi3+FJOYQQS2mXP3tQspFELtKKAfsXfmSXdToZYgwoO69HJndqfE+09g==", + "license": "MIT", + "dependencies": { + "@expressive-code/core": "^0.42.0", + "shiki": "^4.0.2" + } + }, + "node_modules/@expressive-code/plugin-text-markers": { + "version": "0.42.0", + "resolved": "https://registry.npmjs.org/@expressive-code/plugin-text-markers/-/plugin-text-markers-0.42.0.tgz", + "integrity": "sha512-l59lUx8fq1v5g6SpmbDjiU0+7IdfbiWnAyRmtTVSpfhyq+nZMN4UcmYyu2b9Mynhzt7Gr+O+cXyEPDNb2AVWVQ==", + "license": "MIT", + "dependencies": { + "@expressive-code/core": "^0.42.0" + } + }, + "node_modules/@fontsource-variable/inter": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@fontsource-variable/inter/-/inter-5.3.0.tgz", + "integrity": "sha512-OupL48va4JNofb97w6NYeF9S7W/kHNKM0Er8Dem5nqi4jeOLrVJDoE8tZEpnMJmtkvNbB1EIPPwHcdkF6b1oUA==", + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, + "node_modules/@fontsource-variable/jetbrains-mono": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@fontsource-variable/jetbrains-mono/-/jetbrains-mono-5.3.0.tgz", + "integrity": "sha512-F32xpS2NsGYoQi2ADSkKTgpJj7ozajsGgDJ8woTnqjmIB+dxDIqImjl4pXZVEExu8UFZ2ndhmX18EBS/hdz3Lw==", + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, + "node_modules/@gerrit0/mini-shiki": { + "version": "3.23.0", + "resolved": "https://registry.npmjs.org/@gerrit0/mini-shiki/-/mini-shiki-3.23.0.tgz", + "integrity": "sha512-bEMORlG0cqdjVyCEuU0cDQbORWX+kYCeo0kV1lbxF5bt4r7SID2l9bqsxJEM0zndaxpOUT7riCyIVEuqq/Ynxg==", + "license": "MIT", + "dependencies": { + "@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" + } + }, + "node_modules/@gerrit0/mini-shiki/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/@gerrit0/mini-shiki/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/@gerrit0/mini-shiki/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/@gerrit0/mini-shiki/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/@iconify/types": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz", + "integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==", + "license": "MIT" + }, + "node_modules/@iconify/utils": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@iconify/utils/-/utils-3.1.4.tgz", + "integrity": "sha512-b1S7B1k9ohZ+iNTi2ATxbRYG9fTrJmUT0rc46bvVnNxqNRGW7dyo/vRREwyniI5IRN2RSJHDcm+s3BjWrSAjHw==", + "license": "MIT", + "dependencies": { + "@antfu/install-pkg": "^1.1.0", + "@iconify/types": "^2.0.0", + "import-meta-resolve": "^4.2.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", + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "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.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "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.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "cpu": [ + "arm" + ], + "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.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "cpu": [ + "arm64" + ], + "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.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "cpu": [ + "ppc64" + ], + "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.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], + "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.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "cpu": [ + "s390x" + ], + "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.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "cpu": [ + "x64" + ], + "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.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "cpu": [ + "arm64" + ], + "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.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.7.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@mdx-js/mdx": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@mdx-js/mdx/-/mdx-3.1.1.tgz", + "integrity": "sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdx": "^2.0.0", + "acorn": "^8.0.0", + "collapse-white-space": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "estree-util-scope": "^1.0.0", + "estree-walker": "^3.0.0", + "hast-util-to-jsx-runtime": "^2.0.0", + "markdown-extensions": "^2.0.0", + "recma-build-jsx": "^1.0.0", + "recma-jsx": "^1.0.0", + "recma-stringify": "^1.0.0", + "rehype-recma": "^1.0.0", + "remark-mdx": "^3.0.0", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.0.0", + "source-map": "^0.7.0", + "unified": "^11.0.0", + "unist-util-position-from-estree": "^2.0.0", + "unist-util-stringify-position": "^4.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/@mermaid-js/parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@mermaid-js/parser/-/parser-1.2.1.tgz", + "integrity": "sha512-n12NohV3mrUyUL2o93IgG/ifeW9FTyeJn3zDxkhwa8MJ9Fxg3HQMlA3RiGmD/3UnJvheztkjjQAjA2T4LmUcpw==", + "license": "MIT", + "dependencies": { + "@chevrotain/types": "~11.1.2" + } + }, + "node_modules/@napi-rs/lzma-linux-x64-gnu": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", + "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@oslojs/encoding": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@oslojs/encoding/-/encoding-1.1.0.tgz", + "integrity": "sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ==", + "license": "MIT" + }, + "node_modules/@pagefind/darwin-arm64": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@pagefind/darwin-arm64/-/darwin-arm64-1.5.2.tgz", + "integrity": "sha512-MXpI+7HsAdPkvJ0gk9xj9g541BCqBZOBbdwj9g6lB5LCj6kSV6nqDSjzcAJwvOsfu0fjwvC8hQU+ecfhp+MpiQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@pagefind/darwin-x64": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@pagefind/darwin-x64/-/darwin-x64-1.5.2.tgz", + "integrity": "sha512-IojxFWMEJe0RQ7PQ3KXQsPIImNsbpPYpoZ+QUDrL8fAl/O27IX+LVLs74/UzEZy5uA2LD8Nz1AiwKr72vrkZQw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@pagefind/default-ui": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@pagefind/default-ui/-/default-ui-1.5.2.tgz", + "integrity": "sha512-pm1LMnQg8N2B3n2TnjKlhaFihpz6zTiA4HiGQ6/slKO/+8K9CAU5kcjdSSPgpuk1PMuuN4hxLipUIifnrkl3Sg==", + "license": "MIT" + }, + "node_modules/@pagefind/freebsd-x64": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@pagefind/freebsd-x64/-/freebsd-x64-1.5.2.tgz", + "integrity": "sha512-7EVzo9+0w+2cbe671BtMj10UlNo83I+HrLVLfRxO731svHRJKUfJ/mo05gU14pe9PCfpKNQT8FS3Xc/oDN6pOA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@pagefind/linux-arm64": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@pagefind/linux-arm64/-/linux-arm64-1.5.2.tgz", + "integrity": "sha512-Ovt9+K35sqzn8H3ZMXGwls4TD/wMJuvRtShHIsmUQREmaxjrDEX7gHckRCrwYJ4XE1H1p6HkLz3wukrAnsfXQw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@pagefind/linux-x64": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@pagefind/linux-x64/-/linux-x64-1.5.2.tgz", + "integrity": "sha512-V+tFqHKXhQKq/WqPBD67AFy7scn1/aZID00ws4fSDd+1daSi5UHR9VVlRrOUYKxn3VuFQYRD7lYXdZK1WED1YA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@pagefind/windows-arm64": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@pagefind/windows-arm64/-/windows-arm64-1.5.2.tgz", + "integrity": "sha512-hN9Nh90fNW61nNRCW9ZyQrAj/mD0eRvmJ8NlTUzkbuW8kIzGJUi3cxjFkEcMZ5h/8FsKWD/VcouZl4yo1F7B6g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@pagefind/windows-x64": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@pagefind/windows-x64/-/windows-x64-1.5.2.tgz", + "integrity": "sha512-Fa2Iyw7kaDRzGMfNYNUXNW2zbL5FQVDgSOcbDHdzBrDEdpqOqg8TcZ68F22ol6NJ9IGzvUdmeyZypLW5dyhqsg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/pluginutils": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.4.0.tgz", + "integrity": "sha512-MfPp06CjRLfXQ3wY0R8vJDYBy/MvVcc9OulEfR0B8Iv9ko+GCNaRZ+EpJYFl27LhKsZK0o420sYCRHCjfCgeUg==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils/node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.5.tgz", + "integrity": "sha512-jfkGfTwhQpsiSckPF8r9bU3pn3vyd72NlWaO+TgEO6WPSDnUhXzrNYCHBMOYj0ACaUgjm6eERLF+XV9a6RstoA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.5.tgz", + "integrity": "sha512-oGVqyQlxnrz9/ty89oHpU857VUHEl5/Xu4R2lS+aivCTrNnSsbiENzTnNaBsjxH0CNWGPhzHArOLFwo+oKXveA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.5.tgz", + "integrity": "sha512-bW7B8xMEq8n99Q3ieEcPRGuphurdZAaFzQc9Efyyw3FL6DZO6pMy9xhdN+kBoD7Sy05xNXSr4OyPPnpkYriS/A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.5.tgz", + "integrity": "sha512-YSwBS86QeHOGlrxJ1PSOIZSkzRL/JmKeunhc+lV6M1a6En8QuVCD/T/qIA0J4Gd2Y86RIOBYrLcOUtqGh9+/1w==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.5.tgz", + "integrity": "sha512-2fST8lILgl7cKbme/1KDdPCmbXbG+gqoV3bHp19L0ypX/3akYMBVdOunPleRCwonoLnXOZ/0F+Mt/v8POFmfcQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.5.tgz", + "integrity": "sha512-cpIxQCP9J+EVad0a6LO1kY3ZGODlk80VlI+2I96B8xMcdHZ4pLVhfQ49JFpYqjPF91FFkQWftf57YlDcTiw9yQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.5.tgz", + "integrity": "sha512-r9fGh3eFs3e/udWh5ZjXQtxiYK/xoFxQaYR/cELxac/Udkl5Th+IsFm0CX3Kl9hmUH/we7EoMpjJgeQNnE0+IA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.5.tgz", + "integrity": "sha512-xdvFdp7OM6KLJviJT2g/YuRSUjnZgGHk4RNgwIbN7X6cPugOucV60DdHXWzsBVCUdrGb6qSXnJQrrAKMmQuj3Q==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.5.tgz", + "integrity": "sha512-rRqILAndyzHzP7T9NFQrq+4HFWNhqkqkKur7eiBpfLmz01PO0JKx5Vchu3YllE4YXI/Ftgq/szrDWg5GJ0mI8g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.5.tgz", + "integrity": "sha512-Gf4X3qVMucayUvux6aXXPgXovocSFUC0rrffDuPI/S2nHhNMhjcZxsrAFYCOF350PRreW1XwzFj3CT/3bKsWCw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.5.tgz", + "integrity": "sha512-+s5qA0TNM0qm8PK/a5gt/1Hpx+NV08uSuCncvhziIlQzT6AEV2fnUQo7eBtFTFO0nA9scauvoR2HusfXmQnO4w==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.5.tgz", + "integrity": "sha512-ybb6QvWwWJCbBWqERpc8K3pYVGIrXlG8MEQ8IIuJY6Y9KdHQxoFoNyfkAOtKn1VHu3KuLidXvwrvGR1mEjeWCw==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.5.tgz", + "integrity": "sha512-nZb1DtnOyhCmYvsC8A2CwOkopVg+IS1+fPUa7rMOAXtNw5+lLCLLPqd6XAiNrGtoQKsbvIBOwsHnBH/3wnb4HQ==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.5.tgz", + "integrity": "sha512-yMbj63Sp89ryrXLWyz+sy+fYD2HpOnMCLGbe4Oa1smclFSUukdtD/BgdiHaAetJNb74URD8U4hM+qG5KVzMEkg==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.5.tgz", + "integrity": "sha512-mhoan3OJw2kYV/e1jtIdmvUZgyBFeA6zGWsOswmR0Tg19TQbowZuR+JMLID6spbbBN7Zee2ejrgmy3+FxGrIdA==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.5.tgz", + "integrity": "sha512-5ZTLmjWbb1VZdjuyhe83K/8QO0/h11midQCBP+X5OYn32ra7eOBoM0ZqtaY4nkgNsYgmdVhMYPoyVPTjUpHf3w==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.5.tgz", + "integrity": "sha512-m53kG+br6PGxOTmgBEM2DHSDs9RVjsyEbUwjJPJGTFm1grWOG8EKJggDCTb60unD4Tjby8fi7/m9XfkEWasVWg==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.5.tgz", + "integrity": "sha512-6RHPJR1g/uvdYU8uXBnfq3nlqyZCP82Fr6NHgfGoaIeSh0YEqnX/x6uA9MmJJbnSH7swqX4F+CkGdUF+6doiQA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.5.tgz", + "integrity": "sha512-xs+OXQtEXgpXT0DmA5+U3qnRZHdCST/5HRQxS8wSPZTUZN/EMWeHuSIod32LQklTBZBV9DyfncKBQ8n5V3eFdw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.5.tgz", + "integrity": "sha512-e7hD+sl3s+mcLQDZ8pbudBVsdG6r5yN4w3LqG2TJ8sQHDpblWj5lrJs/3m01Cvlxbt4x13zu5thLjgypgtkYzw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.5.tgz", + "integrity": "sha512-GiyJaCf+WpMub/17aPcKk27QMl5W6f+KhdPTjlFOn5akH5Wa/DCM9Stdx5cDfmasyKB08MqpVQ1uJE2RkkpbXg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.5.tgz", + "integrity": "sha512-+OQ8U2DdoEfXl8T4Fb18AjmEwbXMerKDKCL8yCPAYhKCEEKoul7rkbeGCBFCbAlaGaa7pmtRTpkAJM2LE/i5FA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.5.tgz", + "integrity": "sha512-KanvAZrPKbDBFwrgiU9yEVpQoox9QPV1WZOXX7HudJQY+eSlu82CtWxDU8WtuRRvtN5EGkLczkd6Y6DTcvm9wA==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.5.tgz", + "integrity": "sha512-1aC3UEWTtRl3RK3VpDJ/Tqk1XI4SLTmXIthAq6wRWo8XiSXJNd+VprJM4/1P4+i6HIaFEFlVi9sTTziniD2tOQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.5.tgz", + "integrity": "sha512-/gDJaRs4gl0NPIwqCz+6PkpmhhjRAD2j6P4rSNHBzUkO3naEx2mIU0pRle1vUNRQ7mE/+8OOeXLTv/J56FKiQg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@shikijs/core": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-4.4.3.tgz", + "integrity": "sha512-QCR4q2ZO/ILJEuwiBMel4wdcTDb1JGwfjKTxPDF6x8ixOaluPrVqIn06C99AcRPhmYlBR56d/Fb+GN58GzExpg==", + "license": "MIT", + "dependencies": { + "@shikijs/primitive": "4.4.3", + "@shikijs/types": "4.4.3", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.5", + "hast-util-to-html": "^9.0.5" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/engine-javascript": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-4.4.3.tgz", + "integrity": "sha512-FbOjFJp9VLdo1Wevs10BBtVxiTWwNLqZh5Gkhjgda/ioL15YOgeSl9n+6XMa3qRlPQzfhFNe641SrynFHYG0nQ==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "4.4.3", + "@shikijs/vscode-textmate": "^10.0.2", + "oniguruma-to-es": "^4.3.6" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/engine-oniguruma": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-4.4.3.tgz", + "integrity": "sha512-EcOQkxdxGQrc1Row/cC2c96/v1dbZqGnEVu1qTuT/MJmp6+cXCvQussowVmCv5Tqr3KuY3c7IbM6HTW3LJ1k9w==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "4.4.3", + "@shikijs/vscode-textmate": "^10.0.2" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/langs": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-4.4.3.tgz", + "integrity": "sha512-ePic0yfAJGOF83D5wBHK/00EjK65oahBYxFk5epgq33WRv7X9UuxLEV8PtR0szC0z8dl7INIpIodB99JRFlR+A==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "4.4.3" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/primitive": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/@shikijs/primitive/-/primitive-4.4.3.tgz", + "integrity": "sha512-m0wBeLDQDeIxRdUmrCPdQqfuUamDwRL5isCfYbguKD6NiaKpVbsv+3J81DyIKgNW5h4WAIIr8T4EkgQrBBxvaQ==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "4.4.3", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.5" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/themes": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-4.4.3.tgz", + "integrity": "sha512-w8UHjeUnIR965KMWJHUPXOc2mNJUnK3vpVLYLvw5IYU2mnTTJ89E24OrJDBNiJDQ0qzb0tc4l7mrIXx5cFeIyw==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "4.4.3" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/types": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-4.4.3.tgz", + "integrity": "sha512-UEJxmRR++MAGR6hugn0vgVS2W/6lWAts84FFSrnlH9sP0LNol7E5+NQ792pH8liWUhyMyjhTgSUH3k7iD7tc5g==", + "license": "MIT", + "dependencies": { + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.5" + }, + "engines": { + "node": ">=20" + } + }, + "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/@types/d3": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz", + "integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==", + "license": "MIT", + "dependencies": { + "@types/d3-array": "*", + "@types/d3-axis": "*", + "@types/d3-brush": "*", + "@types/d3-chord": "*", + "@types/d3-color": "*", + "@types/d3-contour": "*", + "@types/d3-delaunay": "*", + "@types/d3-dispatch": "*", + "@types/d3-drag": "*", + "@types/d3-dsv": "*", + "@types/d3-ease": "*", + "@types/d3-fetch": "*", + "@types/d3-force": "*", + "@types/d3-format": "*", + "@types/d3-geo": "*", + "@types/d3-hierarchy": "*", + "@types/d3-interpolate": "*", + "@types/d3-path": "*", + "@types/d3-polygon": "*", + "@types/d3-quadtree": "*", + "@types/d3-random": "*", + "@types/d3-scale": "*", + "@types/d3-scale-chromatic": "*", + "@types/d3-selection": "*", + "@types/d3-shape": "*", + "@types/d3-time": "*", + "@types/d3-time-format": "*", + "@types/d3-timer": "*", + "@types/d3-transition": "*", + "@types/d3-zoom": "*" + } + }, + "node_modules/@types/d3-array": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", + "license": "MIT" + }, + "node_modules/@types/d3-axis": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.6.tgz", + "integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-brush": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.6.tgz", + "integrity": "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-chord": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.6.tgz", + "integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-contour": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.6.tgz", + "integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==", + "license": "MIT", + "dependencies": { + "@types/d3-array": "*", + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==", + "license": "MIT" + }, + "node_modules/@types/d3-dispatch": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.7.tgz", + "integrity": "sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==", + "license": "MIT" + }, + "node_modules/@types/d3-drag": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz", + "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-dsv": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz", + "integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-fetch": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.7.tgz", + "integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==", + "license": "MIT", + "dependencies": { + "@types/d3-dsv": "*" + } + }, + "node_modules/@types/d3-force": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz", + "integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==", + "license": "MIT" + }, + "node_modules/@types/d3-format": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz", + "integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==", + "license": "MIT" + }, + "node_modules/@types/d3-geo": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.1.tgz", + "integrity": "sha512-65Emv9fQiQQqphLlRkuQ5ypPsOmWPhtBGCMv61JDPEPMvsx+gzhGf74yw1a78xFKPj6zw4AgQICJoQv0vK9M2w==", + "license": "MIT", + "dependencies": { + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-hierarchy": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz", + "integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-polygon": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.2.tgz", + "integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==", + "license": "MIT" + }, + "node_modules/@types/d3-quadtree": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz", + "integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==", + "license": "MIT" + }, + "node_modules/@types/d3-random": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.4.tgz", + "integrity": "sha512-UHYId5WTCx4L4YNel7NU00XUXXgvgpgZOvp10PuvsQENjMDXhh2RyFc0KBjO7B45ne4Ha1yVH7ii0vnzKkuzWA==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==", + "license": "MIT" + }, + "node_modules/@types/d3-selection": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz", + "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==", + "license": "MIT" + }, + "node_modules/@types/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-kVd74ta9eof3eJOvbNd1vGKS/XERRyQbT26Og63hIsvDO84cjD5gEOhsXf26w3FSoNlPVz84DOFcKv/oou+fMw==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-time-format": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.3.tgz", + "integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" + }, + "node_modules/@types/d3-transition": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz", + "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-zoom": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz", + "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==", + "license": "MIT", + "dependencies": { + "@types/d3-interpolate": "*", + "@types/d3-selection": "*" + } + }, + "node_modules/@types/debug": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", + "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==", + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "license": "MIT" + }, + "node_modules/@types/estree-jsx": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz", + "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==", + "license": "MIT", + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/@types/geojson": { + "version": "7946.0.16", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", + "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", + "license": "MIT" + }, + "node_modules/@types/hast": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.5.tgz", + "integrity": "sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/js-yaml": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz", + "integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==", + "license": "MIT" + }, + "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/mdx": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/@types/mdx/-/mdx-2.0.14.tgz", + "integrity": "sha512-T48PeuJtvLosNTPVhfnIp3i/n3a4g4Bad7YCq5k64D4u7NwDrAotikQ+5+sjtUvBmxCMlbo3dVL+C2dP0rWHzg==", + "license": "MIT" + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "license": "MIT" + }, + "node_modules/@types/nlcst": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/nlcst/-/nlcst-2.0.3.tgz", + "integrity": "sha512-vSYNSDe6Ix3q+6Z7ri9lyWqgGhJTmzRjZRqyq15N0Z/1/UnVsno9G/N40NBijoYx2seFDIl0+B2mgAb9mezUCA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/node": { + "version": "24.13.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.3.tgz", + "integrity": "sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/@types/sax": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/sax/-/sax-1.2.7.tgz", + "integrity": "sha512-rO73L89PJxeYM3s3pPPjiPgVVcymqU490g0YO5n5By0k2Erzj6tay/4lr1CHAAU4JyOWd1rpQ8bCf6cZfHU96A==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT", + "optional": true + }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.3.tgz", + "integrity": "sha512-60YRaenCQcVjYEKOcG824+DRGGIQ3VKErcBoAEDJZz5bKIs2ZG+X/H9Nk+Q6EVkwJk5QNApxbrc5QtBSwtrXAg==", + "license": "ISC" + }, + "node_modules/@upsetjs/venn.js": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@upsetjs/venn.js/-/venn.js-2.0.0.tgz", + "integrity": "sha512-WbBhLrooyePuQ1VZxrJjtLvTc4NVfpOyKx0sKqioq9bX1C1m7Jgykkn8gLrtwumBioXIqam8DLxp88Adbue6Hw==", + "license": "MIT", + "optionalDependencies": { + "d3-selection": "^3.0.0", + "d3-transition": "^3.0.1" + } + }, + "node_modules/acorn": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", + "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/anymatch/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/aria-query": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/array-iterate": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/array-iterate/-/array-iterate-2.0.1.tgz", + "integrity": "sha512-I1jXZMjAgCMmxT4qxXfPXa6SthSoE8h6gkSI9BGGNv8mP8G/v0blc+qFnZu6K42vTOiuME596QaLO0TP3Lk0xg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/astring": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/astring/-/astring-1.9.0.tgz", + "integrity": "sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==", + "license": "MIT", + "bin": { + "astring": "bin/astring" + } + }, + "node_modules/astro": { + "version": "6.4.8", + "resolved": "https://registry.npmjs.org/astro/-/astro-6.4.8.tgz", + "integrity": "sha512-KK5lX90uU9EeVaTjINyj3sy9/NFXVa59aowaqbWBDDKLXZh4rr7GwIaCFYVetE22MJtsCNFerQXn0vlCLmpP/Q==", + "license": "MIT", + "dependencies": { + "@astrojs/compiler": "^4.0.0", + "@astrojs/internal-helpers": "0.10.0", + "@astrojs/markdown-remark": "7.2.0", + "@astrojs/telemetry": "3.3.2", + "@capsizecss/unpack": "^4.0.0", + "@clack/prompts": "^1.1.0", + "@oslojs/encoding": "^1.1.0", + "@rollup/pluginutils": "^5.3.0", + "aria-query": "^5.3.2", + "axobject-query": "^4.1.0", + "ci-info": "^4.4.0", + "clsx": "^2.1.1", + "common-ancestor-path": "^2.0.0", + "cookie": "^1.1.1", + "devalue": "^5.8.1", + "diff": "^8.0.3", + "dset": "^3.1.4", + "es-module-lexer": "^2.0.0", + "esbuild": "^0.27.3", + "flattie": "^1.1.1", + "fontace": "~0.4.1", + "get-tsconfig": "5.0.0-beta.4", + "github-slugger": "^2.0.0", + "html-escaper": "3.0.3", + "http-cache-semantics": "^4.2.0", + "js-yaml": "^4.1.1", + "jsonc-parser": "^3.3.1", + "magic-string": "^0.30.21", + "magicast": "^0.5.2", + "mrmime": "^2.0.1", + "neotraverse": "^0.6.18", + "obug": "^2.1.1", + "p-limit": "^7.3.0", + "p-queue": "^9.1.0", + "package-manager-detector": "^1.6.0", + "piccolore": "^0.1.3", + "picomatch": "^4.0.4", + "rehype": "^13.0.2", + "semver": "^7.7.4", + "shiki": "^4.0.2", + "smol-toml": "^1.6.0", + "svgo": "^4.0.1", + "tinyclip": "^0.1.12", + "tinyexec": "^1.0.4", + "tinyglobby": "^0.2.15", + "ultrahtml": "^1.6.0", + "unifont": "~0.7.4", + "unist-util-visit": "^5.1.0", + "unstorage": "^1.17.5", + "vfile": "^6.0.3", + "vite": "^7.3.2", + "vitefu": "^1.1.2", + "xxhash-wasm": "^1.1.0", + "yargs-parser": "^22.0.0", + "zod": "^4.3.6" + }, + "bin": { + "astro": "bin/astro.mjs" + }, + "engines": { + "node": ">=22.12.0", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/astrodotbuild" + }, + "optionalDependencies": { + "sharp": "^0.34.0" + } + }, + "node_modules/astro-expressive-code": { + "version": "0.42.0", + "resolved": "https://registry.npmjs.org/astro-expressive-code/-/astro-expressive-code-0.42.0.tgz", + "integrity": "sha512-aiTePi2Cn0mJPYWZSzP1GcxCinX9mNtJyCCshVVPSg1yRwM7ADvFJOx0FnS440M9t65hp8JH//dc2qr22Bm4ag==", + "license": "MIT", + "dependencies": { + "rehype-expressive-code": "^0.42.0" + }, + "peerDependencies": { + "astro": "^4.0.0-beta || ^5.0.0-beta || ^3.3.0 || ^6.0.0-beta" + } + }, + "node_modules/astro-mermaid": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/astro-mermaid/-/astro-mermaid-2.1.0.tgz", + "integrity": "sha512-fFRUN0BTZh+DZhDiLyblXoO26XqJ1Rr+qK3JGgSu7OBspKHDm59jkztg/aHsrdo1vO/tIq/+xhP/vgT8Mp92XA==", + "license": "MIT", + "dependencies": { + "import-meta-resolve": "^4.2.0", + "mdast-util-to-string": "^4.0.0", + "unist-util-visit": "^5.0.0" + }, + "peerDependencies": { + "@mermaid-js/layout-elk": "^0.2.0", + "astro": ">=4", + "mermaid": "^10.0.0 || ^11.0.0" + }, + "peerDependenciesMeta": { + "@mermaid-js/layout-elk": { + "optional": true + } + } + }, + "node_modules/astro/node_modules/@astrojs/internal-helpers": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@astrojs/internal-helpers/-/internal-helpers-0.10.0.tgz", + "integrity": "sha512-Ry2R3VPeIN4uPCSA4xQc+e+vsJXkalKpEbDc07hV+a/o5Bs2N/s/uDcPJH/05L19DKh9tAy7e6JM3YZ6Cxfezw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.4", + "@types/mdast": "^4.0.4", + "js-yaml": "^4.1.1", + "picomatch": "^4.0.4", + "retext-smartypants": "^6.2.0", + "shiki": "^4.0.2", + "smol-toml": "^1.6.0", + "unified": "^11.0.5" + } + }, + "node_modules/astro/node_modules/@astrojs/markdown-remark": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@astrojs/markdown-remark/-/markdown-remark-7.2.0.tgz", + "integrity": "sha512-+YxmVQu1Bd+MFfSzjq1rOJvD9+nIOJzz5YIIhdIH01RrxRkKbyKoEgyIqP3yv51MhzMDgd79QaPv+kCVPT8vHw==", + "license": "MIT", + "dependencies": { + "@astrojs/internal-helpers": "0.10.0", + "@astrojs/prism": "4.0.2", + "github-slugger": "^2.0.0", + "hast-util-from-html": "^2.0.3", + "hast-util-to-text": "^4.0.2", + "mdast-util-definitions": "^6.0.0", + "rehype-raw": "^7.0.0", + "rehype-stringify": "^10.0.1", + "remark-gfm": "^4.0.1", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.1.2", + "remark-smartypants": "^3.0.2", + "unified": "^11.0.5", + "unist-util-remove-position": "^5.0.0", + "unist-util-visit": "^5.1.0", + "unist-util-visit-parents": "^6.0.2", + "vfile": "^6.0.3" + } + }, + "node_modules/axobject-query": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/bcp-47": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bcp-47/-/bcp-47-2.1.1.tgz", + "integrity": "sha512-KLw+H/gd2p4zly1X7Yh/qziuyae5/w/QFnvTng9eZL5fvszL7Whl3MBoWF8yxL7ksUjBfOD+OxkytiqbBpG+Fw==", + "license": "MIT", + "dependencies": { + "is-alphabetical": "^2.0.0", + "is-alphanumerical": "^2.0.0", + "is-decimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/bcp-47-match": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/bcp-47-match/-/bcp-47-match-2.0.3.tgz", + "integrity": "sha512-JtTezzbAibu8G0R9op9zb3vcWZd9JF6M0xOYGPn0fNCd7wOpRB1mU2mH9T8gaBGbAAyIIVgB2G7xG0GP98zMAQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "license": "ISC" + }, + "node_modules/brace-expansion": { + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "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": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", + "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/character-reference-invalid": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", + "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/chokidar": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", + "license": "MIT", + "dependencies": { + "readdirp": "^5.0.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/ci-info": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", + "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/collapse-white-space": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/collapse-white-space/-/collapse-white-space-2.1.0.tgz", + "integrity": "sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "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/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/common-ancestor-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/common-ancestor-path/-/common-ancestor-path-2.0.0.tgz", + "integrity": "sha512-dnN3ibLeoRf2HNC+OlCiNc5d2zxbLJXOtiZUudNFSXZrNSydxcCsSpRzXwfu7BBWCIfHPw+xTayeBvJCP/D8Ng==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">= 18" + } + }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cookie-es": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/cookie-es/-/cookie-es-1.2.3.tgz", + "integrity": "sha512-lXVyvUvrNXblMqzIRrxHb57UUVmqsSWlxqt3XIjCkUP0wDAf6uicO6KMbEgYrMNtEvWgWHwe42CKxPu9MYAnWw==", + "license": "MIT" + }, + "node_modules/cose-base": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-1.0.3.tgz", + "integrity": "sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==", + "license": "MIT", + "dependencies": { + "layout-base": "^1.0.0" + } + }, + "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/css-select": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-6.0.0.tgz", + "integrity": "sha512-rZZVSLle8v0+EY8QAkDWrKhpgt6SA5OtHsgBnsj6ZaLb5dmDVOWUDtQitd9ydxxvEjhewNudS6eTVU7uOyzvXw==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^7.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "nth-check": "^2.1.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-selector-parser": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/css-selector-parser/-/css-selector-parser-3.3.0.tgz", + "integrity": "sha512-Y2asgMGFqJKF4fq4xHDSlFYIkeVfRsm69lQC1q9kbEsH5XtnINTMrweLkjYMeaUgiXBy/uvKeO/a1JHTNnmB2g==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/mdevils" + }, + { + "type": "patreon", + "url": "https://patreon.com/mdevils" + } + ], + "license": "MIT" + }, + "node_modules/css-tree": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", + "license": "MIT", + "dependencies": { + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/css-what": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-7.0.0.tgz", + "integrity": "sha512-wD5oz5xibMOPHzy13CyGmogB3phdvcDaB5t0W/Nr5Z2O/agcB8YwOz6e2Lsp10pNDzBoDO9nVa3RGs/2BttpHQ==", + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csso": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/csso/-/csso-5.0.5.tgz", + "integrity": "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==", + "license": "MIT", + "dependencies": { + "css-tree": "~2.2.0" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/csso/node_modules/css-tree": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.2.1.tgz", + "integrity": "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==", + "license": "MIT", + "dependencies": { + "mdn-data": "2.0.28", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/csso/node_modules/mdn-data": { + "version": "2.0.28", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.28.tgz", + "integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==", + "license": "CC0-1.0" + }, + "node_modules/cytoscape": { + "version": "3.34.1", + "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.34.1.tgz", + "integrity": "sha512-Lr0RvH9H75y9ar8h9Toy6u4lxRSCcxUq+hHcQ26sVWo6BnaQp1gwEZOYqwuYTZhyW7npyKnNLP8oJ2p1/3OZ7g==", + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/cytoscape-cose-bilkent": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cytoscape-cose-bilkent/-/cytoscape-cose-bilkent-4.1.0.tgz", + "integrity": "sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==", + "license": "MIT", + "dependencies": { + "cose-base": "^1.0.0" + }, + "peerDependencies": { + "cytoscape": "^3.2.0" + } + }, + "node_modules/cytoscape-fcose": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cytoscape-fcose/-/cytoscape-fcose-2.2.0.tgz", + "integrity": "sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ==", + "license": "MIT", + "dependencies": { + "cose-base": "^2.2.0" + }, + "peerDependencies": { + "cytoscape": "^3.2.0" + } + }, + "node_modules/cytoscape-fcose/node_modules/cose-base": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-2.2.0.tgz", + "integrity": "sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==", + "license": "MIT", + "dependencies": { + "layout-base": "^2.0.0" + } + }, + "node_modules/cytoscape-fcose/node_modules/layout-base": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-2.0.1.tgz", + "integrity": "sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg==", + "license": "MIT" + }, + "node_modules/d3": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/d3/-/d3-7.9.0.tgz", + "integrity": "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==", + "license": "ISC", + "dependencies": { + "d3-array": "3", + "d3-axis": "3", + "d3-brush": "3", + "d3-chord": "3", + "d3-color": "3", + "d3-contour": "4", + "d3-delaunay": "6", + "d3-dispatch": "3", + "d3-drag": "3", + "d3-dsv": "3", + "d3-ease": "3", + "d3-fetch": "3", + "d3-force": "3", + "d3-format": "3", + "d3-geo": "3", + "d3-hierarchy": "3", + "d3-interpolate": "3", + "d3-path": "3", + "d3-polygon": "3", + "d3-quadtree": "3", + "d3-random": "3", + "d3-scale": "4", + "d3-scale-chromatic": "3", + "d3-selection": "3", + "d3-shape": "3", + "d3-time": "3", + "d3-time-format": "4", + "d3-timer": "3", + "d3-transition": "3", + "d3-zoom": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-axis": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz", + "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-brush": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz", + "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "3", + "d3-transition": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-chord": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz", + "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==", + "license": "ISC", + "dependencies": { + "d3-path": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-contour": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz", + "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==", + "license": "ISC", + "dependencies": { + "d3-array": "^3.2.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", + "license": "ISC", + "dependencies": { + "delaunator": "5" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz", + "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", + "license": "ISC", + "dependencies": { + "commander": "7", + "iconv-lite": "0.6", + "rw": "1" + }, + "bin": { + "csv2json": "bin/dsv2json.js", + "csv2tsv": "bin/dsv2dsv.js", + "dsv2dsv": "bin/dsv2dsv.js", + "dsv2json": "bin/dsv2json.js", + "json2csv": "bin/json2dsv.js", + "json2dsv": "bin/json2dsv.js", + "json2tsv": "bin/json2dsv.js", + "tsv2csv": "bin/dsv2dsv.js", + "tsv2json": "bin/dsv2json.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-fetch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz", + "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==", + "license": "ISC", + "dependencies": { + "d3-dsv": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-force": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz", + "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-quadtree": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", + "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-geo": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz", + "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2.5.0 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-hierarchy": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", + "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-polygon": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz", + "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-quadtree": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz", + "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-random": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz", + "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-sankey": { + "version": "0.12.3", + "resolved": "https://registry.npmjs.org/d3-sankey/-/d3-sankey-0.12.3.tgz", + "integrity": "sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-array": "1 - 2", + "d3-shape": "^1.2.0" + } + }, + "node_modules/d3-sankey/node_modules/d3-array": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.12.1.tgz", + "integrity": "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==", + "license": "BSD-3-Clause", + "dependencies": { + "internmap": "^1.0.0" + } + }, + "node_modules/d3-sankey/node_modules/d3-path": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz", + "integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==", + "license": "BSD-3-Clause" + }, + "node_modules/d3-sankey/node_modules/d3-shape": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz", + "integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-path": "1" + } + }, + "node_modules/d3-sankey/node_modules/internmap": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-1.0.1.tgz", + "integrity": "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==", + "license": "ISC" + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-interpolate": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/dagre-d3-es": { + "version": "7.0.14", + "resolved": "https://registry.npmjs.org/dagre-d3-es/-/dagre-d3-es-7.0.14.tgz", + "integrity": "sha512-P4rFMVq9ESWqmOgK+dlXvOtLwYg0i7u0HBGJER0LZDJT2VHIPAMZ/riPxqJceWMStH5+E61QxFra9kIS3AqdMg==", + "license": "MIT", + "dependencies": { + "d3": "^7.9.0", + "lodash-es": "^4.17.21" + } + }, + "node_modules/dayjs": { + "version": "1.11.23", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.23.tgz", + "integrity": "sha512-QDTCU0M0MxR3hQfnlDJfwekQiaanm1ubOD231u73WBckQ/fsamwRLiE2GBz6D3a/xF1NgfiDLJjXBa1hYOYTtQ==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decode-named-character-reference": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz", + "integrity": "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==", + "license": "MIT", + "dependencies": { + "character-entities": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/defu": { + "version": "6.1.7", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.7.tgz", + "integrity": "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==", + "license": "MIT" + }, + "node_modules/delaunator": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.1.0.tgz", + "integrity": "sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ==", + "license": "ISC", + "dependencies": { + "robust-predicates": "^3.0.2" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/destr": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz", + "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==", + "license": "MIT" + }, + "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", + "engines": { + "node": ">=8" + } + }, + "node_modules/devalue": { + "version": "5.9.1", + "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.9.1.tgz", + "integrity": "sha512-+17vil3EVQRzvtDJSFuTWEb8XJRvXqAiV3qZyQWD398QeXUa6CxsUyMdD1fxzEhUrd4FojitFz7lhIHBTlV4fw==", + "license": "MIT" + }, + "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/diff": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.4.tgz", + "integrity": "sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/direction": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/direction/-/direction-2.0.1.tgz", + "integrity": "sha512-9S6m9Sukh1cZNknO1CWAr2QAWsbKLafQiyM5gZ7VgXHeuaoUwffKN4q6NC4A/Mf9iiPlOXQEKW/Mv/mh9/3YFA==", + "license": "MIT", + "bin": { + "direction": "cli.js" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/dom-serializer/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/dompurify": { + "version": "3.4.14", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.14.tgz", + "integrity": "sha512-dVoH9z+MY+C9IilgGCk3YfFqjLi3fChm2OiKJMzh6axrJ5qwxqWaZamgmHrpv22CN/KdbZJuGEGgfQoL00LTdg==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dset": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/dset/-/dset-3.1.4.tgz", + "integrity": "sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-module-lexer": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.2.tgz", + "integrity": "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==", + "license": "MIT" + }, + "node_modules/es-toolkit": { + "version": "1.51.0", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.51.0.tgz", + "integrity": "sha512-zC2lQGkM7QX+Gm6iM3+WIdZJzthsEd14LvRNJneSO2hzyz/zNBENR8+YXWo1cKxgPBtV6ksPYHELbcwBRzmdCw==", + "license": "MIT", + "workspaces": [ + "docs", + "benchmarks", + "tests/types", + "tests/browser-compat" + ] + }, + "node_modules/esast-util-from-estree": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/esast-util-from-estree/-/esast-util-from-estree-2.0.0.tgz", + "integrity": "sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "devlop": "^1.0.0", + "estree-util-visit": "^2.0.0", + "unist-util-position-from-estree": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/esast-util-from-js": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/esast-util-from-js/-/esast-util-from-js-2.0.1.tgz", + "integrity": "sha512-8Ja+rNJ0Lt56Pcf3TAmpBZjmx8ZcK5Ts4cAzIOjsjevg9oSXJnl6SUQ2EevU8tv3h6ZLWmoKL5H4fgWvdvfETw==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "acorn": "^8.0.0", + "esast-util-from-estree": "^2.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/esbuild": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" + } + }, + "node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/estree-util-attach-comments": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/estree-util-attach-comments/-/estree-util-attach-comments-3.0.0.tgz", + "integrity": "sha512-cKUwm/HUcTDsYh/9FgnuFqpfquUbwIqwKM26BVCGDPVgvaCl/nDCCjUfiLlx6lsEZ3Z4RFxNbOQ60pkaEwFxGw==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-util-build-jsx": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/estree-util-build-jsx/-/estree-util-build-jsx-3.0.1.tgz", + "integrity": "sha512-8U5eiL6BTrPxp/CHbs2yMgP8ftMhR5ww1eIKoWRMlqvltHF8fZn5LRDvTKuxD3DUn+shRbLGqXemcP51oFCsGQ==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "estree-walker": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-util-is-identifier-name": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz", + "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-util-scope": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/estree-util-scope/-/estree-util-scope-1.0.0.tgz", + "integrity": "sha512-2CAASclonf+JFWBNJPndcOpA8EMJwa0Q8LUFJEKqXLW6+qBvbFZuF5gItbQOs/umBUkjviCSDCbBwU2cXbmrhQ==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-util-to-js": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/estree-util-to-js/-/estree-util-to-js-2.0.0.tgz", + "integrity": "sha512-WDF+xj5rRWmD5tj6bIqRi6CkLIXbbNQUcxQHzGysQzvHmdYG2G7p/Tf0J0gpxGgkeMZNTIjT/AoSvC9Xehcgdg==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "astring": "^1.8.0", + "source-map": "^0.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-util-visit": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/estree-util-visit/-/estree-util-visit-2.0.0.tgz", + "integrity": "sha512-m5KgiH85xAhhW8Wta0vShLcUvOsh3LLPI2YVwcbio1l7E09NTLL1EyMZFM1OyWowoH0skScNbhOPl4kcBgzTww==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "license": "MIT" + }, + "node_modules/expressive-code": { + "version": "0.42.0", + "resolved": "https://registry.npmjs.org/expressive-code/-/expressive-code-0.42.0.tgz", + "integrity": "sha512-V5DtJLEKuj4wf9O6IRtPtRObkMVy2ggR+S0MdjrTw6m58krZnDioyhW1si3Y04c5YPeooP4nd85Yq9NwEVHS4g==", + "license": "MIT", + "dependencies": { + "@expressive-code/core": "^0.42.0", + "@expressive-code/plugin-frames": "^0.42.0", + "@expressive-code/plugin-shiki": "^0.42.0", + "@expressive-code/plugin-text-markers": "^0.42.0" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, + "node_modules/fast-string-truncated-width": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fast-string-truncated-width/-/fast-string-truncated-width-3.0.3.tgz", + "integrity": "sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==", + "license": "MIT" + }, + "node_modules/fast-string-width": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/fast-string-width/-/fast-string-width-3.0.2.tgz", + "integrity": "sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==", + "license": "MIT", + "dependencies": { + "fast-string-truncated-width": "^3.0.2" + } + }, + "node_modules/fast-wrap-ansi": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/fast-wrap-ansi/-/fast-wrap-ansi-0.2.2.tgz", + "integrity": "sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q==", + "license": "MIT", + "dependencies": { + "fast-string-width": "^3.0.2" + } + }, + "node_modules/fastdom": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/fastdom/-/fastdom-1.0.12.tgz", + "integrity": "sha512-LB+xjSTEbjHE1cWsxu+tN2Xqr1kpi+V9aADI7sVM5ZMaXyYGPHULQMzpJMYqOTULK/73pUkWVzzObFRBkPr+hg==", + "license": "MIT", + "dependencies": { + "strictdom": "^1.0.1" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/flattie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/flattie/-/flattie-1.1.1.tgz", + "integrity": "sha512-9UbaD6XdAL97+k/n+N7JwX46K/M6Zc6KcFYskrYL8wbBV/Uyk0CTAMY0VT+qiK5PM7AIc9aTWYtq65U7T+aCNQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/fontace": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/fontace/-/fontace-0.4.1.tgz", + "integrity": "sha512-lDMvbAzSnHmbYMTEld5qdtvNH2/pWpICOqpean9IgC7vUbUJc3k+k5Dokp85CegamqQpFbXf0rAVkbzpyTA8aw==", + "license": "MIT", + "dependencies": { + "fontkitten": "^1.0.2" + } + }, + "node_modules/fontkitten": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/fontkitten/-/fontkitten-1.0.3.tgz", + "integrity": "sha512-Wp1zXWPVUPBmfoa3Cqc9ctaKuzKAV6uLstRqlR56kSjplf5uAce+qeyYym7F+PHbGTk+tCEdkCW6RD7DX/gBZw==", + "license": "MIT", + "dependencies": { + "tiny-inflate": "^1.0.3" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-tsconfig": { + "version": "5.0.0-beta.4", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-5.0.0-beta.4.tgz", + "integrity": "sha512-7nF7C9fIPFEMHgEMEfgIlO9wDdZ8CyHw27rWciFZfHvHDReIiPhsYuzPRXsfvBCqFy1l8RRyyWV7QLM+ZhUJsQ==", + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "engines": { + "node": ">=20.20.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "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/h3": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/h3/-/h3-1.15.11.tgz", + "integrity": "sha512-L3THSe2MPeBwgIZVSH5zLdBBU90TOxarvhK9d04IDY2AmVS8j2Jz2LIWtwsGOU3lu2I5jCN7FNvVfY2+XyF+mg==", + "license": "MIT", + "dependencies": { + "cookie-es": "^1.2.3", + "crossws": "^0.3.5", + "defu": "^6.1.6", + "destr": "^2.0.5", + "iron-webcrypto": "^1.2.1", + "node-mock-http": "^1.0.4", + "radix3": "^1.1.2", + "ufo": "^1.6.3", + "uncrypto": "^0.1.3" + } + }, + "node_modules/hachure-fill": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/hachure-fill/-/hachure-fill-0.5.2.tgz", + "integrity": "sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==", + "license": "MIT" + }, + "node_modules/hast-util-embedded": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-embedded/-/hast-util-embedded-3.0.0.tgz", + "integrity": "sha512-naH8sld4Pe2ep03qqULEtvYr7EjrLK2QHY8KJR6RJkTUjPGObe1vnx585uzem2hGra+s1q08DZZpfgDVYRbaXA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-is-element": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-format": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/hast-util-format/-/hast-util-format-1.1.0.tgz", + "integrity": "sha512-yY1UDz6bC9rDvCWHpx12aIBGRG7krurX0p0Fm6pT547LwDIZZiNr8a+IHDogorAdreULSEzP82Nlv5SZkHZcjA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-embedded": "^3.0.0", + "hast-util-minify-whitespace": "^1.0.0", + "hast-util-phrasing": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "html-whitespace-sensitive-tag-names": "^3.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-from-html": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hast-util-from-html/-/hast-util-from-html-2.0.3.tgz", + "integrity": "sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "devlop": "^1.1.0", + "hast-util-from-parse5": "^8.0.0", + "parse5": "^7.0.0", + "vfile": "^6.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-from-parse5": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-8.0.3.tgz", + "integrity": "sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "devlop": "^1.0.0", + "hastscript": "^9.0.0", + "property-information": "^7.0.0", + "vfile": "^6.0.0", + "vfile-location": "^5.0.0", + "web-namespaces": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-has-property": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-has-property/-/hast-util-has-property-3.0.0.tgz", + "integrity": "sha512-MNilsvEKLFpV604hwfhVStK0usFY/QmM5zX16bo7EjnAEGofr5YyI37kzopBlZJkHD4t887i+q/C8/tr5Q94cA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-is-body-ok-link": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/hast-util-is-body-ok-link/-/hast-util-is-body-ok-link-3.0.1.tgz", + "integrity": "sha512-0qpnzOBLztXHbHQenVB8uNuxTnm/QBFUOmdOSsEn7GnBtyY07+ENTWVFBAnXd/zEgd9/SUG3lRY7hSIBWRgGpQ==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-is-element": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-is-element/-/hast-util-is-element-3.0.0.tgz", + "integrity": "sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-minify-whitespace": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/hast-util-minify-whitespace/-/hast-util-minify-whitespace-1.0.1.tgz", + "integrity": "sha512-L96fPOVpnclQE0xzdWb/D12VT5FabA7SnZOUMtL1DbXmYiHJMXZvFkIZfiMmTCNJHUeO2K9UYNXoVyfz+QHuOw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-embedded": "^3.0.0", + "hast-util-is-element": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-parse-selector": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz", + "integrity": "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-phrasing": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/hast-util-phrasing/-/hast-util-phrasing-3.0.1.tgz", + "integrity": "sha512-6h60VfI3uBQUxHqTyMymMZnEbNl1XmEGtOxxKYL7stY2o601COo62AWAYBQR9lZbYXYSBoxag8UpPRXK+9fqSQ==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-embedded": "^3.0.0", + "hast-util-has-property": "^3.0.0", + "hast-util-is-body-ok-link": "^3.0.0", + "hast-util-is-element": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-raw": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/hast-util-raw/-/hast-util-raw-9.1.0.tgz", + "integrity": "sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "@ungap/structured-clone": "^1.0.0", + "hast-util-from-parse5": "^8.0.0", + "hast-util-to-parse5": "^8.0.0", + "html-void-elements": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "parse5": "^7.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0", + "web-namespaces": "^2.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-select": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/hast-util-select/-/hast-util-select-6.0.4.tgz", + "integrity": "sha512-RqGS1ZgI0MwxLaKLDxjprynNzINEkRHY2i8ln4DDjgv9ZhcYVIHN9rlpiYsqtFwrgpYU361SyWDQcGNIBVu3lw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "bcp-47-match": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "css-selector-parser": "^3.0.0", + "devlop": "^1.0.0", + "direction": "^2.0.0", + "hast-util-has-property": "^3.0.0", + "hast-util-to-string": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "nth-check": "^2.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-estree": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/hast-util-to-estree/-/hast-util-to-estree-3.1.3.tgz", + "integrity": "sha512-48+B/rJWAp0jamNbAAf9M7Uf//UVqAoMmgXhBdxTDJLGKY+LRnZ99qcG+Qjl5HfMpYNzS5v4EAwVEF34LeAj7w==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-attach-comments": "^3.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "style-to-js": "^1.0.0", + "unist-util-position": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "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-to-jsx-runtime": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz", + "integrity": "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "style-to-js": "^1.0.0", + "unist-util-position": "^5.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-parse5": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/hast-util-to-parse5/-/hast-util-to-parse5-8.0.1.tgz", + "integrity": "sha512-MlWT6Pjt4CG9lFCjiz4BH7l9wmrMkfkJYCxFwKQic8+RTZgWPuWxwAfjJElsXkex7DJjfSJsQIt931ilUgmwdA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "web-namespaces": "^2.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-string": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/hast-util-to-string/-/hast-util-to-string-3.0.1.tgz", + "integrity": "sha512-XelQVTDWvqcl3axRfI0xSeoVKzyIFPwsAGSLIsKdJKQMXDYJS4WYrBNF/8J7RdhIcFI2BOHgAifggsvsxp/3+A==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-text": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/hast-util-to-text/-/hast-util-to-text-4.0.2.tgz", + "integrity": "sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "hast-util-is-element": "^3.0.0", + "unist-util-find-after": "^5.0.0" + }, + "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/hastscript": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-9.0.1.tgz", + "integrity": "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-parse-selector": "^4.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/html-escaper": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-3.0.3.tgz", + "integrity": "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==", + "license": "MIT" + }, + "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/html-whitespace-sensitive-tag-names": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-whitespace-sensitive-tag-names/-/html-whitespace-sensitive-tag-names-3.0.1.tgz", + "integrity": "sha512-q+310vW8zmymYHALr1da4HyXUQ0zgiIwIicEfotYPWGN0OJVEN/58IJ3A4GBYcEq3LGAZqKb+ugvP0GNB9CEAA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "license": "BSD-2-Clause" + }, + "node_modules/i18next": { + "version": "26.4.0", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-26.4.0.tgz", + "integrity": "sha512-rsmK5bFqsD1AetSFSIa43wtNR4WpvvH4p0tLEsTxkC7QTrfdFm06nbQ95bh8Og4wwaCnUEcm9DVYL2cgxitiQg==", + "funding": [ + { + "type": "individual", + "url": "https://www.locize.com/i18next" + }, + { + "type": "individual", + "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" + }, + { + "type": "individual", + "url": "https://www.locize.com" + } + ], + "license": "MIT", + "peerDependencies": { + "typescript": "^5 || ^6 || ^7" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/import-meta-resolve": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.2.0.tgz", + "integrity": "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/inline-style-parser": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz", + "integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==", + "license": "MIT" + }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/iron-webcrypto": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/iron-webcrypto/-/iron-webcrypto-1.2.1.tgz", + "integrity": "sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/brc-dd" + } + }, + "node_modules/is-alphabetical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", + "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-alphanumerical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", + "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", + "license": "MIT", + "dependencies": { + "is-alphabetical": "^2.0.0", + "is-decimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-decimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", + "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-docker": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-4.0.0.tgz", + "integrity": "sha512-LHE+wROyG/Y/0ZnbktRCoTix2c1RhgWaZraMZ8o1Q7zCh0VSrICJQO5oqIIISrcSBtrXv0o233w1IYwsWCjTzA==", + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-hexadecimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", + "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "license": "MIT", + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-inside-container/node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-wsl": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz", + "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==", + "license": "MIT", + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/js-yaml": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz", + "integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsonc-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", + "license": "MIT" + }, + "node_modules/katex": { + "version": "0.16.47", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.47.tgz", + "integrity": "sha512-Eeo8Ys1doU1z+x8AZsPpQu+p/QcZBI5PeOo7QGQdy2x2m0MU/hYagBbGOmXwr5KVbEfVuWv9LpnQWeehogurjg==", + "funding": [ + "https://opencollective.com/katex", + "https://github.com/sponsors/katex" + ], + "license": "MIT", + "dependencies": { + "commander": "^8.3.0" + }, + "bin": { + "katex": "cli.js" + } + }, + "node_modules/katex/node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/khroma": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/khroma/-/khroma-2.1.0.tgz", + "integrity": "sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==" + }, + "node_modules/klona": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/klona/-/klona-2.0.6.tgz", + "integrity": "sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/layout-base": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-1.0.2.tgz", + "integrity": "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==", + "license": "MIT" + }, + "node_modules/linkify-it": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.2.tgz", + "integrity": "sha512-ONTm2jCMAVZjgQa/Fy1kScXsuOoF5NPTsoFBdE1KVIZ2vAh/r9+Bqo+0jINCBYnavTPQZz38QzFTme79ENoN3Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/markdown-it" + } + ], + "license": "MIT", + "dependencies": { + "uc.micro": "^2.0.0" + } + }, + "node_modules/lodash-es": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz", + "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==", + "license": "MIT" + }, + "node_modules/longest-streak": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", + "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/lru-cache": { + "version": "11.5.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", + "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/lunr": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.9.tgz", + "integrity": "sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==", + "license": "MIT" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/magicast": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.4.tgz", + "integrity": "sha512-llBEhWm1SacoRwgHUoQJYtwp4PBLF4faQi5TCpIGyGs9n4y5+juI0tDgyKIfpqxckRHaHzouUEph3THklWh03w==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "source-map-js": "^1.2.1" + } + }, + "node_modules/markdown-extensions": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/markdown-extensions/-/markdown-extensions-2.0.0.tgz", + "integrity": "sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q==", + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/markdown-it": { + "version": "14.3.0", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.3.0.tgz", + "integrity": "sha512-RCEsPjR+sr0x+AuYp601tKTkgFG4YEPLCzHST3cQ/fhlJkqAkz1L2/Qbp1j9qw5SBwQHFBoW8+hoN5xssOF0Tw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/markdown-it" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1", + "entities": "^4.5.0", + "linkify-it": "^5.0.2", + "mdurl": "^2.0.0", + "punycode.js": "^2.3.1", + "uc.micro": "^2.1.0" + }, + "bin": { + "markdown-it": "bin/markdown-it.mjs" + } + }, + "node_modules/markdown-it/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/markdown-table": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz", + "integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/marked": { + "version": "16.4.2", + "resolved": "https://registry.npmjs.org/marked/-/marked-16.4.2.tgz", + "integrity": "sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/mdast-util-definitions": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-definitions/-/mdast-util-definitions-6.0.0.tgz", + "integrity": "sha512-scTllyX6pnYNZH/AIp/0ePz6s4cZtARxImwoPJ7kS42n+MnVsI4XbnG6d4ibehRIldYMWM2LD7ImQblVhUejVQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "unist-util-visit": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-directive": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-directive/-/mdast-util-directive-3.1.0.tgz", + "integrity": "sha512-I3fNFt+DHmpWCYAT7quoM6lHf9wuqtI+oCOfvILnoicNIqjh5E3dEJWiXuYME2gNe8vl1iMQwyUHa7bgFmak6Q==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "parse-entities": "^4.0.0", + "stringify-entities": "^4.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-find-and-replace": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz", + "integrity": "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "escape-string-regexp": "^5.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/mdast-util-from-markdown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.3.tgz", + "integrity": "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz", + "integrity": "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==", + "license": "MIT", + "dependencies": { + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-gfm-autolink-literal": "^2.0.0", + "mdast-util-gfm-footnote": "^2.0.0", + "mdast-util-gfm-strikethrough": "^2.0.0", + "mdast-util-gfm-table": "^2.0.0", + "mdast-util-gfm-task-list-item": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-autolink-literal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz", + "integrity": "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "ccount": "^2.0.0", + "devlop": "^1.0.0", + "mdast-util-find-and-replace": "^3.0.0", + "micromark-util-character": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-strikethrough": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz", + "integrity": "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-table": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz", + "integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "markdown-table": "^3.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-task-list-item": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz", + "integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-mdx/-/mdast-util-mdx-3.0.0.tgz", + "integrity": "sha512-JfbYLAW7XnYTTbUsmpu0kdBUVe+yKVJZBItEjwyYJiDJuZ9w4eeaqks4HQO+R7objWgS2ymV60GYpI14Ug554w==", + "license": "MIT", + "dependencies": { + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-expression": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz", + "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-jsx": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz", + "integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "parse-entities": "^4.0.0", + "stringify-entities": "^4.0.0", + "unist-util-stringify-position": "^4.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdxjs-esm": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz", + "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-phrasing": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", + "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "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/mdast-util-to-markdown": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz", + "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdn-data": { + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", + "license": "CC0-1.0" + }, + "node_modules/mdurl": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.1.0.tgz", + "integrity": "sha512-1+HBaOx0zi/dQWht8rNv9MYf9qqpqL/kxI0hXImU6Y547zM6Sni8BQibt7ifgMcYtQg41ao3Ivd6cnSM86inpg==", + "license": "MIT" + }, + "node_modules/mermaid": { + "version": "11.17.1", + "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-11.17.1.tgz", + "integrity": "sha512-G1BP6qU4BRwEGk2SdsxgDk1cSuApimT32Nkb52YRSv+856FrmB8qo28BaNk9Ee8Fvuon3OxpXDJ4L6cD5AykXg==", + "license": "MIT", + "dependencies": { + "@braintree/sanitize-url": "^7.1.2", + "@iconify/utils": "^3.0.2", + "@mermaid-js/parser": "^1.2.1", + "@types/d3": "^7.4.3", + "@upsetjs/venn.js": "^2.0.0", + "cytoscape": "^3.34.0", + "cytoscape-cose-bilkent": "^4.1.0", + "cytoscape-fcose": "^2.2.0", + "d3": "^7.9.0", + "d3-sankey": "^0.12.3", + "dagre-d3-es": "7.0.14", + "dayjs": "^1.11.21", + "dompurify": "^3.3.3", + "es-toolkit": "^1.45.1", + "fastdom": "1.0.12", + "katex": "^0.16.47", + "khroma": "^2.1.0", + "marked": "^16.3.0", + "roughjs": "^4.6.6", + "stylis": "^4.3.6", + "ts-dedent": "^2.2.0", + "uuid": "^11.1.0 || ^12 || ^13 || ^14.0.0" + } + }, + "node_modules/micromark": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-core-commonmark": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", + "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-destination": "^2.0.0", + "micromark-factory-label": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-title": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-html-tag-name": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-directive": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-directive/-/micromark-extension-directive-4.0.0.tgz", + "integrity": "sha512-/C2nqVmXXmiseSSuCdItCMho7ybwwop6RrrRPk0KbOHW21JKoCldC+8rFOaundDoRBUWBnJJcxeA/Kvi34WQXg==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "parse-entities": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz", + "integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==", + "license": "MIT", + "dependencies": { + "micromark-extension-gfm-autolink-literal": "^2.0.0", + "micromark-extension-gfm-footnote": "^2.0.0", + "micromark-extension-gfm-strikethrough": "^2.0.0", + "micromark-extension-gfm-table": "^2.0.0", + "micromark-extension-gfm-tagfilter": "^2.0.0", + "micromark-extension-gfm-task-list-item": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-autolink-literal": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz", + "integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==", + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-strikethrough": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz", + "integrity": "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-table": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz", + "integrity": "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-tagfilter": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz", + "integrity": "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==", + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-task-list-item": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz", + "integrity": "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-mdx-expression": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/micromark-extension-mdx-expression/-/micromark-extension-mdx-expression-3.0.1.tgz", + "integrity": "sha512-dD/ADLJ1AeMvSAKBwO22zG22N4ybhe7kFIZ3LsDI0GlsNr2A3KYxb0LdC1u5rj4Nw+CHKY0RVdnHX8vj8ejm4Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-mdx-expression": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-events-to-acorn": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-mdx-jsx": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/micromark-extension-mdx-jsx/-/micromark-extension-mdx-jsx-3.0.2.tgz", + "integrity": "sha512-e5+q1DjMh62LZAJOnDraSSbDMvGJ8x3cbjygy2qFEi7HCeUT4BDKCvMozPozcD6WmOt6sVvYDNBKhFSz3kjOVQ==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "micromark-factory-mdx-expression": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-events-to-acorn": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-mdx-md": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-mdx-md/-/micromark-extension-mdx-md-2.0.0.tgz", + "integrity": "sha512-EpAiszsB3blw4Rpba7xTOUptcFeBFi+6PY8VnJ2hhimH+vCQDirWgsMpz7w1XcZE7LVrSAUGb9VJpG9ghlYvYQ==", + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-mdxjs": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-mdxjs/-/micromark-extension-mdxjs-3.0.0.tgz", + "integrity": "sha512-A873fJfhnJ2siZyUrJ31l34Uqwy4xIFmvPY1oj+Ean5PHcPBYzEsvqvWGaWcfEIr11O5Dlw3p2y0tZWpKHDejQ==", + "license": "MIT", + "dependencies": { + "acorn": "^8.0.0", + "acorn-jsx": "^5.0.0", + "micromark-extension-mdx-expression": "^3.0.0", + "micromark-extension-mdx-jsx": "^3.0.0", + "micromark-extension-mdx-md": "^2.0.0", + "micromark-extension-mdxjs-esm": "^3.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-mdxjs-esm": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-mdxjs-esm/-/micromark-extension-mdxjs-esm-3.0.0.tgz", + "integrity": "sha512-DJFl4ZqkErRpq/dAPyeWp15tGrcrrJho1hKK5uBS70BCtfrIFg81sqcTVu3Ta+KD1Tk5vAtBNElWxtAa+m8K9A==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-events-to-acorn": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-position-from-estree": "^2.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-factory-destination": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", + "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", + "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-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-label": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", + "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-mdx-expression": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-factory-mdx-expression/-/micromark-factory-mdx-expression-2.0.3.tgz", + "integrity": "sha512-kQnEtA3vzucU2BkrIa8/VaSAsP+EJ3CKOvhMuJgOEGg9KDC6OAY6nSnNDVRiVNRqj7Y4SlSzcStaH/5jge8JdQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-events-to-acorn": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-position-from-estree": "^2.0.0", + "vfile-message": "^4.0.0" + } + }, + "node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "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-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-title": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", + "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-whitespace": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", + "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "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-chunked": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", + "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", + "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" + } + }, + "node_modules/micromark-util-classify-character": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", + "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", + "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-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-combine-extensions": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", + "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-chunked": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-numeric-character-reference": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", + "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", + "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" + } + }, + "node_modules/micromark-util-decode-string": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", + "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-symbol": "^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-events-to-acorn": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-util-events-to-acorn/-/micromark-util-events-to-acorn-2.0.3.tgz", + "integrity": "sha512-jmsiEIiZ1n7X1Rr5k8wVExBQCg5jy4UXVADItHmNk1zkwEVhBuIUKRu3fqv+hs4nxLISi2DQGlqIOGiFxgbfHg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/unist": "^3.0.0", + "devlop": "^1.0.0", + "estree-util-visit": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "vfile-message": "^4.0.0" + } + }, + "node_modules/micromark-util-html-tag-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", + "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-normalize-identifier": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", + "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", + "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" + } + }, + "node_modules/micromark-util-resolve-all": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", + "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + } + }, + "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-subtokenize": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", + "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^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.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/mrmime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", + "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/neotraverse": { + "version": "0.6.18", + "resolved": "https://registry.npmjs.org/neotraverse/-/neotraverse-0.6.18.tgz", + "integrity": "sha512-Z4SmBUweYa09+o6pG+eASabEpP6QkQ70yHj351pQoEXIs8uHbaU2DWVmzBANKgflPa47A50PtB2+NgRpQvr7vA==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/nlcst-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/nlcst-to-string/-/nlcst-to-string-4.0.0.tgz", + "integrity": "sha512-YKLBCcUYKAg0FNlOBT6aI91qFmSiFKiluk655WzPF+DDMA02qIyy8uiRqI8QXtcFpEvll12LpL5MXqEmAZ+dcA==", + "license": "MIT", + "dependencies": { + "@types/nlcst": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/node-fetch-native": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz", + "integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==", + "license": "MIT" + }, + "node_modules/node-mock-http": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/node-mock-http/-/node-mock-http-1.0.5.tgz", + "integrity": "sha512-KQyt/wLjG3TAc7DOUhpqWzgd4ERxR80JOlTK5VE5R1S12IaPVN5qkj4klBce9HPG1Njuup4Sb5bljaT34lIyjw==", + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/obug": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.4.tgz", + "integrity": "sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==", + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/ofetch": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/ofetch/-/ofetch-1.5.1.tgz", + "integrity": "sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA==", + "license": "MIT", + "dependencies": { + "destr": "^2.0.5", + "node-fetch-native": "^1.6.7", + "ufo": "^1.6.1" + } + }, + "node_modules/ohash": { + "version": "2.0.12", + "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.12.tgz", + "integrity": "sha512-65S/5gk9YSsaRjcyf7Nfa6h/d3E8/1gslpXfI4W7Dxn/oap8IKRuNT5VXkLQ1YFKIEg4apRY4Pj6aiwFzrDdmw==", + "license": "MIT" + }, + "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/p-limit": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-7.3.1.tgz", + "integrity": "sha512-0trZaiG7Y7kN/Egy9a8j47t9osC0Tch4PaIWd9yGF6bvmlk7muExRvGNYb8sXBwEKMoNKsbNN9P8EefuQekE4Q==", + "license": "MIT", + "dependencies": { + "yocto-queue": "^1.2.1" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-queue": { + "version": "9.3.3", + "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-9.3.3.tgz", + "integrity": "sha512-NXAOdnEe5FsZJfT4oK84lE1Y5cFFdWlRuOo5tww8DyNMxyRXwn39fIkUtNLKppcPC+UYU/bXujNCUGDv01y7CA==", + "license": "MIT", + "dependencies": { + "eventemitter3": "^5.0.4", + "p-timeout": "^7.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-timeout": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-7.0.1.tgz", + "integrity": "sha512-AxTM2wDGORHGEkPCt8yqxOTMgpfbEHqF51f/5fJCmwFC3C/zNcGT63SymH2ttOAaiIws2zVg4+izQCjrakcwHg==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/package-manager-detector": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.8.0.tgz", + "integrity": "sha512-yQA4H19AmPEoMUeavPMDIe1higySl/gH/yaQrkT/s07Qp+7pp2hYz30N3z2l5BkjVkF9Ow6o0wjJamm2y7Sn0A==", + "license": "MIT" + }, + "node_modules/pagefind": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/pagefind/-/pagefind-1.5.2.tgz", + "integrity": "sha512-XTUaK0hXMCu2jszWE584JGQT7y284TmMV9l/HX3rnG5uo3rHI/uHU56XTyyyPFjeWEBxECbAi0CaFDJOONtG0Q==", + "license": "MIT", + "bin": { + "pagefind": "lib/runner/bin.cjs" + }, + "optionalDependencies": { + "@pagefind/darwin-arm64": "1.5.2", + "@pagefind/darwin-x64": "1.5.2", + "@pagefind/freebsd-x64": "1.5.2", + "@pagefind/linux-arm64": "1.5.2", + "@pagefind/linux-x64": "1.5.2", + "@pagefind/windows-arm64": "1.5.2", + "@pagefind/windows-x64": "1.5.2" + } + }, + "node_modules/parse-entities": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", + "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "character-entities-legacy": "^3.0.0", + "character-reference-invalid": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "is-alphanumerical": "^2.0.0", + "is-decimal": "^2.0.0", + "is-hexadecimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/parse-entities/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, + "node_modules/parse-latin": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/parse-latin/-/parse-latin-7.0.0.tgz", + "integrity": "sha512-mhHgobPPua5kZ98EF4HWiH167JWBfl4pvAIXXdbaVohtK7a6YBOy56kvhCqduqyo/f3yrHFWmqmiMg/BkBkYYQ==", + "license": "MIT", + "dependencies": { + "@types/nlcst": "^2.0.0", + "@types/unist": "^3.0.0", + "nlcst-to-string": "^4.0.0", + "unist-util-modify-children": "^4.0.0", + "unist-util-visit-children": "^3.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/path-data-parser": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/path-data-parser/-/path-data-parser-0.1.0.tgz", + "integrity": "sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w==", + "license": "MIT" + }, + "node_modules/piccolore": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/piccolore/-/piccolore-0.1.3.tgz", + "integrity": "sha512-o8bTeDWjE086iwKrROaDf31K0qC/BENdm15/uH9usSC/uZjJOKb2YGiVHfLY4GhwsERiPI1jmwI2XrA7ACOxVw==", + "license": "ISC" + }, + "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/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/points-on-curve": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/points-on-curve/-/points-on-curve-0.2.0.tgz", + "integrity": "sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A==", + "license": "MIT" + }, + "node_modules/points-on-path": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/points-on-path/-/points-on-path-0.2.1.tgz", + "integrity": "sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g==", + "license": "MIT", + "dependencies": { + "path-data-parser": "0.1.0", + "points-on-curve": "0.2.0" + } + }, + "node_modules/postcss": { + "version": "8.5.26", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", + "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", + "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.17", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.4.tgz", + "integrity": "sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/prismjs": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz", + "integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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/punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/radix3": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/radix3/-/radix3-1.1.2.tgz", + "integrity": "sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==", + "license": "MIT" + }, + "node_modules/readdirp": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.1.1.tgz", + "integrity": "sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/recma-build-jsx": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/recma-build-jsx/-/recma-build-jsx-1.0.0.tgz", + "integrity": "sha512-8GtdyqaBcDfva+GUKDr3nev3VpKAhup1+RvkMvUxURHpW7QyIvk9F5wz7Vzo06CEMSilw6uArgRqhpiUcWp8ew==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-util-build-jsx": "^3.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/recma-jsx": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/recma-jsx/-/recma-jsx-1.0.1.tgz", + "integrity": "sha512-huSIy7VU2Z5OLv6oFLosQGGDqPqdO1iq6bWNAdhzMxSJP7RAso4fCZ1cKu8j9YHCZf3TPrq4dw3okhrylgcd7w==", + "license": "MIT", + "dependencies": { + "acorn-jsx": "^5.0.0", + "estree-util-to-js": "^2.0.0", + "recma-parse": "^1.0.0", + "recma-stringify": "^1.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + }, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/recma-parse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/recma-parse/-/recma-parse-1.0.0.tgz", + "integrity": "sha512-OYLsIGBB5Y5wjnSnQW6t3Xg7q3fQ7FWbw/vcXtORTnyaSFscOtABg+7Pnz6YZ6c27fG1/aN8CjfwoUEUIdwqWQ==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "esast-util-from-js": "^2.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/recma-stringify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/recma-stringify/-/recma-stringify-1.0.0.tgz", + "integrity": "sha512-cjwII1MdIIVloKvC9ErQ+OgAtwHBmcZ0Bg4ciz78FtbT8In39aAYbaA7zvxQ61xVMSPE8WxhLwLbhif4Js2C+g==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-util-to-js": "^2.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "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/rehype": { + "version": "13.0.2", + "resolved": "https://registry.npmjs.org/rehype/-/rehype-13.0.2.tgz", + "integrity": "sha512-j31mdaRFrwFRUIlxGeuPXXKWQxet52RBQRvCmzl5eCefn/KGbomK5GMHNMsOJf55fgo3qw5tST5neDuarDYR2A==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "rehype-parse": "^9.0.0", + "rehype-stringify": "^10.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-expressive-code": { + "version": "0.42.0", + "resolved": "https://registry.npmjs.org/rehype-expressive-code/-/rehype-expressive-code-0.42.0.tgz", + "integrity": "sha512-8rp/1YMEVVSYbtz+bFBx+uSx3vA4i4T8RwRm5Q/IWbucQnnQqQ0hDqtmKOr8tv+59Cik6cu5aH3WPo0I7csuTA==", + "license": "MIT", + "dependencies": { + "expressive-code": "^0.42.0" + } + }, + "node_modules/rehype-format": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/rehype-format/-/rehype-format-5.0.1.tgz", + "integrity": "sha512-zvmVru9uB0josBVpr946OR8ui7nJEdzZobwLOOqHb/OOD88W0Vk2SqLwoVOj0fM6IPCCO6TaV9CvQvJMWwukFQ==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-format": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-parse": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/rehype-parse/-/rehype-parse-9.0.1.tgz", + "integrity": "sha512-ksCzCD0Fgfh7trPDxr2rSylbwq9iYDkSn8TCDmEJ49ljEUBxDVCzCHv7QNzZOfODanX4+bWQ4WZqLCRWYLfhag==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-from-html": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-raw": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/rehype-raw/-/rehype-raw-7.0.0.tgz", + "integrity": "sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-raw": "^9.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-recma": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/rehype-recma/-/rehype-recma-1.0.0.tgz", + "integrity": "sha512-lqA4rGUf1JmacCNWWZx0Wv1dHqMwxzsDWYMTowuplHF3xH0N/MmrZ/G3BDZnzAkRmxDadujCjaKM2hqYdCBOGw==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/hast": "^3.0.0", + "hast-util-to-estree": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-stringify": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/rehype-stringify/-/rehype-stringify-10.0.1.tgz", + "integrity": "sha512-k9ecfXHmIPuFVI61B9DeLPN0qFHfawM6RsuX48hoqlaKSF61RskNjSm1lI8PhBEM0MRdLxVVm4WmTqJQccH9mA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-to-html": "^9.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-directive": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/remark-directive/-/remark-directive-4.0.0.tgz", + "integrity": "sha512-7sxn4RfF1o3izevPV1DheyGDD6X4c9hrGpfdUpm7uC++dqrnJxIZVkk7CoKqcLm0VUMAuOol7Mno3m6g8cfMuA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-directive": "^3.0.0", + "micromark-extension-directive": "^4.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-gfm": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz", + "integrity": "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-gfm": "^3.0.0", + "micromark-extension-gfm": "^3.0.0", + "remark-parse": "^11.0.0", + "remark-stringify": "^11.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-mdx": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/remark-mdx/-/remark-mdx-3.1.1.tgz", + "integrity": "sha512-Pjj2IYlUY3+D8x00UJsIOg5BEvfMyeI+2uLPn9VO9Wg4MEtN/VTIq2NEJQfde9PnX15KgtHyl9S0BcTnWrIuWg==", + "license": "MIT", + "dependencies": { + "mdast-util-mdx": "^3.0.0", + "micromark-extension-mdxjs": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-parse": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", + "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-rehype": { + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz", + "integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "mdast-util-to-hast": "^13.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-smartypants": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/remark-smartypants/-/remark-smartypants-3.0.3.tgz", + "integrity": "sha512-gCaK+ndZ0hYezlqFegHFCVh2CQemsi0Npdh1qVM9bxlUFknjkbP6VmojWhddOCrbK0PbbacmYLWfTULRiT1eWA==", + "license": "MIT", + "dependencies": { + "retext": "^9.0.0", + "retext-smartypants": "^6.0.0", + "unified": "^11.0.4", + "unist-util-visit": "^5.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/remark-stringify": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz", + "integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-to-markdown": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/retext": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/retext/-/retext-9.0.0.tgz", + "integrity": "sha512-sbMDcpHCNjvlheSgMfEcVrZko3cDzdbe1x/e7G66dFp0Ff7Mldvi2uv6JkJQzdRcvLYE8CA8Oe8siQx8ZOgTcA==", + "license": "MIT", + "dependencies": { + "@types/nlcst": "^2.0.0", + "retext-latin": "^4.0.0", + "retext-stringify": "^4.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/retext-latin": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/retext-latin/-/retext-latin-4.0.0.tgz", + "integrity": "sha512-hv9woG7Fy0M9IlRQloq/N6atV82NxLGveq+3H2WOi79dtIYWN8OaxogDm77f8YnVXJL2VD3bbqowu5E3EMhBYA==", + "license": "MIT", + "dependencies": { + "@types/nlcst": "^2.0.0", + "parse-latin": "^7.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/retext-smartypants": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/retext-smartypants/-/retext-smartypants-6.2.0.tgz", + "integrity": "sha512-kk0jOU7+zGv//kfjXEBjdIryL1Acl4i9XNkHxtM7Tm5lFiCog576fjNC9hjoR7LTKQ0DsPWy09JummSsH1uqfQ==", + "license": "MIT", + "dependencies": { + "@types/nlcst": "^2.0.0", + "nlcst-to-string": "^4.0.0", + "unist-util-visit": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/retext-stringify": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/retext-stringify/-/retext-stringify-4.0.0.tgz", + "integrity": "sha512-rtfN/0o8kL1e+78+uxPTqu1Klt0yPzKuQ2BfWwwfgIUSayyzxpM1PJzkKt4V8803uB9qSy32MvI7Xep9khTpiA==", + "license": "MIT", + "dependencies": { + "@types/nlcst": "^2.0.0", + "nlcst-to-string": "^4.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/robust-predicates": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.3.tgz", + "integrity": "sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==", + "license": "Unlicense" + }, + "node_modules/rollup": { + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.5.tgz", + "integrity": "sha512-/tqMfgP7GPA3PHhCmuiS4vIjrSVhHLgY++i+dhbG462euyAj7FpM4D9uq1X3BgjlqRdpcOrYhcQtfiQLNc8tqw==", + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@napi-rs/lzma-linux-x64-gnu": "1.5.1", + "@rollup/rollup-android-arm-eabi": "4.62.5", + "@rollup/rollup-android-arm64": "4.62.5", + "@rollup/rollup-darwin-arm64": "4.62.5", + "@rollup/rollup-darwin-x64": "4.62.5", + "@rollup/rollup-freebsd-arm64": "4.62.5", + "@rollup/rollup-freebsd-x64": "4.62.5", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.5", + "@rollup/rollup-linux-arm-musleabihf": "4.62.5", + "@rollup/rollup-linux-arm64-gnu": "4.62.5", + "@rollup/rollup-linux-arm64-musl": "4.62.5", + "@rollup/rollup-linux-loong64-gnu": "4.62.5", + "@rollup/rollup-linux-loong64-musl": "4.62.5", + "@rollup/rollup-linux-ppc64-gnu": "4.62.5", + "@rollup/rollup-linux-ppc64-musl": "4.62.5", + "@rollup/rollup-linux-riscv64-gnu": "4.62.5", + "@rollup/rollup-linux-riscv64-musl": "4.62.5", + "@rollup/rollup-linux-s390x-gnu": "4.62.5", + "@rollup/rollup-linux-x64-gnu": "4.62.5", + "@rollup/rollup-linux-x64-musl": "4.62.5", + "@rollup/rollup-openbsd-x64": "4.62.5", + "@rollup/rollup-openharmony-arm64": "4.62.5", + "@rollup/rollup-win32-arm64-msvc": "4.62.5", + "@rollup/rollup-win32-ia32-msvc": "4.62.5", + "@rollup/rollup-win32-x64-gnu": "4.62.5", + "@rollup/rollup-win32-x64-msvc": "4.62.5", + "fsevents": "~2.3.2" + } + }, + "node_modules/roughjs": { + "version": "4.6.6", + "resolved": "https://registry.npmjs.org/roughjs/-/roughjs-4.6.6.tgz", + "integrity": "sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ==", + "license": "MIT", + "dependencies": { + "hachure-fill": "^0.5.2", + "path-data-parser": "^0.1.0", + "points-on-curve": "^0.2.0", + "points-on-path": "^0.2.1" + } + }, + "node_modules/rw": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", + "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==", + "license": "BSD-3-Clause" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/sax": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.1.tgz", + "integrity": "sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } + }, + "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", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sharp": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" + } + }, + "node_modules/shiki": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-4.4.3.tgz", + "integrity": "sha512-Mb/GvXPHBAXdgGIcnfU5L3ldpn1XcxrGkPHwqgRx17/I2XRfqlFKk2vGkHWINn1kdXvzJZeuO3is6I9KLPFm0g==", + "license": "MIT", + "dependencies": { + "@shikijs/core": "4.4.3", + "@shikijs/engine-javascript": "4.4.3", + "@shikijs/engine-oniguruma": "4.4.3", + "@shikijs/langs": "4.4.3", + "@shikijs/themes": "4.4.3", + "@shikijs/types": "4.4.3", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.5" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "license": "MIT" + }, + "node_modules/sitemap": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/sitemap/-/sitemap-9.0.1.tgz", + "integrity": "sha512-S6hzjGJSG3d6if0YoF5kTyeRJvia6FSTBroE5fQ0bu1QNxyJqhhinfUsXi9fH3MgtXODWvwo2BDyQSnhPQ88uQ==", + "license": "MIT", + "dependencies": { + "@types/node": "^24.9.2", + "@types/sax": "^1.2.1", + "arg": "^5.0.0", + "sax": "^1.4.1" + }, + "bin": { + "sitemap": "dist/esm/cli.js" + }, + "engines": { + "node": ">=20.19.5", + "npm": ">=10.8.2" + } + }, + "node_modules/smol-toml": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.8.0.tgz", + "integrity": "sha512-kCZr2V3ch9i00x8zXRhjUNVcjG9ijES5dDudkXvUVCT5QlJNQWElSJdZqyPemffHoLNUYwOcou0Fy+ojN0uHSQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">= 18" + }, + "funding": { + "url": "https://github.com/sponsors/cyyynthia" + } + }, + "node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "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/starlight-image-zoom": { + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/starlight-image-zoom/-/starlight-image-zoom-0.14.2.tgz", + "integrity": "sha512-YBvE724gFMiVjObGtXmfIDi3zAxVBsvGukeRXOiWLNnESFOBZFfO9DpC/reHohrCxFrO+5ot0XR8EimbE0LpsA==", + "license": "MIT", + "dependencies": { + "mdast-util-mdx-jsx": "^3.2.0", + "rehype-raw": "^7.0.0", + "unist-util-visit": "^5.1.0", + "unist-util-visit-parents": "^6.0.2" + }, + "engines": { + "node": ">=22.12.0" + }, + "peerDependencies": { + "@astrojs/starlight": ">=0.38.0" + } + }, + "node_modules/starlight-scroll-to-top": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/starlight-scroll-to-top/-/starlight-scroll-to-top-1.0.1.tgz", + "integrity": "sha512-OkSVwEQBzHkYPgBYL/O9xSiFn75j6HvrmjvdL8+3Kk0oxgt4sNmkOwhB0bGjB/T7M7SAJ4AFk1Ojza1HVxOeHQ==", + "license": "MIT", + "engines": { + "node": ">=22.12.0" + }, + "peerDependencies": { + "@astrojs/starlight": ">=0.38.0" + } + }, + "node_modules/starlight-sidebar-topics": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/starlight-sidebar-topics/-/starlight-sidebar-topics-0.7.2.tgz", + "integrity": "sha512-eKNQqA6EdNJTnAReVFZciPzuoBBwXs7ynNZRAukYSsh6pGfPtS9l0F+knIUbEP+eFQT4Vcbr9SxxiXEZtjowHw==", + "license": "MIT", + "dependencies": { + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=22.12.0" + }, + "peerDependencies": { + "@astrojs/starlight": ">=0.38.0" + } + }, + "node_modules/starlight-typedoc": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/starlight-typedoc/-/starlight-typedoc-0.23.1.tgz", + "integrity": "sha512-H45Mxv4mZ1xUMhUNjTR4CwKDgKKHZboQA5v5+vTVMdjlCX02Xo7veTB7+F/ivmMHPmQ7SV5auF2BTlU/EpQIoQ==", + "license": "MIT", + "dependencies": { + "github-slugger": "^2.0.0" + }, + "engines": { + "node": ">=22.12.0" + }, + "peerDependencies": { + "@astrojs/starlight": ">=0.39.0", + "astro": ">=6.0.0", + "typedoc": ">=0.28.0", + "typedoc-plugin-markdown": ">=4.6.0" + } + }, + "node_modules/stream-replace-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/stream-replace-string/-/stream-replace-string-2.0.0.tgz", + "integrity": "sha512-TlnjJ1C0QrmxRNrON00JvaFFlNh5TTG00APw23j74ET7gkQpTASi6/L2fuiav8pzK715HXtUeClpBTw2NPSn6w==", + "license": "MIT" + }, + "node_modules/strictdom": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/strictdom/-/strictdom-1.0.1.tgz", + "integrity": "sha512-cEmp9QeXXRmjj/rVp9oyiqcvyocWab/HaoN4+bwFeZ7QzykJD6L3yD4v12K1x0tHpqRqVpJevN3gW7kyM39Bqg==", + "license": "MIT" + }, + "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/style-to-js": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.21.tgz", + "integrity": "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==", + "license": "MIT", + "dependencies": { + "style-to-object": "1.0.14" + } + }, + "node_modules/style-to-object": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.14.tgz", + "integrity": "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==", + "license": "MIT", + "dependencies": { + "inline-style-parser": "0.2.7" + } + }, + "node_modules/stylis": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.4.0.tgz", + "integrity": "sha512-5Z9ZpRzfuH6l/UAvCPAPUo3665Nk2wLaZU3x+TLHKVzIz33+sbJqbtrYoC3KD4/uVOr2Zp+L0LySezP9OHV9yA==", + "license": "MIT" + }, + "node_modules/svgo": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-4.1.0.tgz", + "integrity": "sha512-bkxnTg1kSU0guhIBmibA6UUhrQmPVA1XsQLN+ylCd+UWzbnLkySOcXpyk1mrl05f+pcaCx2eHb+sp6BgMZWX+Q==", + "license": "MIT", + "dependencies": { + "commander": "^11.1.0", + "css-select": "^6.0.0", + "css-tree": "^3.0.1", + "css-what": "^7.0.0", + "csso": "^5.0.5", + "picocolors": "^1.1.1", + "sax": "1.6.1" + }, + "bin": { + "svgo": "bin/svgo.js" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/svgo" + } + }, + "node_modules/svgo/node_modules/commander": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", + "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/tailwindcss": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.3.tgz", + "integrity": "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==", + "license": "MIT", + "peer": true + }, + "node_modules/tiny-inflate": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz", + "integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==", + "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==", + "license": "MIT", + "engines": { + "node": "^16.14.0 || >= 17.3.0" + } + }, + "node_modules/tinyexec": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.0.tgz", + "integrity": "sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "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/trough": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", + "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/ts-dedent": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/ts-dedent/-/ts-dedent-2.3.0.tgz", + "integrity": "sha512-JfJeIHke7y2egdGGgRAvpCwYFUsHlM2gPcrVOxFkznt/4uzQ7HFmvE63iFHVLBJNDuyDOQgijDK/tXH/f6Msjg==", + "license": "MIT", + "engines": { + "node": ">=6.10" + } + }, + "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", + "optional": true + }, + "node_modules/typedoc": { + "version": "0.28.20", + "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.28.20.tgz", + "integrity": "sha512-uSKqkh8Cr48vllnEy+jdaAgOeR6Y+QCBW7usgUsKj7gJEfR7stw9U/fE49LBnj2tPRKPY0c0EBJSWe9Appmplg==", + "license": "Apache-2.0", + "dependencies": { + "@gerrit0/mini-shiki": "^3.23.0", + "lunr": "^2.3.9", + "markdown-it": "^14.3.0", + "minimatch": "^10.2.5", + "yaml": "^2.9.0" + }, + "bin": { + "typedoc": "bin/typedoc" + }, + "engines": { + "node": ">= 18", + "pnpm": ">= 10" + }, + "peerDependencies": { + "typescript": "5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x || 5.5.x || 5.6.x || 5.7.x || 5.8.x || 5.9.x || 6.0.x" + } + }, + "node_modules/typedoc-plugin-markdown": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/typedoc-plugin-markdown/-/typedoc-plugin-markdown-4.12.0.tgz", + "integrity": "sha512-eJDEMAfxCmede22c/Jw7d0FA13ggAQv+KkwQYKYCdqI02cin6Rc9QRwbG/7XvvHWinuFejySnZVUWDtvGk3Vbg==", + "license": "MIT", + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "typedoc": "0.28.x" + } + }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "license": "Apache-2.0", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/uc.micro": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", + "license": "MIT" + }, + "node_modules/ufo": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.4.tgz", + "integrity": "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==", + "license": "MIT" + }, + "node_modules/ultrahtml": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/ultrahtml/-/ultrahtml-1.7.0.tgz", + "integrity": "sha512-2xRd0VHoAQE4M+vF/DvFFB7pUV0ZxTW1TLi7lHQWnF/Sb5TPeEUV/l+hxcNnGO00ZXGnR0voCMmYRKQf+rvJ2g==", + "license": "MIT" + }, + "node_modules/uncrypto": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/uncrypto/-/uncrypto-0.1.3.tgz", + "integrity": "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==", + "license": "MIT" + }, + "node_modules/undici": { + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-8.10.0.tgz", + "integrity": "sha512-HvltHd7avK13QIw/oLe4qoOLyoVSoafqJ2jYOrtMRBkbYT31eiBQ8O0ehRKZiEZCMEyLFQNIADpgCWC5fALvYQ==", + "license": "MIT", + "engines": { + "node": ">=22.19.0" + } + }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "license": "MIT" + }, + "node_modules/unified": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", + "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "bail": "^2.0.0", + "devlop": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unifont": { + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/unifont/-/unifont-0.7.5.tgz", + "integrity": "sha512-ULe/Cs+ZIsq+dcFofNkhqielCrUJnb5mr+Yc4EBM2VlL+6OZR6+cjtI2mT1bJvRBrVncqHAbLURxmPLcCXzWMg==", + "license": "MIT", + "dependencies": { + "css-tree": "^3.1.0", + "ohash": "^2.0.11", + "undici": "^8.0.0" + } + }, + "node_modules/unist-util-find-after": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-find-after/-/unist-util-find-after-5.0.0.tgz", + "integrity": "sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "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-modify-children": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-modify-children/-/unist-util-modify-children-4.0.0.tgz", + "integrity": "sha512-+tdN5fGNddvsQdIzUF3Xx82CU9sMM+fA0dLgR9vOmT0oPT2jH+P1nd5lSqfCfXAw+93NhcXNY2qqvTUtE4cQkw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "array-iterate": "^2.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-position-from-estree": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position-from-estree/-/unist-util-position-from-estree-2.0.0.tgz", + "integrity": "sha512-KaFVRjoqLyF6YXCbVLNad/eS4+OfPQQn2yOd7zF/h5T/CSL2v8NpN6a5TPvtbXthAGw5nG+PuTtq+DdIZr+cRQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-remove-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-remove-position/-/unist-util-remove-position-5.0.0.tgz", + "integrity": "sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-visit": "^5.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-children": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unist-util-visit-children/-/unist-util-visit-children-3.0.0.tgz", + "integrity": "sha512-RgmdTfSBOg04sdPcpTSD1jzoNBjt9a80/ZCzp5cI9n1qPzLZWF9YdvWGN2zmTumP1HWhXKdUWexjy/Wy/lJ7tA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.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/unstorage": { + "version": "1.17.5", + "resolved": "https://registry.npmjs.org/unstorage/-/unstorage-1.17.5.tgz", + "integrity": "sha512-0i3iqvRfx29hkNntHyQvJTpf5W9dQ9ZadSoRU8+xVlhVtT7jAX57fazYO9EHvcRCfBCyi5YRya7XCDOsbTgkPg==", + "license": "MIT", + "dependencies": { + "anymatch": "^3.1.3", + "chokidar": "^5.0.0", + "destr": "^2.0.5", + "h3": "^1.15.10", + "lru-cache": "^11.2.7", + "node-fetch-native": "^1.6.7", + "ofetch": "^1.5.1", + "ufo": "^1.6.3" + }, + "peerDependencies": { + "@azure/app-configuration": "^1.8.0", + "@azure/cosmos": "^4.2.0", + "@azure/data-tables": "^13.3.0", + "@azure/identity": "^4.6.0", + "@azure/keyvault-secrets": "^4.9.0", + "@azure/storage-blob": "^12.26.0", + "@capacitor/preferences": "^6 || ^7 || ^8", + "@deno/kv": ">=0.9.0", + "@netlify/blobs": "^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0", + "@planetscale/database": "^1.19.0", + "@upstash/redis": "^1.34.3", + "@vercel/blob": ">=0.27.1", + "@vercel/functions": "^2.2.12 || ^3.0.0", + "@vercel/kv": "^1 || ^2 || ^3", + "aws4fetch": "^1.0.20", + "db0": ">=0.2.1", + "idb-keyval": "^6.2.1", + "ioredis": "^5.4.2", + "uploadthing": "^7.4.4" + }, + "peerDependenciesMeta": { + "@azure/app-configuration": { + "optional": true + }, + "@azure/cosmos": { + "optional": true + }, + "@azure/data-tables": { + "optional": true + }, + "@azure/identity": { + "optional": true + }, + "@azure/keyvault-secrets": { + "optional": true + }, + "@azure/storage-blob": { + "optional": true + }, + "@capacitor/preferences": { + "optional": true + }, + "@deno/kv": { + "optional": true + }, + "@netlify/blobs": { + "optional": true + }, + "@planetscale/database": { + "optional": true + }, + "@upstash/redis": { + "optional": true + }, + "@vercel/blob": { + "optional": true + }, + "@vercel/functions": { + "optional": true + }, + "@vercel/kv": { + "optional": true + }, + "aws4fetch": { + "optional": true + }, + "db0": { + "optional": true + }, + "idb-keyval": { + "optional": true + }, + "ioredis": { + "optional": true + }, + "uploadthing": { + "optional": true + } + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/uuid": { + "version": "14.0.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-14.0.2.tgz", + "integrity": "sha512-xZe/16rV4aa+HGSOCiY2YeLT1OybRLrrkL/Rqaq7p7GMVXjFh+6wN4oMYgjFmnSnhY8t6Xpdl2l9qmnHYuMHwQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist-node/bin/uuid" + } + }, + "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-location": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-5.0.3.tgz", + "integrity": "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile": "^6.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/vite": { + "version": "7.3.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.6.tgz", + "integrity": "sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==", + "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 + } + } + }, + "node_modules/vitefu": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.1.3.tgz", + "integrity": "sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg==", + "license": "MIT", + "workspaces": [ + "tests/deps/*", + "tests/projects/*", + "tests/projects/workspace/packages/*" + ], + "peerDependencies": { + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } + } + }, + "node_modules/web-namespaces": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz", + "integrity": "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/which-pm-runs": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/which-pm-runs/-/which-pm-runs-1.1.0.tgz", + "integrity": "sha512-n1brCuqClxfFfq/Rb0ICg9giSZqCS+pLtccdag6C2HyufBrh3fBOiy9nb6ggRMvWOVH5GrdJskj5iGTZNxd7SA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/xxhash-wasm": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/xxhash-wasm/-/xxhash-wasm-1.1.0.tgz", + "integrity": "sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA==", + "license": "MIT" + }, + "node_modules/yaml": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/yargs-parser": { + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", + "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", + "license": "ISC", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, + "node_modules/yocto-queue": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.2.tgz", + "integrity": "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==", + "license": "MIT", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + } + } +} diff --git a/web/package.json b/web/package.json index 0844cc04..cb9e9be3 100644 --- a/web/package.json +++ b/web/package.json @@ -3,13 +3,14 @@ "type": "module", "version": "0.0.1", "scripts": { - "sync": "node scripts/sync-content.mjs && node scripts/sync-samples.mjs", + "sync": "node scripts/sync-public-content.mjs", + "sync:authoring": "node scripts/sync-content.mjs && node scripts/sync-samples.mjs", "predev": "npm run sync", "dev": "astro dev", "start": "astro dev", - "prebuild": "npm run sync", + "prebuild": "rm -rf dist && npm run sync", "build": "astro build", - "postbuild": "node scripts/emit-doc-artifacts.mjs", + "postbuild": "node scripts/emit-doc-artifacts.mjs && node scripts/check-public-surface.mjs", "preview": "astro preview", "astro": "astro", "lint:docs": "node scripts/lint-docs.mjs", @@ -25,7 +26,7 @@ "build:storybook:arc": "cd ../../Arc/Source/JavaScript && yarn workspace @cratis/arc.react exec storybook build -o ../../../../Documentation/web/public/storybook-arc", "build:storybooks": "npm run build:storybook && npm run build:storybook:arc", "build:api": "node scripts/build-api.mjs", - "check": "npm run build && npm run chronicle-client-docs:check && node scripts/lint-docs.mjs && node scripts/check-links.mjs && node scripts/lint-prose.mjs && node scripts/lint-markdown.mjs && node scripts/check-external-links.mjs" + "check": "npm run build && node scripts/lint-docs.mjs && node scripts/check-links.mjs && node scripts/lint-prose.mjs && node scripts/lint-markdown.mjs && node scripts/check-external-links.mjs" }, "dependencies": { "@astrojs/starlight": "^0.39.2", @@ -38,8 +39,6 @@ "mermaid": "^11.15.0", "sharp": "^0.34.5", "starlight-image-zoom": "^0.14.2", - "starlight-llms-txt": "^0.10.0", - "starlight-page-actions": "^0.5.0", "starlight-scroll-to-top": "^1.0.1", "starlight-sidebar-topics": "^0.7.1", "starlight-typedoc": "^0.23.0", diff --git a/web/public-surface.json b/web/public-surface.json new file mode 100644 index 00000000..957bf176 --- /dev/null +++ b/web/public-surface.json @@ -0,0 +1,140 @@ +{ + "schemaVersion": 1, + "approvedClaimIds": [ + "CLM-011", + "CLM-012", + "CLM-013", + "CLM-014", + "CLM-028", + "CLM-032" + ], + "routes": [ + { + "route": "/", + "contentPath": "index.mdx", + "source": { + "repository": "Documentation", + "revision": "self", + "path": "web/src/public-pages/index.mdx", + "sha256": "4c27c3b9d6de9611562df46551f8a89e57df870b4a4fefc60967a2db0f42bf6f" + }, + "claims": ["CLM-011", "CLM-012", "CLM-013", "CLM-014", "CLM-028", "CLM-032"], + "search": false, + "machine": false, + "sitemap": true + }, + { + "route": "/404.html", + "contentPath": "404.md", + "source": { + "repository": "Documentation", + "revision": "self", + "path": "web/src/public-pages/404.md", + "sha256": "9b12f8538f82e7245d9589d8e47dfde8bf42eb27ee7bdf560c76efbc5ee4b211" + }, + "claims": [], + "search": false, + "machine": false, + "sitemap": false + }, + { + "route": "/chronicle/", + "contentPath": "chronicle/index.mdx", + "source": { + "repository": "Chronicle", + "revision": "1b523b3ab70d58b487b840782992c317dd31e8bb", + "path": "Documentation/index.mdx", + "sha256": "df5778f606efc39e324e29cf733bb21a4606008e7df9aa9403f8df3de313beb8" + }, + "claims": ["CLM-013", "CLM-014", "CLM-028", "CLM-032"], + "search": false, + "machine": false, + "sitemap": true + }, + { + "route": "/chronicle/architecture/", + "contentPath": "chronicle/architecture.md", + "source": { + "repository": "Chronicle", + "revision": "1b523b3ab70d58b487b840782992c317dd31e8bb", + "path": "Documentation/architecture.md", + "sha256": "3163d6221ab9583583c9ddc8ad6a1f192f854503553daf193fb24d73a844d856" + }, + "claims": ["CLM-013", "CLM-014", "CLM-028"], + "search": false, + "machine": false, + "sitemap": true + }, + { + "route": "/chronicle/workbench/", + "contentPath": "chronicle/workbench/index.md", + "source": { + "repository": "Chronicle", + "revision": "1b523b3ab70d58b487b840782992c317dd31e8bb", + "path": "Documentation/workbench/index.md", + "sha256": "b542732dee52bce5d545a7099d58e45bbb223ed6ddd67fc31b60c7ac6cfd14a4" + }, + "claims": ["CLM-013", "CLM-014", "CLM-032"], + "search": false, + "machine": false, + "sitemap": true + }, + { + "route": "/arc/", + "contentPath": "arc/index.mdx", + "source": { + "repository": "Arc", + "revision": "f840015238b48ed9aad9998469d2fdaf2370b8df", + "path": "Documentation/index.mdx", + "sha256": "55e249cffbc3c6af1991f79ee8d2aab9904904aa2fb4322f0b463d2d04fd4c04" + }, + "claims": ["CLM-011", "CLM-012"], + "search": false, + "machine": false, + "sitemap": true + }, + { + "route": "/components/", + "contentPath": "components/index.mdx", + "source": { + "repository": "Components", + "revision": "2f7dd5046b7c5f2addb98ad4204d609a0f11d8fe", + "path": "Documentation/index.mdx", + "sha256": "945a3326dba9756a8eb809254da7c74a4dd25e4457dbb6ba0f77fd97e1e3e313" + }, + "claims": ["CLM-011", "CLM-012"], + "search": false, + "machine": false, + "sitemap": true + }, + { + "route": "/cli/", + "contentPath": "cli/index.mdx", + "source": { + "repository": "cli", + "revision": "db7d2339569042ff10896d0ae3219ad8e4505dd3", + "path": "Documentation/index.mdx", + "sha256": "f721d93f78f65174ee4ea681df1ea65199a42c3efca7b4890ae2aca041c7987f" + }, + "claims": ["CLM-013", "CLM-014"], + "search": false, + "machine": false, + "sitemap": true + } + ], + "generatedArtifacts": { + "prefix": "_astro/", + "extensions": [".css", ".js", ".png", ".svg", ".woff2"], + "sitemapPattern": "^sitemap-(?:index|[0-9]+)\\.xml$" + }, + "staticFiles": [ + { + "path": "favicon-512.png", + "sha256": "da99d76b1513c92617e4f3104437fe8988a3b94cc27d457955eb7e155403b7f6" + }, + { + "path": "favicon.ico", + "sha256": "9edf0320eae889809aa7b5fbd6b6bcd24f7485451f2a85d9804aa07ac9ec37da" + } + ] +} diff --git a/web/scripts/check-public-surface.mjs b/web/scripts/check-public-surface.mjs new file mode 100644 index 00000000..39ad58e7 --- /dev/null +++ b/web/scripts/check-public-surface.mjs @@ -0,0 +1,191 @@ +import { promises as fs } from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const here = path.dirname(fileURLToPath(import.meta.url)); +const webRoot = path.resolve(here, '..'); +const distRoot = path.join(webRoot, 'dist'); +const manifest = JSON.parse(await fs.readFile(path.join(webRoot, 'public-surface.json'), 'utf8')); +const errors = []; + +const approvedSentences = [ + 'Chronicle uses a .NET/Orleans actor-based kernel behind gRPC/HTTP surfaces and supports multiple event stores, namespaces, and persistent event-store subscriptions with outbox/inbox sequences.', + 'Chronicle and its bundled local Workbench are available as MIT-licensed self-hosted software; authorized local use is separate from paid Cratis support, hosted coordination, or managed operational responsibility.', + 'Arc is an opinionated CQRS application framework for ASP.NET Core with commands, queries, validation, authorization, and TypeScript proxy generation.', + 'Components is a React component library aligned with Arc application patterns.', + 'The Cratis CLI provides terminal workflows for inspecting and diagnosing Chronicle.', + 'Chronicle Workbench provides a bundled local browser surface for authorized inspection of Chronicle runtime state and preview of supported projection behavior.', +]; + +const blockedText = [ + 'Event sourcing you can actually be productive with', + 'Everything fits together', + 'Predictable for AI', + 'Production readiness', + 'The Cratis Stack', + 'Chronicle is an event-sourcing database and processing runtime with a first-class .NET SDK and additional TypeScript, Python, Java, Kotlin/JVM, and Elixir clients', +]; + +async function exists(filePath) { + try { + await fs.access(filePath); + return true; + } catch { + return false; + } +} + +async function* walk(dir) { + for (const entry of await fs.readdir(dir, { withFileTypes: true })) { + const full = path.join(dir, entry.name); + if (entry.isDirectory()) yield* walk(full); + else if (entry.isFile()) yield full; + } +} + +function visibleText(html) { + return html + .replace(/]*>[\s\S]*?<\/script>/gi, ' ') + .replace(/]*>[\s\S]*?<\/style>/gi, ' ') + .replace(/<[^>]+>/g, ' ') + .replace(/&/g, '&') + .replace(/'|'/g, "'") + .replace(/"/g, '"') + .replace(/</g, '<') + .replace(/>/g, '>') + .replace(/\s+/g, ' ') + .trim(); +} + +function outputForRoute(route) { + if (route === '/') return 'index.html'; + if (route === '/404.html') return '404.html'; + return `${route.replace(/^\//, '').replace(/\/$/, '')}/index.html`; +} + +const expectedHtml = new Set(manifest.routes.map((entry) => outputForRoute(entry.route))); +const actualHtml = new Set(); +const actualFiles = new Set(); +const allHtml = []; + +if (!(await exists(distRoot))) { + errors.push('dist is missing'); +} else { + for await (const file of walk(distRoot)) { + const relative = path.relative(distRoot, file).replaceAll('\\', '/'); + actualFiles.add(relative); + if (relative.toLowerCase().endsWith('.html')) { + actualHtml.add(relative); + allHtml.push({ relative, html: await fs.readFile(file, 'utf8') }); + } + if (relative.toLowerCase().endsWith('.md')) { + errors.push(`raw Markdown mirror is not allowlisted: ${relative}`); + } + } +} + +for (const expected of expectedHtml) { + if (!actualHtml.has(expected)) errors.push(`allowlisted HTML route is missing: ${expected}`); +} +for (const actual of actualHtml) { + if (!expectedHtml.has(actual)) errors.push(`unlisted HTML route was built: ${actual}`); +} + +const indexEntry = allHtml.find((entry) => entry.relative === 'index.html'); +if (indexEntry) { + const text = visibleText(indexEntry.html); + for (const sentence of approvedSentences) { + const count = text.split(sentence).length - 1; + if (count !== 1) { + errors.push(`front page must contain approved sentence exactly once (${count} found): ${sentence}`); + } + } + const requiredLinks = ['/chronicle/', '/arc/', '/components/', '/cli/']; + for (const href of requiredLinks) { + if (!indexEntry.html.includes(`href="${href}"`)) errors.push(`front page is missing canonical product link: ${href}`); + } + if (!/[^<]*Cratis documentation[^<]*<\/title>/i.test(indexEntry.html)) { + errors.push('front page title does not identify Cratis documentation'); + } + if (!/<meta\s+[^>]*name="description"[^>]*content="Canonical technical documentation for Chronicle, Arc, Components, and the Cratis CLI\."/i.test(indexEntry.html)) { + errors.push('front page description is missing or differs from the reviewed wording'); + } + const canonicalMatches = [ + /<link\s+[^>]*rel="canonical"[^>]*href="https:\/\/cratis\.io\/"/i, + /<link\s+[^>]*href="https:\/\/cratis\.io\/"[^>]*rel="canonical"/i, + ]; + if (!canonicalMatches.some((pattern) => pattern.test(indexEntry.html))) { + errors.push('front page self-canonical is missing'); + } +} + +for (const { relative, html } of allHtml) { + const text = visibleText(html); + for (const blocked of blockedText) { + if (text.includes(blocked)) errors.push(`${relative} contains blocked wording: ${blocked}`); + } +} + +for (const name of ['llms.txt', 'llms-full.txt']) { + if (await exists(path.join(distRoot, name))) { + errors.push(`${name} must remain disabled until machine output is explicitly allowlisted`); + } +} +if (await exists(path.join(distRoot, 'pagefind'))) { + errors.push('Pagefind must remain disabled until search is explicitly allowlisted'); +} +for (const prefix of ['storybook', 'storybook-arc', 'api']) { + if (await exists(path.join(distRoot, prefix))) { + errors.push(`${prefix} output is not admitted by the public manifest`); + } +} + +const generated = manifest.generatedArtifacts; +if (!generated || typeof generated !== 'object') { + errors.push('manifest.generatedArtifacts is missing'); +} +const generatedPrefix = generated?.prefix ?? ''; +const generatedExtensions = new Set(generated?.extensions ?? []); +const sitemapPattern = new RegExp(generated?.sitemapPattern ?? 'a^'); +const staticFiles = new Set(manifest.staticFiles.map((entry) => entry.path)); + +for (const file of actualFiles) { + if (expectedHtml.has(file) || staticFiles.has(file) || sitemapPattern.test(file)) continue; + if (file.startsWith(generatedPrefix)) { + const extension = path.extname(file).toLowerCase(); + if (generatedExtensions.has(extension)) continue; + } + errors.push(`unlisted public artifact was built: ${file}`); +} +for (const file of staticFiles) { + if (!actualFiles.has(file)) errors.push(`allowlisted static file is missing: ${file}`); +} + +const sitemapUrls = new Set(); +if (await exists(distRoot)) { + for await (const file of walk(distRoot)) { + if (!/^sitemap.*\.xml$/i.test(path.basename(file))) continue; + const xml = await fs.readFile(file, 'utf8'); + for (const match of xml.matchAll(/<loc>([^<]+)<\/loc>/g)) { + if (!match[1].endsWith('.xml')) sitemapUrls.add(match[1]); + } + } +} +const expectedSitemap = new Set( + manifest.routes + .filter((entry) => entry.sitemap) + .map((entry) => new URL(entry.route, 'https://cratis.io').href) +); +for (const expected of expectedSitemap) { + if (!sitemapUrls.has(expected)) errors.push(`sitemap is missing allowlisted URL: ${expected}`); +} +for (const actual of sitemapUrls) { + if (!expectedSitemap.has(actual)) errors.push(`sitemap contains unlisted URL: ${actual}`); +} + +if (errors.length) { + for (const error of errors) console.error(`[public-surface] ${error}`); + process.exit(1); +} + +console.log(`[public-surface] ${expectedHtml.size} exact routes built; sitemap and machine-output boundaries verified`); diff --git a/web/scripts/emit-doc-artifacts.mjs b/web/scripts/emit-doc-artifacts.mjs index 4684c181..2bbc1b50 100644 --- a/web/scripts/emit-doc-artifacts.mjs +++ b/web/scripts/emit-doc-artifacts.mjs @@ -1,6 +1,6 @@ -// Emits static artifacts that Starlight does not create by itself: -// - /path.md mirrors for starlight-page-actions' "View in Markdown" and copy action. -// - Static files synced from product docs, such as Chronicle statistics HTML/JS. +// Emits approved static assets that Starlight does not create by itself. +// Raw Markdown mirrors remain disabled until an explicit Approved-claim allowlist +// controls which canonical pages may be exposed as machine-readable source. // // Run after astro build: node scripts/emit-doc-artifacts.mjs @@ -13,7 +13,7 @@ const webRoot = path.resolve(here, '..'); const docsRoot = path.join(webRoot, 'src', 'content', 'docs'); const distRoot = path.join(webRoot, 'dist'); -const STATIC_EXT = new Set(['.png', '.jpg', '.jpeg', '.gif', '.svg', '.webp', '.avif', '.html', '.js', '.css', '.json']); +const STATIC_EXT = new Set(['.png', '.jpg', '.jpeg', '.gif', '.svg', '.webp', '.avif']); function slugifyPath(p) { return p @@ -32,38 +32,26 @@ async function* walk(dir) { } } -function pageMarkdownOutput(relFile) { - const withoutExt = relFile.replace(/\.(md|mdx)$/i, ''); - const slug = slugifyPath(withoutExt.replace(/(^|\/)index$/i, '$1')); - return path.join(distRoot, slug ? `${slug}.md` : 'index.md'); -} - function staticOutput(relFile) { const dir = slugifyPath(path.dirname(relFile)); const file = path.basename(relFile).toLowerCase().replace(/[^a-z0-9._-]+/g, ''); return path.join(distRoot, dir, file); } -let markdownMirrors = 0; let staticFiles = 0; for await (const file of walk(docsRoot)) { const rel = path.relative(docsRoot, file); const ext = path.extname(file).toLowerCase(); - let outFile; - if (ext === '.md' || ext === '.mdx') { - outFile = pageMarkdownOutput(rel); - markdownMirrors++; - } else if (STATIC_EXT.has(ext)) { - outFile = staticOutput(rel); - staticFiles++; - } else { - continue; - } + if (ext === '.md' || ext === '.mdx') continue; + if (!STATIC_EXT.has(ext)) continue; + + const outFile = staticOutput(rel); + staticFiles++; await fs.mkdir(path.dirname(outFile), { recursive: true }); await fs.copyFile(file, outFile); } -console.log(`[postbuild] emitted ${markdownMirrors} markdown mirrors and ${staticFiles} static doc assets`); +console.log(`[postbuild] emitted ${staticFiles} approved static doc assets; Markdown mirrors disabled`); diff --git a/web/scripts/sync-public-content.mjs b/web/scripts/sync-public-content.mjs new file mode 100644 index 00000000..71daacec --- /dev/null +++ b/web/scripts/sync-public-content.mjs @@ -0,0 +1,277 @@ +import { createHash } from 'node:crypto'; +import { existsSync, promises as fs } from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import yaml from 'js-yaml'; + +const here = path.dirname(fileURLToPath(import.meta.url)); +const webRoot = path.resolve(here, '..'); +const documentationRoot = path.resolve(webRoot, '..'); +const repositoriesRoot = path.resolve(webRoot, '..', '..'); +const manifestPath = path.join(webRoot, 'public-surface.json'); +const outputRoot = path.join(webRoot, 'src', 'content', 'docs'); +const generatedRoot = path.join(webRoot, 'src', 'generated'); +const approvedPublicRoot = path.join(webRoot, '.public-approved'); +const sourcePublicRoot = path.join(webRoot, 'public'); + +const allowedClaims = new Set([ + 'CLM-011', + 'CLM-012', + 'CLM-013', + 'CLM-014', + 'CLM-028', + 'CLM-032', +]); + +const expectedRoutePolicy = [ + ['/', 'index.mdx', 'Documentation', 'web/src/public-pages/index.mdx', ['CLM-011', 'CLM-012', 'CLM-013', 'CLM-014', 'CLM-028', 'CLM-032'], true], + ['/404.html', '404.md', 'Documentation', 'web/src/public-pages/404.md', [], false], + ['/chronicle/', 'chronicle/index.mdx', 'Chronicle', 'Documentation/index.mdx', ['CLM-013', 'CLM-014', 'CLM-028', 'CLM-032'], true], + ['/chronicle/architecture/', 'chronicle/architecture.md', 'Chronicle', 'Documentation/architecture.md', ['CLM-013', 'CLM-014', 'CLM-028'], true], + ['/chronicle/workbench/', 'chronicle/workbench/index.md', 'Chronicle', 'Documentation/workbench/index.md', ['CLM-013', 'CLM-014', 'CLM-032'], true], + ['/arc/', 'arc/index.mdx', 'Arc', 'Documentation/index.mdx', ['CLM-011', 'CLM-012'], true], + ['/components/', 'components/index.mdx', 'Components', 'Documentation/index.mdx', ['CLM-011', 'CLM-012'], true], + ['/cli/', 'cli/index.mdx', 'cli', 'Documentation/index.mdx', ['CLM-013', 'CLM-014'], true], +]; + +const repositoryRoots = { + Documentation: documentationRoot, + Chronicle: firstExisting( + path.join(repositoriesRoot, 'Chronicle'), + path.join(documentationRoot, 'Chronicle') + ), + Arc: firstExisting( + path.join(repositoriesRoot, 'Arc'), + path.join(documentationRoot, 'Arc') + ), + Components: firstExisting( + path.join(repositoriesRoot, 'Components'), + path.join(documentationRoot, 'Components') + ), + cli: firstExisting( + path.join(repositoriesRoot, 'cli'), + path.join(documentationRoot, 'CLI'), + path.join(documentationRoot, 'cli') + ), +}; + +function firstExisting(...candidates) { + return candidates.find((candidate) => existsSync(candidate)) ?? candidates[candidates.length - 1]; +} + +function assertObject(value, name) { + if (!value || typeof value !== 'object' || Array.isArray(value)) { + throw new Error(`${name} must be an object`); + } + return value; +} + +function assertSafeRelative(value, name) { + if (typeof value !== 'string' || !value || path.isAbsolute(value)) { + throw new Error(`${name} must be a non-empty relative path`); + } + const normalized = path.posix.normalize(value.replaceAll('\\', '/')); + if (normalized === '..' || normalized.startsWith('../')) { + throw new Error(`${name} escapes its owning root`); + } + return normalized; +} + +function sha256(content) { + return createHash('sha256').update(content).digest('hex'); +} + +function assertHash(value, name) { + if (typeof value !== 'string' || !/^[a-f0-9]{64}$/.test(value)) { + throw new Error(`${name} must be a lowercase SHA-256 digest`); + } +} + +function frontmatter(text, sourceName) { + if (!text.startsWith('---')) return {}; + const end = text.indexOf('\n---', 3); + if (end === -1) throw new Error(`unterminated frontmatter: ${sourceName}`); + const parsed = yaml.load(text.slice(3, end)); + return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {}; +} + +function pageTopic(route) { + if (route.startsWith('/chronicle/')) return 'chronicle'; + if (route.startsWith('/arc/')) return 'arc'; + if (route.startsWith('/components/')) return 'components'; + if (route.startsWith('/cli/')) return 'cli'; + return null; +} + +function topicsFor(routes) { + const topics = [ + { + id: 'chronicle', + label: 'Chronicle', + link: 'chronicle', + icon: 'seti:db', + items: [ + { label: 'Overview', slug: 'chronicle' }, + { label: 'Architecture', slug: 'chronicle/architecture' }, + { label: 'Workbench', slug: 'chronicle/workbench' }, + ], + }, + { + id: 'arc', + label: 'Arc', + link: 'arc', + icon: 'puzzle', + items: [{ label: 'Overview', slug: 'arc' }], + }, + { + id: 'components', + label: 'Components', + link: 'components', + icon: 'laptop', + items: [{ label: 'Overview', slug: 'components' }], + }, + { + id: 'cli', + label: 'CLI', + link: 'cli', + icon: 'rocket', + items: [{ label: 'Overview', slug: 'cli' }], + }, + ]; + + const available = new Set(routes.map((route) => pageTopic(route)).filter(Boolean)); + return topics.filter((topic) => available.has(topic.id)); +} + +const manifest = assertObject(JSON.parse(await fs.readFile(manifestPath, 'utf8')), 'manifest'); +if (manifest.schemaVersion !== 1) throw new Error('manifest.schemaVersion must be 1'); +if (!Array.isArray(manifest.routes) || manifest.routes.length === 0) { + throw new Error('manifest.routes must be a non-empty array'); +} +if (!Array.isArray(manifest.staticFiles)) throw new Error('manifest.staticFiles must be an array'); +const generatedArtifacts = assertObject(manifest.generatedArtifacts, 'manifest.generatedArtifacts'); +const expectedGeneratedExtensions = ['.css', '.js', '.png', '.svg', '.woff2']; +if (generatedArtifacts.prefix !== '_astro/' || + JSON.stringify(generatedArtifacts.extensions) !== JSON.stringify(expectedGeneratedExtensions) || + generatedArtifacts.sitemapPattern !== '^sitemap-(?:index|[0-9]+)\\.xml$') { + throw new Error('manifest.generatedArtifacts differs from the owning-surface generated-artifact policy'); +} + +const actualRoutePolicy = manifest.routes.map((entry) => [ + entry.route, + entry.contentPath, + entry.source?.repository, + entry.source?.path, + entry.claims, + entry.sitemap, +]); +if (JSON.stringify(actualRoutePolicy) !== JSON.stringify(expectedRoutePolicy)) { + throw new Error('manifest.routes differs from the owning-surface exact route/source/claim policy'); +} +if (manifest.routes.some((entry) => entry.search !== false || entry.machine !== false)) { + throw new Error('search and machine output must remain disabled for every current route'); +} + +const declaredClaims = new Set(manifest.approvedClaimIds ?? []); +if (declaredClaims.size !== allowedClaims.size || [...allowedClaims].some((claim) => !declaredClaims.has(claim))) { + throw new Error('manifest.approvedClaimIds differs from the owning-surface Approved claim allowlist'); +} +if (declaredClaims.has('CLM-010')) throw new Error('Draft CLM-010 cannot enter the public manifest'); + +const seenRoutes = new Set(); +const seenContent = new Set(); +await fs.rm(outputRoot, { recursive: true, force: true }); +await fs.mkdir(outputRoot, { recursive: true }); + +for (const [index, rawRoute] of manifest.routes.entries()) { + const route = assertObject(rawRoute, `routes[${index}]`); + if (typeof route.route !== 'string' || !route.route.startsWith('/')) { + throw new Error(`routes[${index}].route must start with /`); + } + if (seenRoutes.has(route.route)) throw new Error(`duplicate public route: ${route.route}`); + seenRoutes.add(route.route); + + const contentPath = assertSafeRelative(route.contentPath, `routes[${index}].contentPath`); + if (seenContent.has(contentPath)) throw new Error(`duplicate content path: ${contentPath}`); + seenContent.add(contentPath); + + const source = assertObject(route.source, `routes[${index}].source`); + const repositoryRoot = repositoryRoots[source.repository]; + if (!repositoryRoot) throw new Error(`unknown source repository: ${source.repository}`); + if (source.repository === 'Documentation') { + if (source.revision !== 'self') throw new Error(`Documentation source ${route.route} must use revision self`); + } else if (typeof source.revision !== 'string' || !/^[a-f0-9]{40}$/.test(source.revision)) { + throw new Error(`routes[${index}].source.revision must be a full commit SHA`); + } + const sourcePath = assertSafeRelative(source.path, `routes[${index}].source.path`); + assertHash(source.sha256, `routes[${index}].source.sha256`); + + if (!Array.isArray(route.claims)) throw new Error(`routes[${index}].claims must be an array`); + for (const claim of route.claims) { + if (!allowedClaims.has(claim)) throw new Error(`route ${route.route} uses unapproved claim ${claim}`); + } + if (route.claims.includes('CLM-010')) throw new Error(`route ${route.route} uses Draft CLM-010`); + + const absoluteSource = path.join(repositoryRoot, sourcePath); + const realRepositoryRoot = await fs.realpath(repositoryRoot); + const realSource = await fs.realpath(absoluteSource); + if (realSource !== realRepositoryRoot && !realSource.startsWith(`${realRepositoryRoot}${path.sep}`)) { + throw new Error(`source escapes its owning repository through a symlink: ${source.repository}:${sourcePath}`); + } + const content = await fs.readFile(realSource); + const actualHash = sha256(content); + if (actualHash !== source.sha256) { + throw new Error(`source hash drift for ${source.repository}:${sourcePath}; expected ${source.sha256}, got ${actualHash}`); + } + const text = content.toString('utf8'); + const metadata = frontmatter(text, `${source.repository}:${sourcePath}`); + if (metadata.draft === true) { + throw new Error(`allowlisted source is still Draft: ${source.repository}:${sourcePath}`); + } + const normalizedText = text.replace(/\s+/g, ' ').toLowerCase(); + const blockedClientTerms = ['typescript', 'python', 'java', 'kotlin/jvm', 'elixir']; + const blockedProviderTerms = ['mongodb', 'postgresql', 'sql server', 'sqlite']; + if (blockedClientTerms.every((term) => normalizedText.includes(term)) || + blockedProviderTerms.every((term) => normalizedText.includes(term))) { + throw new Error(`allowlisted source contains Draft CLM-010 client/provider matrix wording: ${source.repository}:${sourcePath}`); + } + + const destination = path.join(outputRoot, contentPath); + await fs.mkdir(path.dirname(destination), { recursive: true }); + await fs.writeFile(destination, content); + console.log(`[public-sync] ${route.route} <- ${source.repository}:${sourcePath}`); +} + +await fs.rm(approvedPublicRoot, { recursive: true, force: true }); +await fs.mkdir(approvedPublicRoot, { recursive: true }); +for (const [index, rawStatic] of manifest.staticFiles.entries()) { + const staticFile = assertObject(rawStatic, `staticFiles[${index}]`); + const relative = assertSafeRelative(staticFile.path, `staticFiles[${index}].path`); + assertHash(staticFile.sha256, `staticFiles[${index}].sha256`); + const source = path.join(sourcePublicRoot, relative); + const realPublicRoot = await fs.realpath(sourcePublicRoot); + const realSource = await fs.realpath(source); + if (realSource !== realPublicRoot && !realSource.startsWith(`${realPublicRoot}${path.sep}`)) { + throw new Error(`static file escapes public root through a symlink: ${relative}`); + } + const content = await fs.readFile(realSource); + const actualHash = sha256(content); + if (actualHash !== staticFile.sha256) { + throw new Error(`static-file hash drift for ${relative}; expected ${staticFile.sha256}, got ${actualHash}`); + } + const destination = path.join(approvedPublicRoot, relative); + await fs.mkdir(path.dirname(destination), { recursive: true }); + await fs.writeFile(destination, content); +} + +await fs.mkdir(generatedRoot, { recursive: true }); +await fs.writeFile( + path.join(generatedRoot, 'topics.json'), + `${JSON.stringify(topicsFor([...seenRoutes]), null, 2)}\n` +); +await fs.writeFile( + path.join(generatedRoot, 'public-routes.json'), + `${JSON.stringify([...seenRoutes].sort(), null, 2)}\n` +); + +console.log(`[public-sync] materialized ${seenRoutes.size} exact routes and ${manifest.staticFiles.length} static files`); diff --git a/web/src/content/docs/404.md b/web/src/content/docs/404.md deleted file mode 100644 index ba608e9e..00000000 --- a/web/src/content/docs/404.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -title: Page not found -description: That page doesn't exist — here's how to get back on track. -template: splash -editUrl: false -hero: - title: "404" - tagline: That page wandered off. Let's get you back to something useful. - actions: - - text: Documentation home - link: / - icon: right-arrow - - text: Get started with Chronicle - link: /chronicle/get-started/ - variant: minimal ---- - -Looking for something specific? Try the **search** in the top bar, or jump to a product: - -- [Chronicle](/chronicle/) — the event sourcing platform -- [Arc](/arc/) — the full-stack application framework -- [Components](/components/) — the React component library diff --git a/web/src/content/docs/adopting-cratis.mdx b/web/src/content/docs/adopting-cratis.mdx deleted file mode 100644 index 2aa7f8cb..00000000 --- a/web/src/content/docs/adopting-cratis.mdx +++ /dev/null @@ -1,91 +0,0 @@ ---- -title: Adopting Cratis -description: Where to start with Cratis — default to the full event-sourced stack for new information systems, or adopt one product at a time in existing systems. ---- - -import { CardGrid, Aside } from '@astrojs/starlight/components'; -import SimpleCard from '@components/SimpleCard.astro'; -import TopicHero from '@components/TopicHero.astro'; - -<TopicHero icon="rocket" eyebrow="Getting oriented" title="Where to start"> -Cratis products stand on their own and compose when you want them to. For a new information system, our default is the full loop: Chronicle for event sourcing, Arc for CQRS, Components for React, and AuthProxy at the edge. For an existing system or a bounded current-state slice, you can adopt one piece at a time without losing the path back to the full stack. -</TopicHero> - -## Pick your entry point - -Two things decide where you start: **what you're building on** — a new codebase or an existing one — and **which boundary you are changing first**. Event sourcing is the default architecture we recommend for information systems; CQRS, generated contracts, and edge identity can still be adopted independently. - -```mermaid -flowchart TD - Q1{New information system<br/>or existing?} - Q1 -->|New information system| FULL["Default stack:<br/>Arc + Chronicle + Components"] - Q1 -->|Bounded CRUD slice| ARC["Arc + Components<br/>over MongoDB / EF Core"] - Q1 -->|Existing| Q3{What hurts<br/>most?} - Q3 -->|Frontend/backend glue| ADDARC["Add Arc to your app"] - Q3 -->|Audit & history| ADDCHR["Introduce Chronicle,<br/>one slice at a time"] -``` - -| Your situation | Start with | Why | -| --- | --- | --- | -| New information system | **Arc + Chronicle + Components** | The default Cratis loop: CQRS at the boundary, event history underneath, React generated from C# — the [full-stack capstone](/build-a-full-app/) builds the shape. | -| Bounded slice where current state is genuinely enough | **Arc + Components** over a database | Typed commands, queries, and a generated React client with CQRS but no event log — [CQRS without event sourcing](/arc/arc-without-event-sourcing/). | -| Existing .NET API, frontend glue is the pain | **Arc**, added to your app | A typed TS client for new endpoints, without rewriting what you have. | -| Existing system, you need history or audit | **Chronicle**, incrementally | Event-source one bounded context; leave the rest as it is. | -| Need to separate CQRS from event sourcing | Read first | [CQRS without event sourcing](/arc/arc-without-event-sourcing/), then [Why developers choose Cratis](/why-cratis/). | - -## Starting from scratch (greenfield) - -A new project is the easy case — nothing to migrate, no constraints. For an information system, start with the whole loop unless you have a clear reason not to: - -- **Default to the full experience.** Scaffold the stack and follow [getting started](/chronicle/get-started/), then the [full-stack capstone](/build-a-full-app/). You'll have the command → event → projection → query → React loop running, with history available from the event log. Scaffolding wires Arc and Chronicle into one host through the [Cratis package](/arc/backend/chronicle/cratis-package/)'s `AddCratis`/`UseCratis`. -- **Use Arc-only for deliberate current-state slices.** If a bounded part of the app is just reference data, settings, or an adoption step, [Arc over a database](/arc/arc-without-event-sourcing/) still gives you CQRS, generated proxies, and live queries. Because adopting Chronicle is a write-side change, that slice can move to events later without touching your queries or frontend. - -<Aside type="tip" title="CQRS and event sourcing are independent"> -Arc gives you CQRS: commands for information entering the system, queries for information leaving it. Chronicle gives you event sourcing: facts as the durable source of truth. They fit together extremely well, but either one can stand without the other. -</Aside> - -## Adding to an existing system (brownfield) - -You rarely get to start clean. Cratis is built to be adopted a slice at a time, alongside code that already works — you don't rewrite, you grow into it. - -### Add Arc to an existing ASP.NET Core app - -If your pain is the **frontend-to-backend boundary** — hand-written controllers, DTOs duplicated in TypeScript, fetch wrappers that drift out of sync — Arc slots into an app you already have. Register it on your existing host and start expressing *new* endpoints as commands and queries; Arc generates their typed proxies while your existing controllers keep running untouched. - -Arc's persistence meets your data where it already lives: its [MongoDB](/arc/backend/mongodb/) and [Entity Framework](/arc/backend/entity-framework/) integrations read and write the database you already use — EF Core even has a [direct-registration path](/arc/backend/entity-framework/getting-started/) designed for slotting into an existing application without taking over the whole framework. For controllers and MediatR handlers, the [MediatR, MVC, and Arc](/arc/coming-from-mediatr-and-mvc/) bridge maps familiar concepts onto Arc's model. - -### Introduce Chronicle into an existing domain - -If your pain is **history** — you need an audit trail, a "how did this order get into this state?" view, or a new read model the current schema can't serve — you don't have to event-source the whole system in one move. Pick the bounded context where the model is clearest and event-source that first: - -1. Model the facts for that area as [events](/chronicle/concepts/event/), and append them when the corresponding things happen — for new behavior, append the event; for existing writes, append alongside the current write. -2. Build [projections](/chronicle/projections/) that fold those events into exactly the read models that context needs. They can sit right next to your existing tables. -3. Let the rest of the system keep working as it is. Event sourcing earns its place one context at a time, not as a big-bang rewrite. - -The [CRUD, EF Core, and Chronicle](/chronicle/coming-from-crud/) guide maps tables and `SaveChanges` onto events — that's the mental shift that makes this click. - -<Aside type="caution" title="Keep the boundary honest"> -Event sourcing is the default for information systems, not a mandate that every settings table needs a stream. Use Chronicle for the facts and processes that define the domain, and leave genuinely current-state-only corners on Arc-over-a-database. [When to use event sourcing](/chronicle/concepts/when-to-use-event-sourcing/) is the honest filter. -</Aside> - -## Grow into the full stack - -However you start, the pieces are designed to be *added*, not swapped out. The common growth paths: - -- **Arc-over-a-database → add Chronicle.** Move a slice's write side from a direct insert to appending an event with a projection behind it. The query and the React don't change — see the side-by-side in [CQRS without event sourcing](/arc/arc-without-event-sourcing/). -- **Chronicle-only service → add Arc and a frontend.** Already event-sourcing from a worker or service? Put Arc in front to expose typed commands and queries, and Components to render them — without changing how your events are stored. -- **Any backend → add Components.** The React library consumes Arc's generated proxies, so adding it is a frontend-only step. - -## Next steps - -<CardGrid> - <SimpleCard title="Why developers choose Cratis" icon="open-book" link="/why-cratis/"> - How the three products stand alone and compose — the map this page navigates. - </SimpleCard> - <SimpleCard title="CQRS without event sourcing" icon="puzzle" link="/arc/arc-without-event-sourcing/"> - The standalone Arc shape, and the write-side change that moves a slice to Chronicle. - </SimpleCard> - <SimpleCard title="Build the full loop" icon="rocket" link="/build-a-full-app/"> - The full-stack capstone — command to event to live React screen, with history. - </SimpleCard> -</CardGrid> diff --git a/web/src/content/docs/ai.mdx b/web/src/content/docs/ai.mdx deleted file mode 100644 index 882388da..00000000 --- a/web/src/content/docs/ai.mdx +++ /dev/null @@ -1,141 +0,0 @@ ---- -title: AI-native development -description: Cratis combines framework-aware AI workflows with documented CLI and MCP operating tools. Learn what maintainers can use today, what is still in distribution preview, and where each agent ecosystem fits. -slug: ai ---- - -import { CardGrid, Aside } from '@astrojs/starlight/components'; -import SimpleCard from '@components/SimpleCard.astro'; -import TopicHero from '@components/TopicHero.astro'; - -<TopicHero icon="rocket" eyebrow="The Cratis Stack" title="Build and operate with AI agents"> -Cratis is designed to give an AI assistant the same domain vocabulary, framework boundaries, and verification gates your team uses. Cratis maintainers use framework-aware workflows in Cratis repositories today; external portable coding skills remain in distribution preview. The CLI and Chronicle MCP provide the documented operating side now. -</TopicHero> - -## Why "AI-native" - -An AI assistant is only as good as what it knows about your framework and your system. Drop a general-purpose agent into a Cratis codebase and it will *guess* — inventing handler classes, missing the vertical-slice conventions, hand-writing the API client you don't need. Point it at a running event store and it has no idea how to read the log or recover a stuck observer. - -Cratis closes the operating gap with documented CLI and MCP setup today. For coding workflows, it separates the repository-local setup used by Cratis maintainers from a generated portable distribution that remains gated until its first real target and release are approved. - -## Build with AI: skills that know the Cratis way - -The **Cratis AI configuration** teaches an assistant the conventions—vertical -slices, `[Command]` with `Handle()` on the record, model-bound projections, and -`ConceptAs<T>` instead of raw primitives. - -Cratis-owned repositories currently carry repository-local rules and host -adapters, so maintainers can use these workflows today. The public portable -skills are being generated for Agent Skills, Claude Code, Codex, GitHub Copilot, -Cursor, Kiro, Junie, Gemini CLI, and Pi, but they are not released for external -installation yet. The public distribution repository remains fixture-only until -source, target, canary, and release gates pass. - -Among the workflows available to Cratis maintainers: - -- **`new-vertical-slice`** — scaffold a whole feature end to end: command, events, projection, query, React, and specs. -- **`cratis-command`**, **`cratis-readmodel`**, **`add-projection`**, **`add-reactor`**, **`add-concept`** — build one artifact correctly, by convention. -- **`scaffold-feature`**, **`write-specs`**, **`review-code`** — set up a feature folder, cover it with BDD specs, and review the result against the project's standards. - -Because the skills encode the conventions, an agent that uses them produces slices that look like the rest of your codebase — not a layered approximation of it. (The configuration is the canonical `.ai/` source in the Cratis AI repository, dropped into a project as its `.claude/` and `.github/` folders.) - -Start with [AI setup and current availability](/ai/getting-started/), then use -the [ecosystem support matrix](/ai/ecosystems/) to see what is verified and what -remains blocked. [Plugins](/plugins/) explains the shared skill model, while the -[code-analysis gates](/code-analysis/) enforce framework contracts during the -build. - -<Aside type="tip" title="Model first, then generate"> -Pair this with [Studio](/studio/) or [Screenplay](/screenplay/): model the feature on the canvas or as a declarative `.play` script, generate the C# shapes, then let an agent flesh out the slices around them. Design → generate → build, with AI at each step. -</Aside> - -## Operate with AI: teach your assistant your store - -Building is only half the loop. The other half is *operating* what you built — and Cratis makes the running store legible to an assistant two ways. - -### `cratis init` — the CLI, made AI-aware - -Run it once inside your project: - -```bash -cratis init -``` - -It writes a **`CHRONICLE.md`** describing every command the CLI can run, installs instruction files for **Claude Code, GitHub Copilot, Cursor, and Windsurf**, and adds a **`chronicle-diagnose`** slash command. From then on your assistant knows how to browse events, watch observers, and diagnose a stuck partition through the [CLI](/cli/) — because the whole command catalog is in its context. Refresh it after a CLI upgrade with `cratis init --refresh`. The [CLI getting started](/cli/getting-started/) guide walks through it in full. - -### The Chronicle MCP server — an agent, connected to the store - -For tools that speak the **Model Context Protocol**, Cratis publishes a containerized MCP server that connects straight to a running Chronicle store. Point your tool at it with an `mcp.json`: - -```json -{ - "servers": { - "Chronicle": { - "type": "stdio", - "command": "docker", - "args": ["run", "-i", "--rm", - "-eCratis__Chronicle__Mcp__ConnectionString=chronicle://host.docker.internal:35000", - "cratis/chronicle-mcp"] - } - } -} -``` - -With it connected, your assistant can — in plain language — do things like: - -- **Explore** — list event stores and sequences, show event types and their schemas, and read the event log or the events for a single event source. -- **Run observers** — list observers, replay an observer or one partition, and recover a failed partition. -- **Act on recommendations** — list, perform, or ignore the store's recommendations. -- **Manage jobs** — resume, stop, or delete a running job. - -It needs a Chronicle server running; everything it does, it does against the live store. - -<Aside type="note" title="Operate, not mutate"> -The MCP server and the CLI are *operate-and-inspect* tools — the agent reads the log and manages observers and jobs. To change application state you still go through commands and events. History stays honest. -</Aside> - -## The whole loop, AI-accelerated - -```mermaid -flowchart LR - subgraph Build["Build with AI"] - SK["Cratis AI skills<br/>model · build · review"] - end - subgraph Operate["Operate with AI"] - IN["cratis init<br/>CLI command catalog"] - MCP["Chronicle MCP server<br/>connected to the store"] - end - SK -->|scaffold slices| APP["Your Cratis app"] - APP -->|run it| OP["Running Chronicle store"] - IN -.->|inspect & diagnose| OP - MCP -.->|browse · replay · recover| OP -``` - -## Where to go next - -<CardGrid> - <SimpleCard title="Get started with AI" icon="rocket" link="/ai/getting-started/"> - Set up the operating tools today and understand the coding-skills release status. - </SimpleCard> - <SimpleCard title="Ecosystem support" icon="puzzle" link="/ai/ecosystems/"> - See generated formats and verification status across supported AI hosts. - </SimpleCard> - <SimpleCard title="Trust and distribution" icon="approve-check" link="/ai/trust-and-distribution/"> - Learn how approvals, generated bytes, canaries, rollback, and project context fit together. - </SimpleCard> - <SimpleCard title="The Cratis Stack" icon="rocket" link="/cratis-stack/"> - How design, build, and operate fit together end to end — AI accelerates every step. - </SimpleCard> - <SimpleCard title="CLI getting started" icon="rocket" link="/cli/getting-started/"> - Install the CLI, connect to your store, and run `cratis init` to set up AI tooling. - </SimpleCard> - <SimpleCard title="Plugins" icon="puzzle" link="/plugins/"> - The agents, skills, and coding rules in detail — and how each AI assistant loads them. - </SimpleCard> - <SimpleCard title="Code analysis" icon="approve-check" link="/code-analysis/"> - The Roslyn analyzers and ESLint rules that enforce the same conventions during the build. - </SimpleCard> - <SimpleCard title="Vertical slices" icon="seti:folder" link="/arc/vertical-slices/"> - The convention the build-side skills follow — everything for a feature in one folder. - </SimpleCard> -</CardGrid> diff --git a/web/src/content/docs/ai/cratis-maintainers.mdx b/web/src/content/docs/ai/cratis-maintainers.mdx deleted file mode 100644 index 613c36a2..00000000 --- a/web/src/content/docs/ai/cratis-maintainers.mdx +++ /dev/null @@ -1,113 +0,0 @@ ---- -title: Use AI as a Cratis maintainer -description: Follow the Cratis repository profile, project context, skill, verification, review, and shipping workflow without copying the shared AI corpus between repositories. ---- - -import { Steps, Aside } from '@astrojs/starlight/components'; - -Cratis maintainers do not ask a general assistant to guess the framework. Each -Cratis repository already carries the instructions and host adapters needed for -its current development workflow. Your job is to start in the right repository, -state the outcome, and require fresh verification before shipping. - -## Work from the repository root - -<Steps> - -1. **Start the assistant in the repository you intend to change** - - The root `AGENTS.md` identifies whether the repository is a Cratis - application, framework, client, documentation site, or another supported - profile. Profile selection matters: application vertical-slice rules do not - apply to Chronicle, Arc, Components, or client-library source repositories. - -2. **Read project-owned context** - - If the repository has project-specific context, preserve it. Current - repositories may still use `.agents/PROJECT.md`; the controlled migration - design uses `.cratis/PROJECT.md` with small host bootstraps. Never replace, - concatenate, or copy another repository's project context. - -3. **Ask for the outcome, not an implementation pattern** - - Good requests name the behavior and constraints: - - - "Model the registration flow before implementation." - - "Add a strongly typed invoice number." - - "Diagnose why this projection stopped updating." - - "Review this change for Cratis architecture and security." - - The matching skill chooses the Cratis workflow. If you know the skill, you - can invoke it explicitly using your host's skill picker or mention syntax. - -4. **Let the repository answer routine questions** - - The assistant should inspect local rules, source, generated patterns, and - specs before interrupting you. Stop only for product choices, unavailable - authority, credentials, risky production actions, or genuinely conflicting - evidence. - -5. **Require fresh gates** - - Compilation is not behavior evidence. Run the repository's current build, - specs, lint, generated-file, and documentation gates. Re-run the failing gate - after a correction. - -6. **Ship through a reviewed pull request** - - Use logical commits, green CI, and the repository's merge policy. When a task - creates a new Cratis repository, register it through a linked - `Cratis/Strategy` issue so Strategy can establish ownership, metadata, and AI - setup with its own rules. - -</Steps> - -## Common maintainer workflows - -| Intent | Skill or capability | -| --- | --- | -| Design unsettled commands, facts, streams, views, or reactions | `event-modeling` | -| Build a complete application behavior | `new-vertical-slice` | -| Add a command, read model, projection, reactor, reducer, or concept | The matching focused Cratis skill | -| Diagnose source behavior | `diagnose-slice` | -| Inspect a running Chronicle store | `inspect-running-chronicle` and the CLI | -| Review correctness, architecture, security, or performance | The matching review skill | -| Write product or site documentation | The Cratis documentation skills | -| Commit, push, open, monitor, and merge a PR | `ship-changes` | - -<Aside type="note" title="Skills guide the work; analyzers still enforce contracts"> -The AI workflow does not replace Arc or Chronicle analyzers, compiler errors, -specifications, CI, or human review. It helps an agent reach the same gates a -maintainer would use. -</Aside> - -## Do not propagate the corpus - -Do **not** copy shared `.ai`, `.claude`, `.github`, `.agents`, or `.pi` trees from -one repository to another. The old all-to-all propagation model is frozen -because it created mixed versions, ownership ambiguity, and accidental fan-out. - -The replacement model is: - -1. author and approve canonical capabilities in `Cratis/AI`; -2. generate immutable, host-native artifacts into `Cratis/AI.Distribution`; -3. install or pin one reviewed version through the host or organization scope; -4. keep only project facts and minimal host bootstraps in the consuming - repository; -5. canary updates, observe failures, and roll back by version. - -Existing repository-local AI files remain in place until the replacement has -real canary and rollback evidence. Do not delete them early, and do not restart -legacy propagation. - -## Keep shared and project knowledge separate - -Shared skills own Cratis conventions and reusable workflows. The consuming -repository owns architecture decisions, commands, environments, credentials, -fixtures, and verification details. That separation lets a shared package -update without overwriting the facts that make one project different from -another. - -Read [Trust and distribution](/ai/trust-and-distribution/) for the complete -boundary and [Start using AI with Cratis](/ai/getting-started/) for the public -setup status. diff --git a/web/src/content/docs/ai/ecosystems.mdx b/web/src/content/docs/ai/ecosystems.mdx deleted file mode 100644 index 064001db..00000000 --- a/web/src/content/docs/ai/ecosystems.mdx +++ /dev/null @@ -1,75 +0,0 @@ ---- -title: AI ecosystem support -description: See how Cratis AI skills map to Agent Skills, Claude Code, Codex, GitHub Copilot, Cursor, Kiro, Junie, Gemini CLI, Pi, and npm—and which gates remain before public installation. ---- - -import { Aside } from '@astrojs/starlight/components'; - -Cratis authors one passive skill tree and generates the wrapper each host expects. -A wrapper changes packaging and discovery—not the behavior of the skill. This -keeps a projection workflow or event-modeling workflow consistent when your team -uses different assistants. - -<Aside type="caution" title="No supported public coding-skills release yet"> -The formats below have fixture generation evidence. The public distribution -repository is intentionally marked fixture-only, and no real Cratis skill target -is approved for installation. Do not treat generated `main` as a release. -</Aside> - -## Current matrix - -| Ecosystem | Generated form | Current evidence | Public install status | -| --- | --- | --- | --- | -| Agent Skills | `skills/<name>/SKILL.md` | Format and name/frontmatter validation | Blocked pending an approved target | -| Claude Code | Skills-only plugin and marketplace | Strict validation plus isolated install/list/remove smoke | Blocked pending an immutable release | -| OpenAI Codex / ChatGPT | Skills-only Codex plugin and marketplace | Manifest generation and Codex marketplace add/remove smoke | Blocked; ChatGPT desktop review remains | -| GitHub Copilot | Skills-only plugin and marketplace | Isolated marketplace install/list/remove smoke | Blocked pending an immutable release | -| Cursor | Skills-only Cursor plugin and marketplace | Manifest and canonical-byte parity | Blocked; Cursor host and vendor review remain | -| Kiro | Agent Plugins 1.0 skills-only power | Agent Plugin manifest and canonical-byte parity | Blocked; Kiro host review remains | -| Junie | Skills-only Junie extension | Extension manifest and canonical-byte parity | Blocked; Junie host review remains | -| Gemini CLI | Skills-only extension | Isolated link/list/remove smoke | Blocked pending an immutable release | -| Pi | Passive `pi-package` npm layout | Isolated install/list/remove smoke | Blocked pending package ownership and release | -| npm | `@cratis/ai` passive tarball | Pack/install/uninstall with scripts disabled | Blocked pending trusted publishing | - -## What every adapter must preserve - -Every generated target must contain the same approved `SKILL.md`, references, -assets, and required licenses. Target-native manifests may add names, versions, -and discovery roots, but they may not redefine workflow behavior. - -The passive package excludes: - -- lifecycle scripts and executable extensions; -- MCP implementations and credentials; -- engineering rules, agents, hooks, prompts, evals, and authoring tooling; -- project-specific context; -- private product or Strategy content. - -## Why installation is still closed - -Packaging is working, but packaging is not approval. Public installation opens -only after one Cratis product owner approves exact source bytes and claims, the -target passes behavior and collision evidence, an immutable generated release is -reviewed, and a real consuming repository proves install, update, rollback, and -emergency disable. - -The manual gates are tracked in: - -- [`Cratis/AI#148`](https://github.com/Cratis/AI/issues/148) — first approved - public target and source contract; -- [`Cratis/Workflows#72`](https://github.com/Cratis/Workflows/issues/72) — - PR/release-capable distribution bot; -- [`Cratis/Workflows#70`](https://github.com/Cratis/Workflows/issues/70) — npm - package ownership and stage-only trusted publishing; -- [`Cratis/Workflows#71`](https://github.com/Cratis/Workflows/issues/71) — first - real consumer canary and rollback; -- [`Cratis/AI#147`](https://github.com/Cratis/AI/issues/147) — vendor marketplace - submissions after release approval. - -## What to do now - -Use [`cratis init`](/cli/getting-started/) and Chronicle MCP for documented -operating workflows today. Cratis maintainers can use the existing repository- -local AI setup as described in [Using AI as a Cratis maintainer](/ai/cratis-maintainers/). -External application teams should wait for the first immutable release rather -than copying internal repository trees. diff --git a/web/src/content/docs/ai/getting-started.mdx b/web/src/content/docs/ai/getting-started.mdx deleted file mode 100644 index 6e972481..00000000 --- a/web/src/content/docs/ai/getting-started.mdx +++ /dev/null @@ -1,106 +0,0 @@ ---- -title: Start using AI with Cratis -description: Set up the Cratis CLI and agent-facing operating tools today, understand the coding-skills release status, and choose the right next step for your team. ---- - -import { Steps, Aside, CardGrid } from '@astrojs/starlight/components'; -import SimpleCard from '@components/SimpleCard.astro'; - -A general coding assistant can read C#, but it does not automatically understand -Cratis commands, facts, event sources, projections, or vertical slices. Cratis -closes that gap in two places: it gives the assistant documented operating tools -today, and it is preparing portable coding skills for supported agent ecosystems. - -<Aside type="caution" title="Coding-skills distribution is still a preview"> -The public [`Cratis/AI.Distribution`](https://github.com/Cratis/AI.Distribution) -repository currently contains **fixture-only generated bytes**. It is not a -supported installation or release target. Do not install its `main` branch into -a project. This page separates what you can use now from what is still gated. -</Aside> - -## Set up the operating tools - -<Steps> - -1. **Install the Cratis CLI** - - Follow the [CLI getting-started guide](/cli/getting-started/) for your - platform, then open a terminal in your Cratis project. - -2. **Generate the agent-facing CLI context** - - ```bash - cratis init - ``` - - The command writes the CLI context and supported host instructions for the - project. Your assistant can now discover documented Chronicle inspection and - diagnosis commands instead of inventing shell syntax. - -3. **Refresh after a CLI upgrade** - - ```bash - cratis init --refresh - ``` - - Refreshing keeps the assistant's command catalog aligned with the CLI version - you actually run. - -4. **Connect Chronicle MCP when your host supports MCP** - - Use the [Chronicle MCP setup guide](/chronicle-mcp/) to connect an approved - host to a running Chronicle store. Review every requested capability before - enabling mutating operational tools. - -</Steps> - -You now have the operating side of Cratis AI: the assistant can use documented -CLI or MCP surfaces to inspect Chronicle with the same vocabulary you use in the -application. - -## Use coding skills in a Cratis-owned repository - -Cratis maintainers can use the repository-local AI setup already present in -Cratis repositories. Start your agent from the repository root so it reads -`AGENTS.md` and the repository's current host adapter. Ask for the outcome in -plain language—for example, "add a projection," "model this workflow," or -"review these changes"—and let the matching skill apply the repository profile -and quality gates. - -See [Using AI as a Cratis maintainer](/ai/cratis-maintainers/) for the complete -workflow and the rules for project context, verification, and shipping. - -## Set up coding skills in your own repository - -Portable public skills are not released yet. Cratis is validating one canonical -skill tree across Agent Skills, Claude Code, Codex, GitHub Copilot, Cursor, Kiro, -Junie, Gemini CLI, Pi, and npm packaging before opening installation. - -Until an immutable release is published: - -- do not copy `.ai`, `.claude`, `.github`, or skill folders from another - repository; -- do not install the fixture-only distribution repository; -- keep your project-specific facts in your repository rather than in shared - skills; -- use `cratis init` and Chronicle MCP for the currently documented operating - workflow. - -The [ecosystem support matrix](/ai/ecosystems/) shows the generated format and -current verification level for every host. The [trust and distribution model](/ai/trust-and-distribution/) -explains why Cratis uses versioned generated artifacts instead of copying one -repository into another. - -## Choose your next step - -<CardGrid> - <SimpleCard title="Cratis maintainers" icon="seti:folder" link="/ai/cratis-maintainers/"> - Work effectively with the repository-local rules and skills already used by the Cratis team. - </SimpleCard> - <SimpleCard title="Agent ecosystems" icon="puzzle" link="/ai/ecosystems/"> - See Claude, Codex, Copilot, Cursor, Kiro, Junie, Gemini, Pi, and npm status in one place. - </SimpleCard> - <SimpleCard title="Trust and distribution" icon="approve-check" link="/ai/trust-and-distribution/"> - Understand generated packages, approvals, canaries, rollback, and why propagation stays frozen. - </SimpleCard> -</CardGrid> diff --git a/web/src/content/docs/ai/trust-and-distribution.mdx b/web/src/content/docs/ai/trust-and-distribution.mdx deleted file mode 100644 index 249a22b7..00000000 --- a/web/src/content/docs/ai/trust-and-distribution.mdx +++ /dev/null @@ -1,98 +0,0 @@ ---- -title: Trust and distribution for Cratis AI -description: Understand why Cratis generates host-native AI packages from one approved source, how project context stays local, and which approval, canary, rollback, and publication gates protect users. ---- - -import { Aside } from '@astrojs/starlight/components'; - -An AI skill can influence every file an assistant reads or writes. Treating it -like a copied snippet makes updates hard to audit and rollback. Cratis therefore -separates authoring, generated distribution, host packaging, and project-owned -context. - -## One source, generated packages - -```mermaid -flowchart LR - A[Cratis/AI<br/>author and evaluate] -->|approved exact bytes| G[empty generated stage] - G -->|manifest + checksums + provenance| D[Cratis/AI.Distribution] - D --> C[Claude / Codex / Copilot / Cursor] - D --> K[Kiro / Junie / Gemini / Pi] - C --> P[version-pinned project] - K --> P - X[project-owned context] --> P -``` - -`Cratis/AI` is the canonical authoring and approval repository. A generator -selects an exact positive allowlist into an empty stage, verifies byte parity, -and emits target-native manifests. The generated repository contains no -hand-authored behavior. - -`Cratis/AI.Distribution` is public but currently fixture-only. Its protected -`main` branch demonstrates the generated boundary; it does not grant public -installation or release status. - -## Why Cratis does not propagate folders - -The previous model copied shared AI folders across repositories. That creates -three problems: - -1. repositories silently drift to different corpus versions; -2. a target can accidentally become another distribution source; -3. rollback means reconstructing overwritten files instead of changing a pin. - -The replacement is ordinary versioned distribution. One source produces one -release manifest. Consumers install or pin that version through their host. -Workflows can canary, update, disable, and roll it back without rewriting the -project's own facts. - -## Project context remains project-owned - -Shared packages teach reusable Cratis concepts and workflows. They do not own a -project's architecture, environment names, commands, credentials, test fixtures, -or product decisions. - -The controlled context design uses `.cratis/PROJECT.md` as canonical project -content, with minimal host bootstraps where a host cannot discover it directly. -Some current repositories still use `.agents/PROJECT.md` during migration. A -resolver chooses one; it never merges, overwrites, or deletes either file. - -<Aside type="note" title="Uninstall must leave the project intact"> -Removing a shared AI package must remove shared capabilities only. It must not -delete `AGENTS.md`, `CLAUDE.md`, `GEMINI.md`, `.cratis/PROJECT.md`, legacy project -context, or repository-local overrides. -</Aside> - -## The release gates - -A public capability moves through separate gates: - -| Gate | Evidence required | -| --- | --- | -| Source authority | Owning product repository, immutable revision, owner, permission, claims, digest, and expiry | -| Target approval | Behavior, trigger, negative trigger, collision, security, originality, and portability evidence | -| Materialization | Exact file closure, native manifests, byte parity, checksums, and provenance | -| Package lifecycle | Pack, install, discovery, smoke, update, uninstall, and rollback | -| Canary | One approved consuming repository with observable version-bound results | -| Publication | Protected environment, machine identity, reviewer, immutable release, and vendor/npm approval | -| Retirement | Fleet visibility, rollback evidence, emergency disable, and proof the old topology cannot restart | - -A green build at one gate never implies the next gate passed. - -## Current state - -Cratis has fixture evidence for native generation, package lifecycle, checksums, -local and hosted canary/rollback simulation, and a generated-only protected Git -repository. The one-time initialization credential was removed after use. - -Still blocked: - -- no real public skill target or product-source contract is approved; -- no PR/release-capable distribution bot is provisioned; -- `@cratis/ai` package ownership and trusted publishing are not configured; -- no real consuming-repository canary has run; -- marketplace review and listing are not complete. - -That boundary is deliberate. Follow the [ecosystem support matrix](/ai/ecosystems/) -for status and [Using AI as a Cratis maintainer](/ai/cratis-maintainers/) for the -current internal workflow. diff --git a/web/src/content/docs/api-reference.md b/web/src/content/docs/api-reference.md deleted file mode 100644 index 99de157c..00000000 --- a/web/src/content/docs/api-reference.md +++ /dev/null @@ -1,36 +0,0 @@ ---- -title: API reference -description: Where to find the generated .NET and TypeScript API reference, and how it's produced. ---- - -The narrative documentation — guides, concepts, tutorials — is what you're reading here. The **API reference** is the exhaustive, generated description of every public type and member, produced directly from the source so it never drifts. - -## .NET / C# - -The .NET API reference is generated with **DocFX** from the XML documentation comments across the Chronicle client SDK, Arc, and Fundamentals assemblies. - -**[Browse the .NET API reference →](/api/)** — every public type and member, organized per library (Chronicle clients, Arc + MongoDB, Fundamentals). - -While you're coding, the same XML docs power **IntelliSense** in your IDE — so the reference is right there as you type. On NuGet: - -- [`Cratis.Chronicle`](https://www.nuget.org/packages/Cratis.Chronicle) — the Chronicle client SDK -- [`Cratis.Arc`](https://www.nuget.org/packages/Cratis.Arc) — the Arc application framework - -## TypeScript - -The TypeScript API reference is generated with **TypeDoc** from the `@cratis/*` packages and surfaced alongside the rest of the site: - -- [`@cratis/arc`](/api/arc/javascript/arc/) — the Arc client core -- [`@cratis/arc` React bindings](/api/arc/javascript/arc.react/) — hooks and components -- [`@cratis/arc` MVVM](/api/arc/javascript/arc.react.mvvm/) — the MVVM layer -- [`@cratis/arc` Vite plugin](/api/arc/javascript/arc.vite/) — build-time proxy generation -- [`@cratis/fundamentals`](/api/fundamentals/javascript/) — shared utilities and concepts - -## How it's produced (for contributors) - -We deliberately **combine tooling**: a modern site for the narrative docs, and the best generator for each kind of API reference. The reference is built in the documentation pipeline from the product source — there's nothing to hand-maintain. See the documentation site's `README.md` for the build details. - -## When to reach for what - -- **Learning or solving a problem?** Stay in the guides, [tutorial](/chronicle/tutorial/), and [scenarios](/chronicle/scenarios/) — they explain the *why* and the *how*. -- **Looking up an exact signature?** The API reference and your IDE's IntelliSense are the fastest path. diff --git a/web/src/content/docs/architecture/CodeAnalysis/index.md b/web/src/content/docs/architecture/CodeAnalysis/index.md deleted file mode 100644 index 01d879ee..00000000 --- a/web/src/content/docs/architecture/CodeAnalysis/index.md +++ /dev/null @@ -1,42 +0,0 @@ ---- -title: Code Analysis ---- - -Cratis Architecture ships Roslyn analyzers that enforce architectural and coding conventions across Cratis codebases. - -## Goals - -- Keep architectural intent enforceable -- Surface violations as early as possible -- Give developers and agents clear, actionable feedback - -## Rules Overview - -| Rule ID | Title | Severity | Description | -|---|---|---|---| -| [CRARCH0001](/architecture/codeanalysis/rules/crarch0001-exception-type-naming/) | Exception type naming | Warning | Exception types must use domain terminology and avoid the generic Exception suffix | -| [CRARCH0002](/architecture/codeanalysis/rules/crarch0002-no-built-in-exception-types/) | No built-in exception types | Warning | Throwing framework exceptions hides domain intent | -| [CRARCH0003](/architecture/codeanalysis/rules/crarch0003-no-postfixes-on-class-names/) | No postfixes on class names | Warning | Class names must describe domain concepts, not technical roles | -| [CRARCH0004](/architecture/codeanalysis/rules/crarch0004-no-features-in-namespace/) | No Features in namespace | Warning | Namespace paths should stay domain-oriented and avoid framework-driven structure names | -| [CRARCH0005](/architecture/codeanalysis/rules/crarch0005-no-regions/) | No regions | Warning | Region directives usually indicate files that need better separation of responsibilities | -| [CRARCH0006](/architecture/codeanalysis/rules/crarch0006-logging-via-loggermessage/) | Logging via LoggerMessage | Warning | Structured, source-generated logging ensures consistency and better performance | -| [CRARCH0007](/architecture/codeanalysis/rules/crarch0007-no-iserviceprovider-injection/) | No IServiceProvider injection | Warning | Service locator patterns hide dependencies and make code harder to reason about | -| [CRARCH0008](/architecture/codeanalysis/rules/crarch0008-use-is-null-checks/) | Use is null checks | Warning | Pattern matching null checks are the preferred and consistent style | -| [CRARCH0009](/architecture/codeanalysis/rules/crarch0009-use-string-interpolation/) | Use string interpolation | Warning | Interpolated strings are clearer and easier to maintain | -| [CRARCH0010](/architecture/codeanalysis/rules/crarch0010-constructor-fan-out/) | Constructor fan-out | Warning | Too many dependencies indicate excessive responsibility in one type | -| [CRARCH0011](/architecture/codeanalysis/rules/crarch0011-file-length-threshold/) | File length threshold | Warning | Very large files are difficult to understand and maintain | -| [CRARCH0012](/architecture/codeanalysis/rules/crarch0012-async-void-forbidden/) | async void forbidden | Error | async void methods hide failures and cannot be awaited in regular flows | -| [CRARCH0013](/architecture/codeanalysis/rules/crarch0013-no-blocking-on-async/) | No blocking on async | Warning | Blocking asynchronous calls can cause deadlocks and reliability issues | -| [CRARCH0014](/architecture/codeanalysis/rules/crarch0014-no-test-types-in-production/) | No test types in production | Error | Production assemblies must remain independent of test-only infrastructure | -| [CRARCH0015](/architecture/codeanalysis/rules/crarch0015-static-class-naming-convention/) | Static class naming convention | Warning | Static utility types follow strict naming conventions to improve discoverability | -| [CRARCH0016](/architecture/codeanalysis/rules/crarch0016-unused-interfaces/) | Unused interfaces | Warning | Speculative interfaces without implementations add unnecessary abstraction | -| [CRARCH0017](/architecture/codeanalysis/rules/crarch0017-namespace-must-align-with-folder-path/) | Namespace must align with folder path | Warning | Namespace and folder alignment improves navigability and consistency | -| [CRARCH0018](/architecture/codeanalysis/rules/crarch0018-avoid-concrete-type-injection/) | Avoid concrete type injection | Warning | Constructor dependencies should favor abstractions for loose coupling | -| [CRARCH0019](/architecture/codeanalysis/rules/crarch0019-avoid-async-postfix-on-method-names/) | Avoid Async postfix on method names | Warning | Method names should avoid unnecessary suffixes unless sync/async pairs exist | -| [CRARCH0020](/architecture/codeanalysis/rules/crarch0020-handle-asynchronous-calls/) | Handle asynchronous calls | Warning | Fire-and-forget calls can hide failures and produce nondeterministic behavior | -| [CRARCH0021](/architecture/codeanalysis/rules/crarch0021-serializable-attribute-not-allowed/) | Serializable attribute not allowed | Warning | Legacy serialization attributes are not part of modern Cratis architecture guidance | -| [CRARCH0022](/architecture/codeanalysis/rules/crarch0022-private-modifier-not-allowed/) | Private modifier not allowed | Warning | Private is implicit in C#, so explicit modifiers add noise | -| [CRARCH0023](/architecture/codeanalysis/rules/crarch0023-use-typed-logger-category/) | Use typed logger category | Warning | Typed logger categories align log events with the producing type | -| [CRARCH0024](/architecture/codeanalysis/rules/crarch0024-loggermessage-container-conventions/) | LoggerMessage container conventions | Warning | LoggerMessage methods must live in convention-based containers for consistency | -| [CRARCH0025](/architecture/codeanalysis/rules/crarch0025-use-cratis-fundamentals-traces/) | Use Cratis Fundamentals traces | Warning | Tracing should flow through Cratis Fundamentals abstractions for consistency | -| [CRARCH0026](/architecture/codeanalysis/rules/crarch0026-use-cratis-fundamentals-metrics/) | Use Cratis Fundamentals metrics | Warning | Metrics should use Cratis Fundamentals abstractions instead of raw instrument creation | diff --git a/web/src/content/docs/architecture/index.md b/web/src/content/docs/architecture/index.md deleted file mode 100644 index 43a8fdb7..00000000 --- a/web/src/content/docs/architecture/index.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -title: Cratis Architecture Documentation ---- - -This documentation covers the Cratis Architecture analyzer package and its rule set. - -- Start with the [code analysis overview](/architecture/codeanalysis/) -- Browse the complete [rule reference](/architecture/codeanalysis/rules/) diff --git a/web/src/content/docs/auth-and-compliance.mdx b/web/src/content/docs/auth-and-compliance.mdx deleted file mode 100644 index 7c0fc67e..00000000 --- a/web/src/content/docs/auth-and-compliance.mdx +++ /dev/null @@ -1,138 +0,0 @@ ---- -title: Auth and compliance end to end -description: How authentication, tenant resolution, Arc authorization, Chronicle compliance subjects, PII encryption, read models, and erasure fit together across the Cratis stack. ---- - -import { Aside, CardGrid, LinkCard } from '@astrojs/starlight/components'; - -Security in a Cratis application is not one feature in one package. A request crosses the edge, becomes -an Arc command or query, writes events through Chronicle, builds read models, and comes back to a React -screen. Authentication, authorization, tenant isolation, and compliance each belong to a different part -of that journey, and the stack is designed so each concern has one clear owner. - -The short version: **AuthProxy authenticates and resolves tenant context, Arc authorizes the operation, -Chronicle stores events in the right namespace and encrypts PII by compliance subject, and Arc releases -PII before query results reach the client.** - -```mermaid -flowchart LR - Browser["Browser or API client"] --> Proxy["AuthProxy<br/>authenticate + resolve tenant"] - Proxy --> Me["Arc /.cratis/me<br/>identity enrichment"] - Me --> Proxy - Proxy -->|Tenant-ID + identity| Arc["Arc commands and queries<br/>authorize operation"] - Arc -->|events + subject| Chronicle["Chronicle<br/>tenant namespace"] - Chronicle --> EventLog["Event log<br/>PII encrypted by subject"] - EventLog --> ReadModels["Read models<br/>encrypted at rest"] - ReadModels --> Release["Arc read-model release<br/>decrypts for response"] - Release --> UI["Components / React<br/>typed result"] -``` - -## The edge owns authentication - -Put [AuthProxy](/authproxy/) in front of your frontend and backend services when you want one place to -own the browser and API edge. It handles OpenID Connect, OAuth providers, JWT bearer authentication, -provider selection, invite onboarding, tenant resolution, and routing. - -AuthProxy then forwards requests with the resolved tenant and identity context. In an Arc application, -the identity-enrichment endpoint is `/.cratis/me`: AuthProxy calls it, Arc composes the domain-specific -identity payload, and the frontend can consume the resulting identity state instead of making every -service rediscover the user. - -<Aside type="note" title="Authentication is not authorization"> -Authentication answers "who is calling?" Tenant resolution answers "which tenant are they acting in?" -Authorization answers "can they run this operation?" Compliance answers "whose personal data is this, -and how is it protected?" -</Aside> - -## Arc owns operation-level access - -Arc applies authorization where the behavior is declared: commands and queries. Use ASP.NET Core -authorization attributes, Arc's `[Roles]` convenience attribute, and policy-based authorization on the -command record or read-model query method that represents the operation. - -That matters because authorization happens before the behavior runs. If a command is not authorized, its -`Handle()` method is not executed. If a query is not authorized, the read is not performed. The generated -frontend proxy receives authorization state as part of the command or query result, so screens can react -without hand-written protocol glue. - -Use edge authentication for entry, then command/query authorization for business operations. Do not rely -on the UI hiding a button as the only access-control layer. - -## Tenant context follows the request into Chronicle - -In a multi-tenant Cratis application, the tenant resolved at the edge becomes application context in Arc. -When Arc is integrated with Chronicle, that tenant context is used as the Chronicle namespace. Events, -projections, reducers, and read models for one tenant stay separated from another tenant without every -command manually choosing a namespace. - -This is the operational boundary: - -| Concern | Owner | Result | -| --- | --- | --- | -| Resolve the tenant | AuthProxy or Arc tenancy resolvers | One tenant id for the request | -| Carry tenant through command/query handling | Arc | Commands, queries, filters, identity, and generated endpoints see the same tenant context | -| Isolate event-sourced state | Arc + Chronicle integration | Chronicle uses the tenant as the event-store namespace | - -## Compliance subject decides the PII key - -Chronicle compliance uses a **subject** to identify whose protected data an event contains. That subject -is the lookup key for PII encryption material. It is often a person or customer id; it is not always the -authenticated user, and it is not always the aggregate id. - -For example, an order event may be appended to an `OrderId` event source while the protected email -address in the event belongs to a `CustomerId`. In that case the command should provide the customer as -the Chronicle `Subject`. If no explicit subject is provided, Chronicle falls back to the event source id. - -Arc's Chronicle integration can resolve the subject from command return values, command properties, -`ICanProvideSubject`, or a `[Subject]`-marked property. The rule of thumb is simple: **set the subject to -the identity that owns the PII, not merely the thing being changed.** - -## PII is protected at write and released at read - -Chronicle detects `[PII]` on event properties and on `ConceptAs<T>` value types. At append time, those -values are encrypted under the subject's key before they are stored in the event log. - -Read models keep that protection: - -| Read-model style | How PII is tracked | -| --- | --- | -| Projection | Chronicle can infer PII lineage from mapped event properties. | -| Reducer | The read model must mark PII properties explicitly, because reducer logic is arbitrary C#. | -| Arc query response | Arc releases the read model before serving it, so clients receive decrypted values when the subject key exists. | - -Managed read-model documents also carry Chronicle's reserved `_subject` field so the correct key can be -used during release. Do not model your own `_subject` property. - -## Erasure is key deletion plus rebuild - -The event log can stay structurally immutable while protected values become unreadable. For GDPR-style -erasure, Chronicle deletes the subject's PII encryption key. After that, PII encrypted with that key -cannot be decrypted. - -For read models, finish the erasure by re-projecting or re-reducing affected models. Until then, stored -read-model documents may still contain ciphertext. The key is gone, so the value is unreadable, but -rebuilding the read model removes the remaining encrypted payload from the query store. If an entire -event payload must be removed, combine key deletion with [event redaction](/chronicle/events/redaction/). - -## What to decide up front - -| Decision | Ask this | Start here | -| --- | --- | --- | -| Identity provider | Where do browser users and API clients authenticate? | [AuthProxy authentication](/authproxy/configuration/authentication/) | -| Tenant resolution | Which host, claim, route, or configured value selects the tenant? | [AuthProxy tenancy](/authproxy/configuration/tenancy/) | -| Operation access | Which roles or policies can run each command or query? | [Arc authorization](/arc/backend/core/authorization/) | -| Compliance subject | Whose personal data does this event contain? | [Arc Chronicle subject](/arc/backend/chronicle/compliance/subject/) | -| PII modeling | Which event properties or concepts are personal data? | [Chronicle compliance](/chronicle/compliance/) | -| Erasure | What must be key-deleted, re-projected, or redacted? | [Chronicle read models and PII](/chronicle/compliance/read-models/) | - -## Go deeper - -<CardGrid> - <LinkCard title="AuthProxy" description="Authenticate users, resolve tenants, enrich identity, and route requests at the edge." href="/authproxy/" /> - <LinkCard title="Arc identity" description="Expose /.cratis/me and compose the identity details AuthProxy and the frontend consume." href="/arc/backend/identity/" /> - <LinkCard title="Arc authorization" description="Authorize commands and queries with attributes, roles, and policies before behavior runs." href="/arc/backend/core/authorization/" /> - <LinkCard title="Chronicle subject" description="Set the compliance subject so PII is encrypted under the correct identity." href="/arc/backend/chronicle/compliance/subject/" /> - <LinkCard title="Chronicle compliance" description="Understand PII annotations, per-subject encryption keys, read models, and erasure." href="/chronicle/compliance/" /> - <LinkCard title="Production readiness" description="Review the operational checklist for TLS, secrets, storage, observability, and deployment." href="/production-readiness/" /> -</CardGrid> - diff --git a/web/src/content/docs/build-a-full-app.mdx b/web/src/content/docs/build-a-full-app.mdx deleted file mode 100644 index 910677e1..00000000 --- a/web/src/content/docs/build-a-full-app.mdx +++ /dev/null @@ -1,148 +0,0 @@ ---- -title: Build a full-stack feature -description: Put Chronicle, Arc, and Components together — go from a domain event to a working React screen in one vertical slice, with full-stack type safety. ---- - -import { Steps } from '@astrojs/starlight/components'; - -This is where the three products meet. You'll build one **vertical slice** of a library app — registering an author and listing authors — end to end: a command and event on the backend ([Arc](/arc/) + [Chronicle](/chronicle/)), a read model built by a projection, and a React screen ([Components](/components/)) that consumes the **generated, type-safe proxies**. No hand-written API client, no DTO duplication. - -Everything for the feature lives in **one folder** — that's the vertical-slice idea: you navigate by feature, not by technical layer. - -Here's the slice as an **[event model](/event-modeling/)** — read left to right, the user acts on a screen, a command records a fact, that fact is projected into a read model, and the next screen reads it: - -```mermaid -eventmodeling - -tf 01 ui Authors.AddAuthor -tf 02 cmd Authors.RegisterAuthor { id: uuid, name: string } -tf 03 evt Authors.AuthorRegistered { name: string } -tf 04 rmo Authors.Author ->> 03 -tf 05 ui Authors.Authors ->> 04 -``` - -The blocks are the slice, one per product: the `ui` screens are [Components](/components/), the `cmd` and the `rmo`'s query are [Arc](/arc/), the `evt` is [Chronicle](/chronicle/), and the build generates the typed proxies that connect them. Frames 01–03 are the **command pattern** (intent → fact); 03–05 are the **view pattern** (fact → read model → screen). - -## Before you start - -Scaffold a full-stack app and have it running — `dotnet new cratis -o Library` from the [Chronicle getting started](/chronicle/get-started/). You'll add the slice below into a `Features/Authors/` folder. - -## The host - -`dotnet new cratis` generates the host for you — the whole stack in two calls. `AddCratis` brings up Arc and Chronicle together; `UseCratis` activates both: - -```csharp -var builder = WebApplication.CreateBuilder(args); - -builder.AddCratis( - configureArcBuilder: arc => arc.WithMongoDB(), - configureChronicleOptions: chronicle => chronicle.EventStore = "Library", - configureChronicleBuilder: chronicle => chronicle.WithCamelCaseNamingPolicy()); - -var app = builder.Build(); - -app.UseCratis(); -app.Run(); -``` - -You won't touch this again for the slice below — it's here so you can see where the command, event, read model, and query you're about to write get wired in. The [Cratis package](/arc/backend/chronicle/cratis-package/) reference covers what `AddCratis` configures and how to adjust it. - -## 1. Model the slice (backend) - -<Steps> - -1. **A strongly-typed id.** Never pass a raw `Guid` around — wrap it: - - ```csharp - public record AuthorId(Guid Value) : ConceptAs<Guid>(Value) - { - public static AuthorId New() => new(Guid.NewGuid()); - public static implicit operator EventSourceId(AuthorId id) => new(id.Value.ToString()); - } - ``` - -2. **The command and the event, together in one file.** The command is a record with `Handle()` on it — no separate handler class. `Handle()` returns the event that happened: - - ```csharp - [Command] - public record RegisterAuthor(AuthorId Id, string Name) - { - public AuthorRegistered Handle() => new(Name); - } - - [EventType] - public record AuthorRegistered(string Name); - ``` - -3. **The read model and its projection.** Declare the shape you want to query and which event feeds it — AutoMap matches `AuthorRegistered.Name` to `Name`. A static method exposes the query as an observable so the UI updates live: - - ```csharp - [ReadModel] - [FromEvent<AuthorRegistered>] - public record Author([property: Key] AuthorId Id, string Name) - { - public static ISubject<IEnumerable<Author>> AllAuthors(IMongoCollection<Author> collection) => - collection.Observe(); - } - ``` - -</Steps> - -That's the whole backend for the feature — one file, three records. Run `dotnet build`. - -:::note[What the build did] -Building the backend **generates TypeScript proxies** for `RegisterAuthor` and `AllAuthors`. The frontend now has typed clients that match your C# exactly — change a property here and the frontend won't compile until you fix it. -::: - -## 2. Build the screen (frontend) - -<Steps> - -1. **A dialog that executes the command.** `CommandDialog` instantiates and runs the generated command and renders the OK/Cancel footer for you — you just supply the fields: - - ```tsx - import { CommandDialog } from '@cratis/components/CommandDialog'; - import { InputTextField } from '@cratis/components/CommandForm'; - import { DialogProps } from '@cratis/arc.react/dialogs'; - import { RegisterAuthor } from './RegisterAuthor'; - import { Guid } from '@cratis/fundamentals'; - - export const AddAuthor = ({ closeDialog }: DialogProps) => ( - <CommandDialog<RegisterAuthor> - command={RegisterAuthor} - title="Add author" - okLabel="Add" - onBeforeExecute={(values) => { values.id = Guid.create(); return values; }}> - <InputTextField<RegisterAuthor> value={i => i.name} title="Name" /> - </CommandDialog> - ); - ``` - -2. **A table that reads the observable query.** The generated `AllAuthors` proxy is observable — `.use()` re-renders when the projection changes: - - ```tsx - import { AllAuthors } from './Author'; - - export const Authors = () => { - const [authors] = AllAuthors.use(); - return ( - <ul> - {authors.data.map(a => <li key={String(a.id)}>{a.name}</li>)} - </ul> - ); - }; - ``` - -</Steps> - -## What you built - -- One **vertical slice**: command, event, read model, projection, and UI — all for a single behavior, in one place. -- **Full-stack type safety**: the React code calls proxies generated from your C# types. There is no second source of truth to drift. -- The **CQRS loop**: the command appends a fact, a projection builds the read model, and the observable query streams it to the UI live. - -## Go deeper - -- Backend: [Commands](/arc/backend/commands/) and [Queries](/arc/backend/queries/) in Arc, [Projections](/chronicle/projections/) in Chronicle. -- Frontend: [Dialogs](/components/) and data tables in Components. -- The reasoning: [Why developers choose Cratis](/why-cratis/) and [Why Arc](/arc/why-arc/). diff --git a/web/src/content/docs/code-analysis.mdx b/web/src/content/docs/code-analysis.mdx deleted file mode 100644 index 008bce29..00000000 --- a/web/src/content/docs/code-analysis.mdx +++ /dev/null @@ -1,86 +0,0 @@ ---- -title: Code analysis -description: Cratis ships Roslyn analyzers and ESLint rules that turn framework conventions into build gates — a wrong projection shape, a missing [EventType], a Handle() in the wrong place, generated proxy noise, or a heavy Components import fails before it ships. ---- - -import { CardGrid, Aside } from '@astrojs/starlight/components'; -import SimpleCard from '@components/SimpleCard.astro'; -import TopicHero from '@components/TopicHero.astro'; - -<TopicHero icon="approve-check" eyebrow="The Cratis Stack" title="The build enforces the conventions"> -Cratis is convention-heavy by design — model-bound commands, `[EventType]` events, declarative projections, generated proxies, vertical slices, and React views that stay thin. Conventions make the easy path the right one, but only if something *catches* you when you stray. Cratis does that with **Roslyn analyzers** in `dotnet build` and **ESLint rules** in the JavaScript build, so the feedback lands where a developer or AI agent can act on it. -</TopicHero> - -## Why build gates, not docs alone - -A convention you can break silently isn't a convention — it's a suggestion. If you append a type that isn't marked `[EventType]`, or write a reactor method with the wrong signature, the framework can't discover it, and the failure shows up at *runtime* as "nothing happened." The analyzers move that failure left: the build fails, the message tells you exactly which rule and how to fix it, and several rules ship a one-click code fix. - -The same pattern matters on the frontend. Generated proxy files should not produce lint noise, view models should not call React hooks, and Components consumers should not import the root barrel or bypass the Cratis dialog wrappers. Those are architectural choices, so Cratis packages them as ESLint rules instead of leaving them as tribal knowledge. - -This matters doubly in the [AI-native](/ai/) workflow. When an agent scaffolds a slice, these gates are the same guardrail for the agent that they are for you — a wrong shape fails, the agent reads the diagnostic, and it corrects itself. The [coding rules in the AI configuration](/plugins/) teach the conventions; the build gates enforce them. - -## Roslyn analyzer packages - -Each analyzer ships *inside* the package it validates — there's nothing extra to install. Reference the package and the rules are active on your next build. - -<CardGrid> - <SimpleCard title="Chronicle — CHR####" icon="seti:db" link="/chronicle/code-analysis/"> - 21 rules for event-sourcing constructs. Ships with the <code>Cratis.Chronicle</code> package. - </SimpleCard> - <SimpleCard title="Arc core — ARC####" icon="puzzle" link="/arc/backend/code-analysis/"> - Rules for commands and read models. Ships with <code>Arc.Core</code>. - </SimpleCard> - <SimpleCard title="Arc + Chronicle — ARCCHR####" icon="seti:db" link="/arc/backend/chronicle/code-analysis/"> - Aggregate-root event-handler rules for Arc-on-Chronicle. Ships with the Chronicle integration. - </SimpleCard> - <SimpleCard title="Architecture — CRARCH####" icon="seti:config" link="/architecture/codeanalysis/"> - Rules for architectural boundaries and dependency flow. Ships with the <code>Cratis.Architecture</code> package. - </SimpleCard> -</CardGrid> - -## ESLint packages - -TypeScript and React projects get the same kind of feedback through shared flat-config presets and product-specific ESLint plugins. These are the packages in the current source tree: - -| Package | Source | Build gate | -|---|---|---| -| `@cratis/eslint-config` | Fundamentals | Shared `consumer` and `internal` presets: TypeScript/React hygiene, BDD spec relaxations for `for_*` files, and the Cratis license-header rule for product repos. | -| `@cratis/eslint-plugin-arc` | Arc | Skips generated Arc proxies and forbids React hook calls inside MVVM view-model classes. | -| `@cratis/eslint-plugin-components` | Components | Forbids root-barrel imports from `@cratis/components` and raw `primereact/dialog` imports when the Cratis wrappers should be used. | - -Compose them in `eslint.config.mjs`: - -```javascript -import cratis from '@cratis/eslint-config'; -import arc from '@cratis/eslint-plugin-arc'; -import components from '@cratis/eslint-plugin-components'; - -export default [ - ...cratis.configs.consumer, - ...arc.configs.recommended, - ...components.configs.recommended, -]; -``` - -Inside the Cratis product repos, CI runs these as lint gates. The common workspace script is `yarn g:lint:ci`, while package apps like the Chronicle Workbench use `yarn lint:ci`. - -<Aside type="tip" title="Treat warnings as errors"> -These rules are most valuable when the build can't go green around them. Cratis projects build with warnings-as-errors and quiet lint output — adopt the same in your project so a convention slip stops the build instead of scrolling past in the output. -</Aside> - -## The AI layer and the enforcement layer - -The architecture and coding instructions live in the [Cratis AI configuration](/plugins/): C#, TypeScript, vertical slices, React components, BDD specs, Chronicle reactors, Orleans, review agents, and task-specific skills. That layer tells an assistant what good Cratis code looks like. - -Code analysis is the enforcement layer. Roslyn analyzers and ESLint rules run after the assistant writes code, catch the parts that drift, and produce concrete diagnostics that can be fixed in the same loop. For AI-assisted development, the combination matters more than either half alone: instructions reduce bad output, build gates make the remaining mistakes visible. - -## Where to go next - -<CardGrid> - <SimpleCard title="Plugins" icon="puzzle" link="/plugins/"> - The agents, skills, and coding rules that teach the same conventions these gates enforce. - </SimpleCard> - <SimpleCard title="Vertical slices" icon="seti:folder" link="/arc/vertical-slices/"> - The convention the analyzers are guarding — everything for a feature in one folder. - </SimpleCard> -</CardGrid> diff --git a/web/src/content/docs/community.mdx b/web/src/content/docs/community.mdx deleted file mode 100644 index 512d1aad..00000000 --- a/web/src/content/docs/community.mdx +++ /dev/null @@ -1,72 +0,0 @@ ---- -title: Community and help -description: Join the Cratis Discord community, ask questions, report issues, and find the right source for help. ---- - -import { CardGrid, LinkCard, Aside } from '@astrojs/starlight/components'; - -You do not need to be a customer, a contributor, or an event-sourcing expert to ask for help. Cratis is an open community, and we are glad to help people who are evaluating the stack, building their first slice, debugging a running store, or trying to understand the model. - -The fastest place to start a conversation is the [Cratis Discord community](https://discord.gg/kt4AMpV8WV). Bring the question you have, even if it is not polished yet. There, [Prompter](/prompter/) — the community's documentation assistant — takes the first swing automatically, answering from these docs with citations, or handing you to a human when the docs do not cover it. - -<CardGrid> - <LinkCard title="Join the Cratis Discord" description="Ask questions, talk through designs, get help choosing a path, and meet the people building and using Cratis." href="https://discord.gg/kt4AMpV8WV" /> - <LinkCard title="Ask Prompter" description="The community's docs assistant on Discord — @mention it, use /ask, or open a help-forum thread and it answers grounded in these docs, with citations, in seconds." href="/prompter/" /> - <LinkCard title="Share feedback" description="Tell us what is confusing, missing, rough, or worth improving in the products and docs." href="/feedback/" /> - <LinkCard title="Work with us" description="Event modeling, adoption, architecture review, implementation help, and training — direct from the people who build Cratis." href="/work-with-us/" /> - <LinkCard title="Report security privately" description="Use the security page for vulnerability reports and responsible disclosure." href="/security/" /> - <LinkCard title="Browse the Cratis source" description="Find Chronicle, Arc, Components, CLI, Fundamentals, samples, and the documentation repositories on GitHub." href="https://github.com/cratis" /> - <LinkCard title="Run the samples" description="Choose a focused example or explore the larger Library application." href="/samples/" /> -</CardGrid> - -## Where to ask - -| When you need... | Start with... | -|---|---| -| A grounded, cited answer to a docs question in seconds | [Prompter on Discord](/prompter/) | -| Help deciding whether Cratis fits your system | [Cratis Discord](https://discord.gg/kt4AMpV8WV) | -| A second pair of eyes on an event model, projection, command, or slice | [Cratis Discord](https://discord.gg/kt4AMpV8WV) | -| Help with a tutorial, sample, local setup, or first application | [Cratis Discord](https://discord.gg/kt4AMpV8WV) | -| A reproducible bug in a product | The matching GitHub issue tracker | -| A feature request or design proposal | GitHub issues, with Discord for early discussion | -| Suggestions, rough edges, or documentation gaps | [Feedback and suggestions](/feedback/) | -| Private, dedicated, or commercial help | [Work with us](/work-with-us/) | -| To understand how dedicated help differs from community help | [Professional help](/professional-help/) | -| A suspected vulnerability | [Security](/security/) | -| Contribution workflow, coding standards, or build setup | [Contributing](/contributing/) | - -If you are not sure which product owns the problem, ask in Discord first. We can help route it to Chronicle, Arc, Components, CLI, Fundamentals, or the documentation. - -## GitHub issue trackers - -Use issues when the work needs a durable record: reproducible bugs, feature requests, regression reports, or documentation fixes. For open-ended questions, Discord is usually better. - -| Area | Issue tracker | -|---|---| -| Chronicle event store, clients, Workbench, and hosting | [Chronicle issues](https://github.com/Cratis/Chronicle/issues) | -| Arc commands, queries, proxy generation, validation, and backend/frontend integration | [Arc issues](https://github.com/Cratis/Arc/issues) | -| React components, forms, dialogs, tables, styling, and Storybook | [Components issues](https://github.com/Cratis/Components/issues) | -| CLI commands, diagnostics, and AI command catalogs | [CLI issues](https://github.com/Cratis/cli/issues) | -| Fundamentals libraries and shared .NET/TypeScript helpers | [Fundamentals issues](https://github.com/Cratis/Fundamentals/issues) | -| Documentation site, guides, examples, and broken links | [Documentation issues](https://github.com/Cratis/Documentation/issues) | - -## How to get a useful answer - -You can ask early. When you have the details, include the parts that make the problem reproducible: - -- What you are trying to build or decide. -- Which product you are using: Chronicle, Arc, Components, CLI, Fundamentals, or the full stack. -- The package or Docker image versions, if you know them. -- The command, event, projection, query, component, or configuration involved. -- The expected behavior and what happened instead. -- Logs, error messages, screenshots, or a small repository when the problem depends on runtime behavior. - -<Aside type="tip" title="Design questions are welcome"> -If the question is more about modeling than a specific exception, describe the business decision you need to make and the facts you already have. That is often enough for a useful event-sourcing conversation. -</Aside> - -## Community expectations - -We want Cratis to be useful to people who are learning, evaluating, operating, and contributing. Be clear about what you need, assume good intent, and keep discussions respectful. The [Code of Conduct](/contributing/code_of_conduct/) applies to the community spaces and the repositories. - -Start with the [Cratis Discord community](https://discord.gg/kt4AMpV8WV), then move durable bugs, proposals, and documentation changes into the relevant GitHub repository when the thread becomes actionable. diff --git a/web/src/content/docs/compatibility.mdx b/web/src/content/docs/compatibility.mdx deleted file mode 100644 index 2ba53f67..00000000 --- a/web/src/content/docs/compatibility.mdx +++ /dev/null @@ -1,51 +0,0 @@ ---- -title: Version compatibility -description: Runtime, package, framework, storage, and tooling compatibility for the current Cratis documentation baseline. ---- - -import { CardGrid, LinkCard, Aside } from '@astrojs/starlight/components'; - -Cratis spans .NET packages, TypeScript packages, Docker images, storage engines, React components, Storybook, and documentation tooling. This page gives the current compatibility baseline used by the documentation site. For exact released package versions, use the package manager and repository release pages linked from [What's new](/whats-new/). - -<CardGrid> - <LinkCard title="Review releases" description="Use curated release digests and repository releases for exact published versions." href="/whats-new/" /> - <LinkCard title="Check production readiness" description="Use the operational checklist before deploying Chronicle or the full stack." href="/production-readiness/" /> - <LinkCard title="Ask about a version" description="Use Discord when a specific runtime or package version matters to your project." href="/community/" /> -</CardGrid> - -## Current baseline - -| Area | Baseline | -|---|---| -| .NET local development | .NET 10 is the current development target in Chronicle, Arc, Fundamentals, and CLI source. | -| .NET package targeting | Arc and Fundamentals package across `net8.0`, `net9.0`, and `net10.0` in release configuration. Chronicle packaging also carries `net8.0`, `net9.0`, and `net10.0` where configured. | -| CLI | The `cratis` CLI targets `net10.0` in the current source tree. | -| Node | Arc, Components, and Fundamentals JavaScript workspaces declare `node >=23.0.0`. | -| Package manager | The JavaScript workspaces use Yarn 4.5.3. | -| React | Arc React declares React 18 or 19 peer compatibility. Components is currently built and documented around the React 19 workspace. | -| TypeScript | Current JavaScript workspaces use TypeScript 6. | -| Chronicle storage | MongoDB, PostgreSQL, Microsoft SQL Server, and SQLite are documented storage choices. | -| Arc persistence | MongoDB and Entity Framework Core integrations are documented. | -| Docs site | Astro Starlight, generated product content, Storybook builds, API reference builds, and `/llms.txt` exports are part of the documentation site. | - -## Product relationships - -| Product | Common dependency relationship | -|---|---| -| Chronicle | Uses Fundamentals and Arc packages internally, exposes .NET clients and gRPC/protobuf contracts, and can be inspected through CLI and Workbench. | -| Arc | Uses Fundamentals and can integrate with Chronicle, MongoDB, Entity Framework Core, ASP.NET Core, OpenAPI, Swagger, and generated TypeScript proxies. | -| Components | Depends on Arc-generated frontend contracts and React; wraps PrimeReact-oriented UI patterns. | -| CLI | Talks to Chronicle and produces AI command catalogs through `cratis init`. | -| Documentation | Aggregates product `Documentation/` folders and builds Storybook/API reference artifacts into the site. | - -## How to verify your stack - -1. Check the package versions you plan to install. -2. Check the matching release notes in [What's new](/whats-new/). -3. Check the product guide for runtime-specific setup. -4. Pin container images and package versions in production. -5. Ask in [Community and help](/community/) if your runtime combination is not listed. - -<Aside type="caution" title="Do not rely on latest tags in production"> -Use explicit package versions and Docker image tags for deployed systems. The Chronicle hosting docs also recommend specific version tags for production deployments. -</Aside> diff --git a/web/src/content/docs/cratis-stack.mdx b/web/src/content/docs/cratis-stack.mdx deleted file mode 100644 index 88d55093..00000000 --- a/web/src/content/docs/cratis-stack.mdx +++ /dev/null @@ -1,171 +0,0 @@ ---- -title: The Cratis Stack -description: One stack, end to end — model your domain in Studio, secure the edge with AuthProxy, build it with Arc, Chronicle, and Components, choose the right Chronicle storage, run it all from one Aspire AppHost, operate it with the CLI and Workbench, and accelerate every step with AI. ---- - -import { CardGrid } from '@astrojs/starlight/components'; -import SimpleCard from '@components/SimpleCard.astro'; -import TopicHero from '@components/TopicHero.astro'; -import StackJourney from '@components/StackJourney.astro'; - -<TopicHero icon="rocket" eyebrow="The Cratis Stack" title="From a sticky note to a running, typed, full-stack app"> -Most stacks are a bag of libraries you bolt together and then spend the whole project keeping in sync. Developers choose Cratis because the layers are designed as **one thing** — the domain you sketch at the top of the hour is typed C#, authenticated tenant-aware requests, a live React screen, and an inspectable event log before the hour is out. Not because you typed faster — because event sourcing is the default backbone for information systems and every handoff you'd normally hand-write is covered by conventions, generation, and open runtime boundaries. [Start building →](/chronicle/get-started/) · [Why developers choose Cratis →](/why-cratis/) -</TopicHero> - -## One idea, all the way down - -Each product is strong on its own — you can [use them separately](/why-cratis/). The reason to reach for the *whole* stack is that the seams disappear: CQRS shapes information going in and out, event sourcing keeps the facts underneath, and everywhere you'd normally hand-write glue, Cratis generates it. - -- You **model** the domain — visually on the [Studio](/studio/) canvas, or as a declarative `.play` script in [Screenplay](/screenplay/) — and that one model is how you manage the entire lifecycle of the project, from generated C# to the running system. -- Or you **start from a system that already exists**, and [Prologue](/prologue/) captures what it actually does — HTTP commands, database changes, telemetry — and interprets that into the same kind of model, evidenced rather than guessed. -- You **secure the edge**, and [AuthProxy](/authproxy/) authenticates, resolves the tenant, enriches identity, and routes requests. -- You **write a command**, and [Arc](/arc/) generates the [typed TypeScript proxy](/arc/understanding-the-proxy-boundary/) for it. -- You **append an event**, and [Chronicle](/chronicle/) accepts it through a gRPC/protobuf boundary, stores it in MongoDB, PostgreSQL, Microsoft SQL Server, or SQLite, and projects the read model from it. -- You **render a screen**, and [Components](/components/) consumes the proxy with no API client to write. -- You **press F5**, and the [Aspire integration](/chronicle/hosting/aspire/) starts Chronicle next to your services — development image, embedded MongoDB, no compose file. -- You **run it**, and the [CLI](/cli/), Workbench, OpenTelemetry, jobs, recommendations, and replay tools let you watch and operate the event store. - -Nothing in that list is a thing *you* keep in sync. The build does. - -Wiring the stack into one host is itself a single call: the [`Cratis` package](/arc/backend/chronicle/cratis-package/) brings Arc and Chronicle up together with `AddCratis`/`UseCratis`, sharing storage, identity, and hosting. - -How the two line up *in code* — how a command's `Handle()` return becomes an appended event, how the event source id picks the stream, and how a `[ReadModel]` projection is served back by a query — is laid out in [Integrate with Chronicle](/arc/backend/chronicle/). - -<StackJourney - eyebrow="Design, build, operate" - title="The journey, end to end" - intro="Read it left to right: a domain idea becomes a secured tenant-aware request, a generated slice, a durable event, a live typed React screen, and an inspectable running system. AI can accelerate the same path because the conventions are explicit." -/> - -## Run the whole stack from one AppHost - -Before any of this renders, Chronicle and a database have to be running — and "clone the repo, then fix the -compose file" is where most first hours go to die. Chronicle ships a first-class .NET Aspire resource instead, -so the event store is one line in your AppHost: - -```csharp -var builder = DistributedApplication.CreateBuilder(args); - -var chronicle = builder.AddCratisChronicle(); - -builder.AddProject<Projects.MyApi>("api") - .WithReference(chronicle); - -builder.Build().Run(); -``` - -With no arguments, `AddCratisChronicle()` registers a resource named `chronicle` running the development -image — embedded MongoDB included — so F5 brings the store up with zero external infrastructure, and -`WithReference(chronicle)` hands your API the connection string. When you graduate to real storage, the same -call takes a configure callback after the name — `builder.AddCratisChronicle("chronicle", c => c.WithMongoDB(mongo))`, -or `WithPostgreSql`, `WithMsSql`, `WithSqlite` — and compliance encryption keys can live in HashiCorp Vault or -Azure Key Vault via `WithHashiCorpVault` and `WithAzureKeyVault`. - -[Chronicle's Aspire integration](/chronicle/hosting/aspire/) walks through development versus production -images, every storage option, ports, and connecting .NET clients. - -## Security and compliance end to end - -Authentication, authorization, tenancy, and compliance are separate concerns, but they travel through the -same request. AuthProxy authenticates and resolves tenant context. Arc authorizes the command or query. -The Arc + Chronicle integration writes events into the tenant's Chronicle namespace. Chronicle encrypts -PII by compliance subject, and Arc releases protected read models before serving query responses. - -[Auth and compliance end to end](/auth-and-compliance/) shows that flow across the stack: where identity -is enriched, where access decisions happen, how subjects differ from event source ids, and what erasure -requires after protected data has reached read models. - -## Test the same flow - -The same column you draw in an event model is also the shape of the test: - -```mermaid -eventmodeling - -tf 01 ui Authors.RegisterAuthorScreen -tf 02 cmd Authors.RegisterAuthor { authorId: uuid, name: string } -tf 03 evt Authors.AuthorRegistered { name: string } -tf 04 rmo Authors.Author ->> 03 -tf 05 ui Authors.AuthorsScreen ->> 04 -``` - -That becomes a BDD-style specification: - -| Given | When | Then | -|---|---|---| -| no author exists for this id | `RegisterAuthor` runs through Arc | the command succeeds | -| no author exists for this id | `RegisterAuthor` runs through Arc | Chronicle records `AuthorRegistered` | -| `AuthorRegistered` exists | the read model projection handles it | the `Authors` screen can read the author | - -Cratis Specifications gives that shape a light wrapper over xUnit: `Establish()` is the given, `Because()` is the when, and each `[Fact]` is one then. Arc adds `CommandScenario<TCommand>` so the command runs through the real pipeline. Chronicle adds in-process scenarios for events, projections, and reactors. Together, you test the slice in the same language you designed it: facts in, command happens, facts and read models out. - -[Testing with Cratis](/testing-with-cratis/) walks through the packages and the full stack-slice example. - -## Build it with AI - -Cratis ships first-class tooling so an AI agent can build *and* operate the stack alongside you. That works because the stack is intentionally conventional: the same feature shape, naming, generated boundary, Chronicle diagnostics, and CLI command catalog show up across products. - -- **`cratis init`** drops the Cratis AI configuration into your repo — agents and skills (like *new vertical slice* and *add projection*) that already know the Cratis conventions, so an assistant scaffolds correct slices instead of guessing at the framework. -- The **Chronicle MCP server** lets an agent connect to a running store and inspect events, observers, and read models — the same window the CLI gives you, handed to your assistant. - -Model in Studio, generate the slices, build them, inspect the result — an agent can take part at every step. [AI-native development](/ai/) covers the setup and exactly what an agent can do. - -And when you or a teammate gets stuck, [Prompter](/prompter/) — the community's Discord assistant — answers Cratis questions grounded in this same documentation, with citations, right where your team already talks. - -## Each layer, on its own and together - -<CardGrid> - <SimpleCard title="Studio — design" icon="open-book" link="/studio/"> - Model the domain on a collaborative event-modeling canvas and generate type-safe C# from it. *(Coming soon.)* - </SimpleCard> - <SimpleCard title="Screenplay — model as code" icon="pencil" link="/screenplay/"> - The modeling language: a whole bounded context — events, commands, queries, projections, screens, and rules — in one declarative `.play` file that Stage runs live and Studio visualizes. - </SimpleCard> - <SimpleCard title="Prologue — capture" icon="magnifier" link="/prologue/"> - Point it at an existing system — no Cratis constructs required — and it captures HTTP commands, database changes, and telemetry, then interprets them into an event model and a Screenplay. - </SimpleCard> - <SimpleCard title="Chronicle — events" icon="seti:db" link="/chronicle/"> - The event-sourcing engine: gRPC/protobuf boundary, .NET-first client, TypeScript and Elixir clients/contracts, MongoDB/PostgreSQL/SQL Server/SQLite storage, Orleans inside. - </SimpleCard> - <SimpleCard title="AuthProxy — edge" icon="seti:lock" link="/authproxy/"> - Authentication, tenant resolution, identity enrichment, routing, and invite-based onboarding in front of your services. - </SimpleCard> - <SimpleCard title="Auth and compliance — end to end" icon="seti:lock" link="/auth-and-compliance/"> - How authenticated tenant-aware requests become authorized operations, protected events, and released read models. - </SimpleCard> - <SimpleCard title="Arc — CQRS + proxies" icon="puzzle" link="/arc/"> - Full-stack CQRS with generated, typed C# → TypeScript proxies. It pairs naturally with Chronicle, and can also run over MongoDB or EF Core. - </SimpleCard> - <SimpleCard title="Components — React" icon="laptop" link="/components/"> - Command dialogs, forms, and live data tables that consume Arc's proxies — a screen is a few lines. - </SimpleCard> - <SimpleCard title="CLI + Workbench — operate" icon="rocket" link="/cli/"> - Inspect and interact with a running store: events, observers, read models, recommendations, jobs, replay, failed partitions, and diagnostics. - </SimpleCard> - <SimpleCard title="Specifications — test" icon="approve-check" link="/testing-with-cratis/"> - BDD-style specs over xUnit, with Arc command scenarios and Chronicle in-process event, read-model, and reactor scenarios. - </SimpleCard> - <SimpleCard title="Fundamentals — foundation" icon="seti:folder" link="/fundamentals/"> - The shared .NET and TypeScript building blocks the rest of the stack is built on. - </SimpleCard> -</CardGrid> - -## Where to start - -<CardGrid> - <SimpleCard title="Get started" icon="rocket" link="/chronicle/get-started/"> - Scaffold a project and watch one event flow through a projection and a reactor — in minutes. - </SimpleCard> - <SimpleCard title="Build a full-stack feature" icon="open-book" link="/build-a-full-app/"> - Put Chronicle, Arc, and Components together — backend to React, type-safe throughout. - </SimpleCard> - <SimpleCard title="Test a full-stack slice" icon="approve-check" link="/testing-with-cratis/"> - Turn an event-model column into given/when/then specs. - </SimpleCard> - <SimpleCard title="Choosing where to start" icon="right-arrow" link="/adopting-cratis/"> - Greenfield or brownfield — pick an entry point and adopt one piece at a time. - </SimpleCard> - <SimpleCard title="Why developers choose Cratis" icon="approve-check" link="/why-cratis/"> - How the products stand alone and compose, and when the stack is — and isn't — the right fit. - </SimpleCard> -</CardGrid> diff --git a/web/src/content/docs/event-modeling.mdx b/web/src/content/docs/event-modeling.mdx deleted file mode 100644 index f548f846..00000000 --- a/web/src/content/docs/event-modeling.mdx +++ /dev/null @@ -1,122 +0,0 @@ ---- -title: Event Modeling -description: What event modeling is, why it gives event-sourced design a shared language, and why its building blocks map almost one-to-one onto Cratis — commands, events, read models, projections, and reactors. ---- - -import { CardGrid, Aside } from '@astrojs/starlight/components'; -import SimpleCard from '@components/SimpleCard.astro'; -import TopicHero from '@components/TopicHero.astro'; - -<TopicHero icon="list-format" eyebrow="The Cratis Stack" title="Design the flow before you write the code"> -The hard part of an event-sourced system isn't the plumbing — it's deciding *what the events are*, where commands come from, and which read model each screen needs. Get that wrong and you find out late, in code. **Event modeling** is a way to lay the whole flow out on a single timeline first. And because that timeline's pieces are exactly Cratis's building blocks, the model you draw *is* the blueprint for your slices. -</TopicHero> - -## What it is - -[Event modeling](https://eventmodeling.org/) describes a system as a **timeline of information moving between people and machines** — you read it left to right, like a comic strip. A user does something on a **screen**, that issues a **command**, the command records an **event**, and events are projected into **read models** that feed the next screen. Nothing else. The whole notation is five kinds of sticky note: - -```mermaid -eventmodeling - -tf 01 ui Reservations.ReserveBookScreen -tf 02 cmd Reservations.ReserveBook -tf 03 evt Reservations.BookReserved -tf 04 rmo Reservations.Availability ->> 03 -tf 05 ui Reservations.CatalogScreen ->> 04 -tf 06 pcr Reservations.StockKeeper -tf 07 cmd Inventory.DecreaseStock -tf 08 evt Inventory.StockDecreased -``` - -Read it as a story. A reader reserves a book on a **screen** (`ui`); that fires the `ReserveBook` **command** (`cmd`), which records the `BookReserved` **event** (`evt`) — an immutable fact. That fact is projected into an `Availability` **read model** (`rmo`) that the catalog screen reads. And a **processor** (`pcr`) watching reservations quietly issues a follow-up command to decrease stock. The rows are **swimlanes** — one per bounded context (`Reservations`, `Inventory`). - -That's the entire vocabulary: **wireframe, command, event, read model, processor.** No class diagrams, no database schemas — just the flow of facts. - -<Aside type="tip" title="Why past tense matters"> -Events are named in the **past tense** — `BookReserved`, not `ReserveBook` — because they are facts that already happened and can never be un-happened. If you catch yourself wanting a nullable field on an event ("reserved-or-maybe-cancelled"), that's the model telling you it's really *two* events. -</Aside> - -## Why it's useful - -A timeline this plain does three things a box-and-arrows architecture diagram can't: - -- **It's a shared language.** A domain expert and an engineer read the *same* picture. The expert can point at `BookReserved` and say "no, that only happens after payment" — a correction that would otherwise surface as a bug three sprints later. -- **It forces completeness.** Every event must trace back to a command; every command to a screen or a processor; every screen must read a real read model. Walk the timeline and the gaps light up — a screen with no data behind it, an event nothing produces. You find the holes on a whiteboard, not in production. -- **There are only four shapes.** Every connection in a correct model is one of four repeating patterns. Once you can see them, you can see *any* feature as a handful of small, known pieces. - -## The four patterns - -These four shapes are the whole grammar. Every slice of every system is built from them: - -| Pattern | Shape | What it does | -|---|---|---| -| **Command** | `ui ➜ cmd ➜ evt` | A user acts; a command validates and records a fact. *The write side.* | -| **View** | `evt ➜ rmo ➜ ui` | Facts are projected into the state a screen reads. *The read side.* | -| **Automation** | `evt ➜ pcr ➜ cmd` | A processor reacts to a fact and issues a command on its own. | -| **Translation** | external `evt ➜ pcr ➜ cmd` | A fact from another context is adapted into this one's commands. | - -The first two are the everyday CQRS loop. The last two are how work flows *between* contexts without anything being directly coupled — a processor watches for a fact and acts. A translation begins on a **reset frame** (`rf`) because its triggering event comes from somewhere else: - -```mermaid -eventmodeling - -rf 01 evt Payments.PaymentReceived -tf 02 pcr Orders.PaymentTranslator -tf 03 cmd Orders.ConfirmOrder -tf 04 evt Orders.OrderConfirmed -``` - -## Why it fits Cratis almost one-to-one - -Here's the part that makes event modeling more than a whiteboard exercise for Cratis: **every block is a real Cratis primitive,** and the four patterns are *literally* the four kinds of [vertical slice](/arc/vertical-slices/) Cratis already organizes code around. The model isn't a sketch you translate — it's the slice, drawn. - -| Event-model block | What it means | In Cratis | -|---|---|---| -| Wireframe (`ui`) | a screen the user sees | a React screen — [Components](/components/) + Arc.React | -| Command (`cmd`) | an intent to change state | a `[Command]` record with `Handle()` — [Arc](/arc/) | -| Event (`evt`) | a fact that happened | an `[EventType]` record in [Chronicle](/chronicle/) | -| Read model (`rmo`) | state a screen reads | a `[ReadModel]` built by a [projection](/chronicle/projections/) | -| Processor (`pcr`) | automation that reacts to facts | an `IReactor` in Chronicle | -| Swimlane | a bounded context | a vertical slice / feature folder | - -And the patterns line up exactly with how a Cratis slice is classified: - -| Event-modeling pattern | Cratis slice type | -|---|---| -| Command | **State Change** — command + events | -| View | **State View** — read model + projection | -| Automation | **Automation** — a reactor that decides and acts | -| Translation | **Translation** — a reactor that adapts events across slices | - -This is not a coincidence — both come from the same idea, that **events are the source of truth and everything else is derived from them.** A `[Command]`'s `Handle()` returns the event it records; a `[ReadModel]` declares the events it's built from; an `IReactor` reacts to a fact and issues the next command. Draw the timeline and you've named your commands, your events, your read models, and your reactors — in the order they happen. - -<Aside type="note" title="The model is the column, the slice is the code"> -Read a single vertical column of an event model — one screen, its command, the event, the read model behind the next screen — and you're looking at one [vertical slice](/arc/vertical-slices/): one feature folder with the command, event, projection, and React component that implement it. The [full-stack walkthrough](/build-a-full-app/) builds exactly that column, block by block. -</Aside> - -## The model is also the test - -Event modeling already has the given/when/then shape baked in. The left side of the column is the context, the command is the behavior under test, and the event/read-model blocks are the expected outcome: - -| Event-model block | Specification role | -|---|---| -| Existing events before the command | **Given** | -| Command or reactor in the column | **When** | -| New event, projected read model, or side effect | **Then** | - -That is why Cratis uses BDD-style specifications so heavily. `Cratis.Specifications.XUnit` gives xUnit the `Establish()` / `Because()` / `[Fact]` lifecycle, while Arc and Chronicle add scenarios that speak the same language: `CommandScenario<TCommand>` for the command, `EventScenario.Given` for past facts, `ReadModelScenario<TReadModel>` for projections, and `ReactorScenario<TReactor>` for automation. - -[Testing with Cratis](/testing-with-cratis/) shows a concrete event-model column translated into an executable stack spec. - -## When it's the wrong fit - -Event modeling earns its keep when behavior is interesting — when facts accumulate, screens derive state from history, and work flows between contexts. It's overkill when there isn't much of a story to tell: a settings form that reads and writes a single row, a static lookup table, a pure request/response with no meaningful event worth keeping. If a feature is honestly just CRUD over one record, model it as CRUD. Reach for event modeling when the *flow* — not the storage — is where the complexity lives. - -## Go deeper - -<CardGrid> - <SimpleCard title="Build the slice it describes" icon="add-document" link="/build-a-full-app/" /> - <SimpleCard title="Test the slice it describes" icon="approve-check" link="/testing-with-cratis/" /> - <SimpleCard title="Events & projections in Chronicle" icon="seti:db" link="/chronicle/" /> - <SimpleCard title="Vertical slices in Arc" icon="seti:folder" link="/arc/vertical-slices/" /> -</CardGrid> diff --git a/web/src/content/docs/faq.mdx b/web/src/content/docs/faq.mdx deleted file mode 100644 index cb6a14ed..00000000 --- a/web/src/content/docs/faq.mdx +++ /dev/null @@ -1,64 +0,0 @@ ---- -title: FAQ -description: Practical answers for teams evaluating Cratis, event sourcing, CQRS, adoption paths, production readiness, and support. ---- - -import { CardGrid, LinkCard } from '@astrojs/starlight/components'; - -This FAQ answers the questions that usually come up before a team commits to Cratis. For deeper explanations, follow the linked pages. - -<CardGrid> - <LinkCard title="Choose a starting point" description="Greenfield, brownfield, Arc-only, Chronicle-only, or full stack." href="/adopting-cratis/" /> - <LinkCard title="Learn the model" description="Follow role-based paths from event sourcing basics to operations." href="/learning-paths/" /> - <LinkCard title="Ask the community" description="Bring open questions to Discord when the answer depends on your context." href="/community/" /> -</CardGrid> - -## Is Cratis only for event sourcing? - -No. Chronicle is the event-sourcing engine, but Arc can be used without Chronicle for CQRS over current-state storage. Components can be used with Arc-generated proxies, and Fundamentals provides shared .NET and TypeScript utilities. - -Start with [Adopting Cratis](/adopting-cratis/) and [CQRS without event sourcing](/arc/arc-without-event-sourcing/). - -## Should every feature be event-sourced? - -No. Event sourcing is a strong default for information systems that care about history, decisions, audit, process, replay, and integration. It is not automatically worth it for static lookup tables or settings screens where the current value is the whole story. - -Read [When to use event sourcing](/chronicle/concepts/when-to-use-event-sourcing/) and [CRUD, EF Core, and Chronicle](/chronicle/coming-from-crud/). - -## Can I use Chronicle without Arc? - -Yes. Chronicle has its own clients and hosting model. Arc gives you an application framework around commands, queries, and generated frontend proxies, but Chronicle can be used directly from services, workers, or APIs. - -Start with [Chronicle getting started](/chronicle/get-started/) and [Chronicle architecture](/chronicle/architecture/). - -## Can I use Arc without Chronicle? - -Yes. Arc supports commands, queries, validation, generated TypeScript proxies, and integrations over current-state persistence. That is useful when you want typed full-stack CQRS before adopting event sourcing, or when a bounded slice does not need an event log. - -Read [CQRS without event sourcing](/arc/arc-without-event-sourcing/). - -## Is Cratis production-ready? - -The products include production-oriented pieces such as TLS configuration, storage configuration, OpenTelemetry, deployment guidance, and operational CLI diagnostics. You still need to make deliberate choices about version tags, storage, backups, secrets, identity, replay, monitoring, and incident handling. - -Use [Production readiness](/production-readiness/) as the cross-stack checklist. - -## Which storage engines are supported? - -Chronicle documents MongoDB, PostgreSQL, Microsoft SQL Server, and SQLite as storage options. Arc integrates with MongoDB and Entity Framework Core for application persistence. - -See [Chronicle storage](/chronicle/hosting/configuration/storage/), [Arc MongoDB](/arc/backend/mongodb/), and [Arc Entity Framework Core](/arc/backend/entity-framework/). - -## How does Cratis compare to MediatR, MVC, EF Core, Marten, or Wolverine? - -Cratis is not a single library replacement. Chronicle is an event-sourcing engine; Arc is a CQRS and proxy-generation framework; Components is a React library built around those generated contracts. The comparisons are about architecture shape more than API surface. - -Start with [MediatR, MVC, and Arc](/arc/coming-from-mediatr-and-mvc/) and [CRUD, EF Core, and Chronicle](/chronicle/coming-from-crud/). - -## Where do I ask for help? - -Use [Community and help](/community/) for open questions, [Feedback and suggestions](/feedback/) for product and docs feedback, GitHub issues for durable bugs and feature requests, and [Work with us](/work-with-us/) when you need private, dedicated time. - -## Where do I see what changed? - -Use [What's new](/whats-new/) for curated release digests and links to repository releases. diff --git a/web/src/content/docs/feedback.mdx b/web/src/content/docs/feedback.mdx deleted file mode 100644 index 64f75912..00000000 --- a/web/src/content/docs/feedback.mdx +++ /dev/null @@ -1,66 +0,0 @@ ---- -title: Feedback and suggestions -description: Share ideas, product feedback, documentation gaps, and suggestions for improving Cratis. ---- - -import { CardGrid, LinkCard, Aside } from '@astrojs/starlight/components'; - -Cratis gets better when people tell us where the model is confusing, where the tools feel rough, and where the docs leave a gap. You do not need a fully shaped proposal before you speak up. A rough observation from a real project is often the most useful feedback. - -Use this page when you want to suggest an improvement, report that something was hard to understand, or tell us what would make Cratis easier to adopt. - -<CardGrid> - <LinkCard title="Discuss an idea in Discord" description="Use Discord for early suggestions, adoption feedback, design questions, and exploratory product ideas." href="https://discord.gg/kt4AMpV8WV" /> - <LinkCard title="Open a documentation issue" description="Report unclear guides, missing examples, broken explanations, or topics you expected to find." href="https://github.com/Cratis/Documentation/issues" /> - <LinkCard title="Read the roadmap" description="See what is stable, what is being hardened, what is being explored, and where feedback helps." href="/roadmap/" /> - <LinkCard title="Understand governance" description="See how ideas become issues, design notes, pull requests, releases, and documentation." href="/governance/" /> - <LinkCard title="Browse product issues" description="Turn actionable product feedback into an issue in the repository that owns the behavior." href="https://github.com/cratis" /> -</CardGrid> - -## What to send where - -| Feedback | Best place | -|---|---| -| "This concept confused me" | [Cratis Discord](https://discord.gg/kt4AMpV8WV), then a [Documentation issue](https://github.com/Cratis/Documentation/issues) if the docs need a change | -| "This API or workflow feels awkward" | [Cratis Discord](https://discord.gg/kt4AMpV8WV) for discussion, then the matching product issue tracker | -| "The docs are missing an example" | [Documentation issues](https://github.com/Cratis/Documentation/issues) | -| "This bug is reproducible" | The matching product issue tracker | -| "I have an idea for a feature" | [Cratis Discord](https://discord.gg/kt4AMpV8WV) for early shaping, then a GitHub issue when the proposal is concrete | -| "I want to understand what is planned" | [Roadmap](/roadmap/) | -| "I want to propose a larger design change" | [Governance](/governance/) | -| "I want to contribute the change" | [Contributing](/contributing/) | - -If you are not sure where the feedback belongs, start in [Community and help](/community/). We can help route it. - -## Product issue trackers - -| Area | Issue tracker | -|---|---| -| Chronicle event store, clients, Workbench, and hosting | [Chronicle issues](https://github.com/Cratis/Chronicle/issues) | -| Arc commands, queries, proxy generation, validation, and backend/frontend integration | [Arc issues](https://github.com/Cratis/Arc/issues) | -| React components, forms, dialogs, tables, styling, and Storybook | [Components issues](https://github.com/Cratis/Components/issues) | -| CLI commands, diagnostics, and AI command catalogs | [CLI issues](https://github.com/Cratis/cli/issues) | -| Fundamentals libraries and shared .NET/TypeScript helpers | [Fundamentals issues](https://github.com/Cratis/Fundamentals/issues) | -| Documentation site, guides, examples, and broken links | [Documentation issues](https://github.com/Cratis/Documentation/issues) | - -## Useful feedback - -The most useful feedback tells us what happened in context: - -- What you were trying to do. -- Which product or page you were using. -- What you expected to happen. -- What happened instead, or where you got stuck. -- What would have made the next step obvious. - -For documentation feedback, a short note like "I reached this page looking for X, but found Y" is enough. For product feedback, include the package version, runtime, command, API, or component involved when you know it. - -<Aside type="tip" title="Real examples beat polished proposals"> -If you can share the event, command, projection, screen, CLI command, or paragraph that caused the friction, start there. We can help turn the observation into a concrete issue or improvement. -</Aside> - -## What happens next - -Open feedback may stay as a conversation until the right shape is clear. Once it becomes actionable, we prefer to capture it as a GitHub issue so it has a durable record, can be linked from pull requests, and does not disappear in chat history. - -For questions that need an answer more than a change, use [Community and help](/community/). diff --git a/web/src/content/docs/glossary.md b/web/src/content/docs/glossary.md deleted file mode 100644 index 22b8ac89..00000000 --- a/web/src/content/docs/glossary.md +++ /dev/null @@ -1,61 +0,0 @@ ---- -title: Glossary -description: One place for the vocabulary of the Cratis stack — event sourcing, CQRS, and full-stack terms, each defined once and linked to its full explanation. ---- - -Event sourcing comes with its own vocabulary, and the Cratis stack adds a few terms of its own. This is -the one place each term is defined — one term, one meaning, everywhere. Where a term has a fuller -explanation, the name links to it. - -## Event sourcing - -These are the core ideas behind [Chronicle](/chronicle/). The [Concepts](/chronicle/concepts/) section -explains how they fit together. - -| Term | Definition | -| --- | --- | -| [Event](/chronicle/concepts/event/) | A fact — something that happened, named in the past tense (`AccountOpened`). Immutable, never nullable, never multipurpose. | -| [Event type](/chronicle/concepts/event-type/) | The schema and identity of an event — its shape and name. | -| [Event source](/chronicle/concepts/event-source/) | The thing an event happened *to*, identified by an id (an account, a book). | -| [Event sequence](/chronicle/concepts/event-sequence/) | An ordered, append-only stream of events you can subscribe to. The **event log** is the primary one — your source of truth. | -| **Sequence number** | An event's position within a sequence. | -| [Event store](/chronicle/concepts/event-store/) | The database that holds event sequences. | -| **Event sourcing** | Storing state as the full history of events, deriving current state by replaying them. See [when to use it](/chronicle/concepts/when-to-use-event-sourcing/). | -| [Namespace](/chronicle/concepts/namespaces/) | A partition of an event store, used for multi-tenancy. | -| **Tenant** | An isolated set of data for one customer — see [namespaces](/chronicle/namespaces/). | -| **Identity** | Who or what caused an event. | -| **Correlation** | Links events that belong to the same logical operation. | -| **Causation** | Links an event to the event that caused it. | -| [Tags](/chronicle/concepts/tagging/) | Labels on events for filtering and correlation — see also [event metadata tags](/chronicle/concepts/event-metadata-tags/). | -| [Aggregate](/arc/backend/chronicle/aggregates/) | A consistency boundary that encapsulates behavior and produces events. | - -## Turning events into state - -How events become the things you read and the actions you take. - -| Term | Definition | -| --- | --- | -| [Observer](/chronicle/concepts/observers/) | Anything that watches events and acts — a projection, reducer, or reactor. | -| [Projection](/chronicle/projections/) | Builds a read model by mapping events declaratively. | -| [Reducer](/chronicle/reducers/) | Builds a read model by folding events imperatively. | -| [Reactor](/chronicle/reactors/) | Produces side effects (notifications, calls to other systems) in response to events. | -| [Read model](/chronicle/read-models/) | A queryable view shaped for one screen or question, built from events. | -| **Changeset** | The set of changes an observer applies for a single event. | -| [Eventual consistency](/chronicle/concepts/consistency/) | A read model that catches up shortly after an event is appended — the default. | -| [Immediate consistency](/chronicle/concepts/consistency/) | A read model updated synchronously, before the append returns — for reads you must get right now. | - -## Full-stack: CQRS and the frontend - -The terms [Arc](/arc/) and [Components](/components/) add on top of Chronicle. - -| Term | Definition | -| --- | --- | -| [Command](/arc/backend/commands/) | An intent to change state — a record with a `Handle()` method that appends events. | -| [Query](/arc/backend/queries/) | A read of data, exposed to the frontend as a typed proxy. | -| **Observable query** | A query that holds a live connection and pushes new results when the data changes. | -| **CQRS** | Command Query Responsibility Segregation — separating the write side (commands) from the read side (queries). | -| [Proxy generation](/arc/backend/proxy-generation/) | Arc emitting a typed TypeScript client from your C# commands and queries at build time. | -| [Concept](/fundamentals/csharp/concepts/) | A strongly-typed wrapper around a primitive (`AccountId` over `Guid`) so the compiler catches mix-ups. | -| [Vertical slice](/arc/vertical-slices/) | Everything for one behavior — command, events, projection, UI, specs — kept together in one folder. | - -New to all this? Start with [Why developers choose Cratis](/why-cratis/), then the [Chronicle tutorial](/chronicle/tutorial/). diff --git a/web/src/content/docs/governance.mdx b/web/src/content/docs/governance.mdx deleted file mode 100644 index eaed6296..00000000 --- a/web/src/content/docs/governance.mdx +++ /dev/null @@ -1,45 +0,0 @@ ---- -title: Governance -description: How Cratis decisions are discussed, shaped, captured, reviewed, merged, released, and documented. ---- - -import { CardGrid, LinkCard, Aside } from '@astrojs/starlight/components'; - -Cratis is an open-source project, but not every decision belongs in the same channel. Small fixes can move directly through issues and pull requests. Larger design choices need discussion, context, and a durable record. - -<CardGrid> - <LinkCard title="Share a suggestion" description="Use the feedback page for product ideas, rough edges, documentation gaps, and feature suggestions." href="/feedback/" /> - <LinkCard title="Join the community" description="Use Discord for early discussion and questions that need context." href="/community/" /> - <LinkCard title="Contribute" description="Read the contribution workflow, code standards, and project values." href="/contributing/" /> -</CardGrid> - -## Decision path - -| Change | Path | -|---|---| -| Small documentation fix | Pull request or documentation issue | -| Reproducible bug | Product issue, then pull request | -| Small feature | Product issue with motivation and expected behavior | -| Cross-product design | Discord discussion, then a durable GitHub issue or design note | -| Breaking change | Design discussion, migration notes, release note, and documentation update | -| Security fix | Private report first, public disclosure only after coordination | - -## How proposals become work - -1. Start with the problem, not the implementation. -2. Discuss early in [Community and help](/community/) when the shape is unclear. -3. Capture actionable work in the repository that owns the behavior. -4. Link related docs, examples, issues, and pull requests. -5. Update documentation and release notes when behavior changes. - -## Maintainer judgment - -Maintainers may ask for narrower scope, more context, tests, documentation, migration guidance, or a different product boundary. The goal is to keep Cratis coherent across Chronicle, Arc, Components, CLI, Fundamentals, tools, and the docs. - -<Aside type="note" title="The project favors connected changes"> -Cratis spans backend, frontend, storage, operations, and documentation. A good proposal explains where the change lands in that loop and what it means for users. -</Aside> - -## Community standards - -The [Code of Conduct](/contributing/code_of_conduct/) applies across repositories, Discord, issues, pull requests, and project spaces. Security reports use the private process on [Security](/security/). diff --git a/web/src/content/docs/index.mdx b/web/src/content/docs/index.mdx deleted file mode 100644 index 6932450c..00000000 --- a/web/src/content/docs/index.mdx +++ /dev/null @@ -1,255 +0,0 @@ ---- -title: Cratis -description: Build event-sourced applications with Chronicle, Arc, and Components — the full-stack, type-safe Cratis platform. -template: splash -hero: - title: Event sourcing you can actually be productive with - tagline: "We treat event sourcing as the default architecture for information systems: capture what happened as immutable facts, project exactly the read models you need, and ship a full-stack, type-safe app — identity, tenancy, backend, React, storage choice, and operations — without the boilerplate." - image: - dark: ../../assets/cratis-mark-dark.svg - light: ../../assets/cratis-mark-light.svg - actions: - - text: Get started - link: '#get-started-in-3-steps' - icon: right-arrow - - text: Explore samples - link: /samples/ - variant: minimal - icon: rocket - - text: Why event sourcing? - link: /chronicle/why-event-sourcing/ - variant: minimal - icon: open-book - - text: Learn more - link: /chronicle/ - variant: minimal - icon: open-book - - text: GitHub - link: https://github.com/cratis - variant: minimal - - text: Community - link: /community/ - variant: minimal - - text: Work with us - link: /work-with-us/ - variant: minimal ---- - -import { Card, CardGrid, LinkCard, Tabs, TabItem, Steps } from '@astrojs/starlight/components'; -import SimpleCard from '../../components/SimpleCard.astro'; -import StackJourney from '../../components/StackJourney.astro'; -import RotatingHero from '../../components/RotatingHero.astro'; - -{/* Rotates the hero above through the platform's stories (Event Sourcing, CQRS, - Application Framework, the Stack, AI-Native, Studio, Screenplay, Prologue). The - frontmatter hero is the first pane and the no-JS fallback — keep the two in sync. */} -<RotatingHero /> - -## Get started in 3 steps - -Install the .NET templates and spin up your first full-stack Cratis app — Chronicle, Arc, and a React frontend, all wired up — in minutes. - -<Steps> - -1. **Install the Cratis templates.** Add the official project templates to your .NET CLI. You only need to do this once. - - ```bash - dotnet new install Cratis.Templates - ``` - -2. **Create your application.** Scaffold a new app complete with Chronicle, Arc, and a React frontend. - - ```bash - dotnet new cratis -n MyApp --allow-scripts Yes - ``` - -3. **Run it.** Start the supporting services, then the backend and the frontend from your new app folder. - - ```bash - cd MyApp && docker compose up -d - dotnet run - yarn dev - ``` - -</Steps> - -<CardGrid> - <LinkCard - title="Explore runnable samples" - description="Choose a focused Chronicle, Arc + React, multi-tenant, cross-store, operations, model-first, or full Library journey." - href="/samples/" - /> - <LinkCard - title="Open the sample source" - description="Browse the projects, diagrams, run commands, and ideas to try in the Cratis Samples repository." - href="https://github.com/Cratis/Samples" - /> -</CardGrid> - -Prefer a guided path? The [Chronicle getting-started walkthrough](/chronicle/get-started/) takes one event through a projection and a reactor, step by step. - -<StackJourney - eyebrow="One feature, every layer" - title="See the full-stack loop before you choose a product" - intro="Cratis is most different when you follow one behavior all the way through: model the domain, resolve identity and tenant at the edge, build the slice, record the event, render the React screen, then inspect the running system with tools and AI." -/> - -## Why developers choose Cratis - -Cratis is for teams that want the domain model to be the running system, not a diagram beside it. The big win is fit: Chronicle, Arc, Components, AuthProxy, Studio, the CLI, and the AI tooling are opinionated in the same direction, so codebases stay consistent, predictable, and easier for both developers and agents to work in. - -<CardGrid> - <SimpleCard title="Everything fits together" icon="puzzle" link="/cratis-stack/"> - Domain model, identity, tenant context, backend behavior, React screens, event history, and tools share one set of conventions. - </SimpleCard> - <SimpleCard title="Build behavior, not glue" icon="approve-check" link="/why-cratis/"> - Commands, queries, read models, UI, and tooling share one model. You spend time on the slice, not the handoffs between products. - </SimpleCard> - <SimpleCard title="One contract to React" icon="seti:typescript" link="/arc/understanding-the-proxy-boundary/"> - C# commands and queries generate the TypeScript your frontend calls. Rename a property and the compiler finds every mismatch. - </SimpleCard> - <SimpleCard title="End-to-end foundations" icon="seti:lock" link="/authproxy/"> - Authentication, tenant resolution, identity enrichment, authorization, and tenant isolation are part of the stack instead of every app's custom plumbing. - </SimpleCard> - <SimpleCard title="Event sourcing by default" icon="seti:db" link="/chronicle/why-event-sourcing/"> - Chronicle records facts, projects read models, and keeps the history most information systems eventually need. - </SimpleCard> - <SimpleCard title="Open event backbone" icon="puzzle" link="/chronicle/architecture/"> - Chronicle exposes a gRPC/protobuf boundary, has a first-class .NET client, and also ships TypeScript and Elixir clients/contracts. - </SimpleCard> - <SimpleCard title="Storage choice" icon="seti:db" link="/chronicle/hosting/configuration/storage/"> - Run Chronicle over MongoDB, PostgreSQL, Microsoft SQL Server, or SQLite without changing the event model. - </SimpleCard> - <SimpleCard title="Predictable for AI" icon="rocket" link="/ai/"> - Strong conventions, analyzers, and `.ai` guidance give assistants the same rails developers use to build and operate the system. - </SimpleCard> - <SimpleCard title="Live screens by default" icon="laptop" link="/components/"> - Observable queries and typed components keep forms, tables, and dialogs current without reload code or duplicate client models. - </SimpleCard> - <SimpleCard title="Inspect it live" icon="rocket" link="/cli/"> - Workbench, CLI, OpenTelemetry, recommendations, jobs, replay, and failed partitions show what Chronicle is doing. - </SimpleCard> -</CardGrid> - -## One feature, one slice — typed end to end - -You write the behavior once in C#. Arc generates the TypeScript proxies. The React side can't drift — rename a property in C#, rebuild, and the frontend stops compiling until you fix it. - -<Tabs> -<TabItem label="C# — the slice" icon="seti:c-sharp"> -```csharp -// Command, event, and read model for one feature — in one file. -[Command] -public record RegisterAuthor(AuthorId Id, AuthorName Name) -{ - public AuthorRegistered Handle() => new(Name); // returns the fact that happened -} - -[EventType] -public record AuthorRegistered(AuthorName Name); - -[ReadModel, FromEvent<AuthorRegistered>] -public record Author(AuthorId Id, AuthorName Name) -{ - // This static method *is* the query — exposed over HTTP automatically. - public static Task<IEnumerable<Author>> AllAuthors(IReadModels readModels) => - readModels.Materialized.GetInstances<Author>(); -} -``` -</TabItem> -<TabItem label="React — the screen" icon="seti:react"> -```tsx -// Proxies are generated from the C# above — fully typed, always in sync. -const [authors] = AllAuthors.use(); // typed result, no API client to write - -<CommandDialog command={RegisterAuthor} title="Add author"> - <InputTextField value={i => i.name} title="Name" /> -</CommandDialog> -``` -</TabItem> -</Tabs> - -## The platform pieces - -<CardGrid> - <SimpleCard title="Chronicle" icon="seti:db" link="/chronicle/"> - The event sourcing engine — gRPC/protobuf boundary, .NET-first client, storage choice, Orleans runtime, and rich tooling over a durable event log. - </SimpleCard> - <SimpleCard title="Arc" icon="puzzle" link="/arc/"> - The full-stack CQRS framework — commands, queries, and the generated proxies that keep React in sync. - </SimpleCard> - <SimpleCard title="Components" icon="laptop" link="/components/"> - The React component library — command dialogs, forms, and data tables wired to your proxies. - </SimpleCard> - <SimpleCard title="Screenplay" icon="pencil" link="/screenplay/"> - The modeling language — describe a whole bounded context in one declarative `.play` file that Stage runs live and Studio visualizes. - </SimpleCard> - <SimpleCard title="AuthProxy" icon="seti:lock" link="/authproxy/"> - The edge gateway — authentication, tenant resolution, identity enrichment, routing, and invites. - </SimpleCard> - <SimpleCard title="CLI" icon="rocket" link="/cli/"> - A terminal window into a running store — inspect events, watch observers, and diagnose issues. - </SimpleCard> - <SimpleCard title="AI tooling" icon="approve-check" link="/ai/"> - Skills, rules, analyzers, CLI catalogs, and MCP support that let agents build and operate with the platform's conventions. - </SimpleCard> - <SimpleCard title="Prologue" icon="magnifier" link="/prologue/"> - Bring an existing system in — capture its real HTTP commands, database changes, and telemetry, and interpret them into an event model. - </SimpleCard> -</CardGrid> - -## Choose your starting point - -Cratis is modular. For a new information system, the default path is Chronicle + Arc + Components. For an existing system, a bounded CRUD slice, or a frontend/backend contract problem, you can adopt the pieces separately and still keep the same conventions. - -| If you need... | Start here | -|---|---| -| A durable history of what happened | [Chronicle](/chronicle/) | -| A language-neutral event-store boundary | [Chronicle architecture](/chronicle/architecture/) | -| MongoDB, PostgreSQL, SQL Server, or SQLite storage | [Chronicle storage](/chronicle/hosting/configuration/storage/) | -| Typed commands, queries, and generated React proxies | [Arc](/arc/) | -| Forms, dialogs, and data tables wired to Arc | [Components](/components/) | -| To model a whole bounded context as one declarative file | [Screenplay](/screenplay/) | -| To bring an existing system into Cratis, not build one from scratch | [Prologue](/prologue/) | -| Authentication, tenant resolution, identity enrichment, and routing | [AuthProxy](/authproxy/) | -| A way to inspect a running store | [CLI](/cli/) | -| AI guidance for building and operating the stack | [AI-native development](/ai/) | -| A guided adoption path from an existing app | [Adopting Cratis](/adopting-cratis/) | -| A role-based route through the docs | [Learning paths](/learning-paths/) | -| Runtime, package, and storage compatibility | [Version compatibility](/compatibility/) | -| A production deployment checklist | [Production readiness](/production-readiness/) | -| Help choosing a path, debugging setup, or talking through a design | [Community and help](/community/) | -| Grounded, cited answers to Cratis questions on Discord | [Prompter](/prompter/) | -| Help modeling your domain, adopting Cratis, or training your team | [Work with us](/work-with-us/) | -| A place to suggest improvements or report confusing docs | [Feedback and suggestions](/feedback/) | - -Coming from a familiar architecture? Start with the bridge that matches the shape of your code: [CRUD / EF Core](/chronicle/coming-from-crud/), [MediatR, MVC, and Arc](/arc/coming-from-mediatr-and-mvc/), or [PrimeReact and Components](/components/coming-from-primereact/). - -## Need help getting there? - -Most teams get productive from the documentation, the samples, and the community. Some want the model right the first time — because the early slices set the pattern that everything after them copies, and consistency boundaries are cheap to change now and expensive to change later. - -We build Chronicle, Arc, and Components, and we work with teams directly. - -<CardGrid> - <SimpleCard title="Work with us" icon="rocket" link="/work-with-us/"> - Event modeling, Cratis adoption, architecture review, implementation help, AI-ready foundations, and training — from the people who build the stack. - </SimpleCard> - <SimpleCard title="Ask the community" icon="open-book" link="/community/"> - Free, open, and often faster: bring the question to Discord, where Prompter answers from these docs and the maintainers pick up what it cannot. - </SimpleCard> -</CardGrid> - -## Ready to build? - -<CardGrid> - <LinkCard title="Get started" description="Scaffold a project, run it, and watch one event flow through a projection and a reactor — in minutes." href="/chronicle/get-started/" /> - <LinkCard title="Why developers choose Cratis" description="The platform fit: strong conventions, open Chronicle boundaries, typed contracts, storage choice, and a system you can inspect." href="/why-cratis/" /> - <LinkCard title="Build the library, step by step" description="Learn the model by building a small event-sourced system one concept at a time." href="/chronicle/tutorial/" /> - <LinkCard title="Build a full-stack feature" description="Put Chronicle, Arc, and Components together — backend to React, type-safe throughout." href="/build-a-full-app/" /> - <LinkCard title="Follow a learning path" description="Choose a route for event sourcing, full-stack building, evaluation, operations, frontend work, or contribution." href="/learning-paths/" /> - <LinkCard title="Read the FAQ" description="Quick answers for adoption, production readiness, product boundaries, and support channels." href="/faq/" /> - <LinkCard title="Ask the community" description="Join Discord for questions, design discussion, setup help, and guidance on which GitHub repository owns an issue." href="/community/" /> - <LinkCard title="Ask Prompter on Discord" description="The community's docs assistant answers Cratis questions in seconds, grounded in this site and with citations — or says honestly when the docs don't cover it." href="/prompter/" /> - <LinkCard title="Share feedback" description="Tell us what is confusing, missing, rough, or worth improving in Cratis and the docs." href="/feedback/" /> -</CardGrid> diff --git a/web/src/content/docs/learning-paths.mdx b/web/src/content/docs/learning-paths.mdx deleted file mode 100644 index 080236e2..00000000 --- a/web/src/content/docs/learning-paths.mdx +++ /dev/null @@ -1,62 +0,0 @@ ---- -title: Learning paths -description: Role-based paths through Cratis for event-sourcing beginners, full-stack builders, evaluators, operators, frontend developers, and contributors. ---- - -import { CardGrid, LinkCard } from '@astrojs/starlight/components'; - -The docs are broad because Cratis is a stack. Use a path when you want sequence: what to read first, what to build, and where to go next. - -<CardGrid> - <LinkCard title="New to event sourcing" description="Understand why facts, projections, consistency, and event modeling matter." href="#new-to-event-sourcing" /> - <LinkCard title="Build a full-stack app" description="Follow the command to event to read model to React loop." href="#build-a-full-stack-app" /> - <LinkCard title="Operate Chronicle" description="Run, inspect, diagnose, replay, and harden a Chronicle deployment." href="#operate-chronicle" /> -</CardGrid> - -## New to event sourcing - -1. Read [Why event sourcing](/chronicle/why-event-sourcing/). -2. Read [Event modeling](/event-modeling/). -3. Build the [Chronicle tutorial](/chronicle/tutorial/). -4. Read [Consistency](/chronicle/concepts/consistency/) and [When to use event sourcing](/chronicle/concepts/when-to-use-event-sourcing/). -5. Use [Community and help](/community/) when your domain model raises questions. - -## Build a full-stack app - -1. Start with [Get started](/chronicle/get-started/). -2. Follow [Build a full-stack feature](/build-a-full-app/). -3. Learn [Arc vertical slices](/arc/vertical-slices/). -4. Add screens with [Components getting started](/components/getting-started/). -5. Run the [Samples](/samples/) beside the docs. - -## Evaluate Cratis - -1. Read [Why developers choose Cratis](/why-cratis/). -2. Read [Adopting Cratis](/adopting-cratis/). -3. Check [FAQ](/faq/) and [Version compatibility](/compatibility/). -4. Review [Production readiness](/production-readiness/). -5. Ask adoption questions in [Community and help](/community/), or [Work with us](/work-with-us/) for a dedicated review. - -## Operate Chronicle - -1. Read [Chronicle hosting](/chronicle/hosting/). -2. Review [Chronicle configuration](/chronicle/hosting/configuration/). -3. Learn the [CLI](/cli/) and its operational scenarios. -4. Use [Production readiness](/production-readiness/) as the cross-stack checklist. -5. Keep [Security](/security/) and [What's new](/whats-new/) in your release process. - -## Frontend developer - -1. Read [Arc frontend](/arc/frontend/). -2. Learn [generated proxies](/arc/understanding-the-proxy-boundary/). -3. Use [Components](/components/) for command forms, dialogs, data tables, and styling. -4. Browse [Storybook](/components/storybook/). -5. Use [Samples](/samples/) to see the screen and backend together. - -## Contributor - -1. Read [Contributing](/contributing/). -2. Read [Governance](/governance/). -3. Find product docs and issues in the repository that owns the behavior. -4. Use [Feedback and suggestions](/feedback/) for early ideas. -5. Run the relevant build and docs checks before a pull request. diff --git a/web/src/content/docs/plugins.mdx b/web/src/content/docs/plugins.mdx deleted file mode 100644 index 8c917350..00000000 --- a/web/src/content/docs/plugins.mdx +++ /dev/null @@ -1,123 +0,0 @@ ---- -title: AI skills and plugins -description: Cratis authors one set of framework-aware AI skills and generates passive, host-native packages for Claude Code, Codex, GitHub Copilot, Cursor, Kiro, Junie, Gemini CLI, and Pi. ---- - -import { CardGrid, Aside } from '@astrojs/starlight/components'; -import SimpleCard from '@components/SimpleCard.astro'; -import TopicHero from '@components/TopicHero.astro'; - -<TopicHero icon="puzzle" eyebrow="The Cratis Stack" title="Teach your assistant the Cratis way"> -A general-purpose assistant knows C# and TypeScript, but it does not automatically know why a Cratis command lives beside its fact, why a projection consumes events instead of another read model, or why generated proxies should never be hand-edited. Cratis skills provide that missing framework context as reviewed workflows instead of another prompt you have to remember. -</TopicHero> - -## One behavior, several native packages - -Cratis authors canonical skills in [`Cratis/AI`](https://github.com/Cratis/AI). -Approved exact bytes are projected into the generated -[`Cratis/AI.Distribution`](https://github.com/Cratis/AI.Distribution) repository. -Each target receives the manifest and discovery layout it expects, while the -skill instructions, references, assets, and licenses remain byte-identical. - -```mermaid -flowchart LR - A[Canonical Cratis skill] --> G[Generated and checksummed stage] - G --> C[Claude / Codex / Copilot / Cursor] - G --> K[Kiro / Junie / Gemini / Pi] -``` - -This avoids behavior forks. A projection skill should not teach one architecture -in Claude and another in Copilot merely because their package manifests differ. - -<Aside type="caution" title="Fixture-only distribution"> -The generated repository currently proves packaging, byte parity, lifecycle -smoke, canary/rollback, and protected generation with a sanitized fixture. It is -not a supported installation target and contains no approved real public skill. -Do not install its `main` branch. -</Aside> - -## Public packages stay passive - -The planned public package contains portable Agent Skills and their static -references, assets, and licenses. It excludes executable extensions, lifecycle -scripts, credentials, project facts, authoring evals, engineering hooks, agents, -prompts, and repository tooling. - -Executable Chronicle operations belong to the documented CLI and Chronicle MCP -surfaces, with their own authorization and confirmation boundaries. They are not -smuggled into a passive coding-skills package. - -## Skills guide complete workflows - -Skills are the high-value pieces: your assistant invokes one when a request -matches and follows a Cratis workflow instead of improvising. - -- **`event-modeling`** — settle commands, facts, streams, state views, reactions, - compliance, and scenarios before implementation. -- **`new-vertical-slice`** — build an application behavior end to end through - backend, specs, frontend, documentation, and gates. -- **`add-concept`**, **`add-projection`**, **`add-reactor`**, - **`add-business-rule`**, **`add-ef-migration`** — implement one focused Cratis - artifact by convention. -- **`write-specs`**, **`write-documentation`** — preserve behavior or explain the - product through the repository's preferred structure. -- **`review-code`**, **`review-security`**, **`review-performance`** — review - against Cratis architecture and quality boundaries. -- **`diagnose-slice`** and **`inspect-running-chronicle`** — separate source-code - diagnosis from authorized running-store inspection. - -Framework and client repositories select a different profile from Cratis -applications. A skill reads the repository profile before applying application -vertical-slice conventions. - -## Internal engineering behavior is separate - -Cratis maintainers currently have repository-local rules, agents, prompts, and -hooks in Cratis-owned repositories. Those engineering artifacts can coordinate -specialists, enforce repository gates, and carry contribution conventions, but -they are not part of the planned public passive package. - -See [Using AI as a Cratis maintainer](/ai/cratis-maintainers/) for that workflow. - -## Do not copy configuration between repositories - -The old all-to-all propagation model is frozen. Copying `.ai`, `.claude`, -`.github`, `.agents`, or `.pi` trees creates mixed versions and lets a consuming -repository become an accidental distributor. - -The replacement uses immutable generated releases and version pins. A consuming -repository owns only its project context and minimal host bootstraps. Updating or -rolling back changes the shared version without overwriting project facts. - -[Trust and distribution](/ai/trust-and-distribution/) explains the approval, -generation, canary, rollback, and retirement gates in detail. - -## Get the operating tools today - -Portable coding skills remain gated, but the documented operating workflow is -available now: - -1. follow the [CLI getting-started guide](/cli/getting-started/); -2. run `cratis init` in your project to generate the supported CLI context; -3. connect [Chronicle MCP](/chronicle-mcp/) when your host and authorization - policy allow it. - -Read [Start using AI with Cratis](/ai/getting-started/) for the guided setup and -[AI ecosystem support](/ai/ecosystems/) for host-by-host status. - -## Where to go next - -<CardGrid> - <SimpleCard title="Start using AI" icon="rocket" link="/ai/getting-started/"> - Set up the available operating tools and choose the right coding-skills path. - </SimpleCard> - <SimpleCard title="Ecosystem support" icon="puzzle" link="/ai/ecosystems/"> - Compare generated formats and release gates across supported hosts. - </SimpleCard> - <SimpleCard title="Cratis maintainers" icon="seti:folder" link="/ai/cratis-maintainers/"> - Use repository-local skills and gates without restarting propagation. - </SimpleCard> - <SimpleCard title="Code analysis" icon="seti:json" link="/code-analysis/"> - See the compiler and lint gates that verify contracts independently of the assistant. - </SimpleCard> -</CardGrid> diff --git a/web/src/content/docs/production-readiness.mdx b/web/src/content/docs/production-readiness.mdx deleted file mode 100644 index 9a0bfd23..00000000 --- a/web/src/content/docs/production-readiness.mdx +++ /dev/null @@ -1,59 +0,0 @@ ---- -title: Production readiness -description: Cross-stack checklist for deploying and operating Cratis with versioned artifacts, TLS, storage, secrets, identity, observability, replay, and support paths. ---- - -import { CardGrid, LinkCard, Aside } from '@astrojs/starlight/components'; - -Production readiness is not one switch. Cratis gives you the event log, generated contracts, operational tools, and hosting configuration, but you still need explicit choices for storage, secrets, identity, deployment, monitoring, replay, and incident handling. - -Use this checklist before deploying Chronicle or a full Cratis application. - -<CardGrid> - <LinkCard title="Chronicle production hosting" description="Container deployment, storage, scaling, TLS, and operational concerns for Chronicle." href="/chronicle/hosting/production/" /> - <LinkCard title="Version compatibility" description="Runtime, package, React, Node, storage, and tooling compatibility." href="/compatibility/" /> - <LinkCard title="Security" description="Report vulnerabilities privately and review operational hardening expectations." href="/security/" /> -</CardGrid> - -## Release and deployment - -- Pin package versions and Docker image tags. -- Keep environment-specific configuration outside images. -- Run the full build and docs/test gates before release. -- Keep a rollback plan for application code, projections, configuration, and infrastructure. -- Record which Chronicle, Arc, Components, CLI, and app versions are deployed together. - -## Storage and data - -- Choose the Chronicle storage engine deliberately: MongoDB, PostgreSQL, Microsoft SQL Server, or SQLite. -- Configure backups and restore drills for the chosen store. -- Decide retention for replay-generated read model versions. -- Plan how projection rebuilds and event replays are executed and monitored. -- Keep event type migrations tested before old event generations reach production. - -## Security and identity - -- Use TLS for service-to-service and client connections, or document where TLS is terminated. -- Store secrets in a real secret manager or platform secret store. -- Configure authentication, admin bootstrap, clients, and identity-provider certificates intentionally. -- Avoid development-only passwords, certificates, and generated secrets in staging or production. -- Follow the private reporting path on [Security](/security/) for vulnerabilities. - -## Operations - -- Enable OpenTelemetry where your environment can collect traces and metrics. -- Decide who watches failed partitions, jobs, observers, projections, and subscriptions. -- Install and configure the [CLI](/cli/) for production diagnostics. -- Keep runbooks for replaying projections, retrying failed partitions, and verifying appended events. -- Define escalation paths: community, GitHub issue, or [Work with us](/work-with-us/) for dedicated support. - -## User-facing application - -- Decide where AuthProxy, tenant resolution, authentication, and authorization live. -- Keep generated TypeScript proxies in the frontend build so drift fails at compile time. -- Verify command validation, optimistic concurrency, and error handling from the UI. -- Use Components only after the provider, theme, and peer dependencies are configured. - -<Aside type="tip" title="Production readiness is a review loop"> -Revisit this page whenever you add a storage engine, identity provider, deployment region, replay strategy, or critical event type migration. -</Aside> diff --git a/web/src/content/docs/professional-help.mdx b/web/src/content/docs/professional-help.mdx deleted file mode 100644 index 493cf43d..00000000 --- a/web/src/content/docs/professional-help.mdx +++ /dev/null @@ -1,55 +0,0 @@ ---- -title: Professional help -description: How dedicated, private help differs from community support — what an engagement looks like, when it is worth it, and where to go instead. ---- - -import { CardGrid, LinkCard, Aside } from '@astrojs/starlight/components'; - -The open community is the right place for most questions: public design discussion, help getting unstuck, and a second pair of eyes on a model. It is free, it is fast, and it stays open to everyone. - -Sometimes a team needs something the community cannot give it — private domain context, a scheduled workshop, a written architecture review, or hands-on help moving a real slice into Cratis. That is what dedicated help is for. This page explains how it differs from community support and when it is worth the cost; [Work with us](/work-with-us/) covers what we actually help with and how to get in touch. - -<CardGrid> - <LinkCard title="Work with us" description="What we help teams with, how we work, and how to start a conversation." href="/work-with-us/" /> - <LinkCard title="Ask in the community first" description="Use Discord when the question can be discussed in the open, or when you are still exploring the problem." href="/community/" /> - <LinkCard title="Choose an adoption path" description="Work through the adoption guide before a paid engagement, so the starting point is already clear." href="/adopting-cratis/" /> -</CardGrid> - -## Community help compared with dedicated help - -| | Community | Dedicated | -|---|---|---| -| Cost | Free | Paid, scoped per engagement | -| Response | Best-effort, when someone is available | Agreed up front | -| Context | Public — share only what you can share openly | Private, including code and domain detail | -| Shape | Questions and answers, design discussion | Workshops, reviews, pairing, focused development, training | -| Output | A thread, and often a documentation improvement | A deliverable: a model, a plan, a written review, or working code | -| Depth | One problem at a time | Your whole system, in context | - -## When dedicated help is worth it - -Reach for a paid engagement when the thing blocking you is not a missing answer: - -- **The context cannot be public.** The domain, the schema, or the roadmap is confidential. -- **You need someone's calendar, not their goodwill.** A workshop on a date, or a review by a deadline, is a commitment the community cannot make. -- **The decision is expensive to reverse.** Consistency boundaries, tenancy, and storage choices are cheap to change now and costly later. -- **You need the whole team moved at once.** Training a team is a session, not a thread. -- **You want a durable artifact.** A written review or a modeled backlog outlives a Discord conversation. - -If none of those apply, the community will probably serve you better — and faster. - -## Where to go instead - -| If you need... | Go to... | -|---|---| -| What we help with, and how to make contact | [Work with us](/work-with-us/) | -| An open question, design discussion, or setup help | [Community and help](/community/) | -| A grounded, cited answer from the documentation | [Prompter on Discord](/prompter/) | -| To report a suspected vulnerability | [Security](/security/) | -| To suggest an improvement or report confusing documentation | [Feedback and suggestions](/feedback/) | -| A reproducible bug or a feature request | The matching [GitHub issue tracker](/community/) | -| A route through the documentation for your role | [Learning paths](/learning-paths/) | - -<Aside type="note" title="Commercial work is separate from community help"> -Discord remains open and best-effort, and asking there never depends on being a customer. Professional engagements are private, time-boxed, and handled separately from the open-source support channels. Any engagement also depends on availability and on the relevant consultancy, employer, and conflict-of-interest arrangements. -</Aside> diff --git a/web/src/content/docs/roadmap.mdx b/web/src/content/docs/roadmap.mdx deleted file mode 100644 index cedc8a3b..00000000 --- a/web/src/content/docs/roadmap.mdx +++ /dev/null @@ -1,57 +0,0 @@ ---- -title: Roadmap -description: "Directional roadmap for Cratis: what is stable, what is being hardened, what is being explored, and where feedback helps." ---- - -import { CardGrid, LinkCard, Aside } from '@astrojs/starlight/components'; - -The roadmap is directional, not a delivery contract. Cratis is open source, and priorities move when real adoption uncovers sharper needs. Treat this page as a map of intent: what you can rely on, what is being strengthened, and where feedback will shape the work. - -<CardGrid> - <LinkCard title="Share roadmap feedback" description="Tell us what would unblock adoption, production use, training, or contribution." href="/feedback/" /> - <LinkCard title="Watch what changed" description="Read curated release digests across the Cratis repositories." href="/whats-new/" /> - <LinkCard title="Ask before depending on a future item" description="Use Discord when a planned or exploratory item matters to your project." href="/community/" /> -</CardGrid> - -## Stable foundations - -These are the core areas the documentation treats as the current Cratis foundation: - -| Area | Direction | -|---|---| -| Chronicle | Event log, event types, projections, reducers, reactors, subscriptions, storage choices, hosting, CLI inspection, and testing helpers | -| Arc | Commands, queries, validation, generated TypeScript proxies, backend integrations, identity, tenancy, and CQRS without mandatory event sourcing | -| Components | Command forms, dialogs, data tables, styling, Storybook, and typed React screens over Arc proxies | -| Fundamentals | Concepts, serialization, type discovery, diagnostics helpers, and TypeScript utilities | -| Documentation | Product docs, full-stack guides, samples, API reference, Storybook, community routes, and AI-readable exports | - -## Being hardened - -- Production guidance across the full stack. -- Compatibility documentation across .NET, Node, React, storage engines, and package versions. -- API reference and Storybook publishing. -- CLI diagnostics and AI command catalogs. -- Documentation coverage for common adoption paths and migration paths. -- Testing guidance from unit-level specs to full workflow specs. - -## Being explored - -- Studio and richer visual operations around a running system. -- Lens and developer tools that make event models and running systems easier to inspect. -- More reference architectures and runnable sample applications. -- Professional help, workshops, and training material around Cratis and event sourcing. -- A clearer RFC path for larger design proposals. - -## Feedback wanted - -The most useful roadmap feedback is concrete: - -- Which adoption step felt risky or unclear? -- Which production question blocked a decision? -- Which tool or diagnostic would have saved time? -- Which docs page did you expect but could not find? -- Which integration would make Cratis easier to introduce in an existing system? - -<Aside type="tip" title="Roadmap items need context"> -If a future item matters to your project, start a conversation before depending on it. We can clarify whether it is stable, experimental, or only an idea. -</Aside> diff --git a/web/src/content/docs/samples.mdx b/web/src/content/docs/samples.mdx deleted file mode 100644 index 3b49d5fc..00000000 --- a/web/src/content/docs/samples.mdx +++ /dev/null @@ -1,35 +0,0 @@ ---- -title: Samples -description: Explore runnable Cratis examples, from focused Chronicle applications to a larger Arc and React experience. ---- - -import SampleRoster from '../../components/SampleRoster.astro'; -import SamplesHero from '../../components/SamplesHero.astro'; -import { LinkCard } from '@astrojs/starlight/components'; - -<SamplesHero /> - -```bash -# Choose a sample after cloning the repository -git clone https://github.com/Cratis/Samples.git -cd Samples -``` - -<SampleRoster /> - -## How to explore a sample - -1. **Read its README.** Each sample starts with the shape of the application and the shortest path to running it. -2. **Run the happy path.** Make one change and follow it through the runtime. -3. **Open Chronicle Workbench when Chronicle is involved.** Connect the source code to events, processing, and read-side state. -4. **Change one thing.** Add a fact, a view, or a user interaction and see which parts of the slice move together. - -## Focused examples and full applications - -A focused sample intentionally leaves things out so one concept stays easy to see. The larger applications show how those concepts meet in a real repository. Neither is a production template: use the exact sample README to understand its scope and local-development assumptions. - -<LinkCard - title="Build a full-stack feature" - description="Follow the guided Arc, Chronicle, generated-contract, and React journey in the documentation." - href="/build-a-full-app/" -/> diff --git a/web/src/content/docs/scenarios/camel-casing/index.md b/web/src/content/docs/scenarios/camel-casing/index.md deleted file mode 100644 index e44b20bc..00000000 --- a/web/src/content/docs/scenarios/camel-casing/index.md +++ /dev/null @@ -1,54 +0,0 @@ ---- -title: Configure camel casing for Chronicle and MongoDB -description: How to configure camel casing when you use the Cratis meta package setup in an ASP.NET Core app. ---- - -# Configure camel casing for Chronicle and MongoDB - -This how-to shows how to configure camel casing when you use the Cratis meta package setup in an ASP.NET Core app. - -## Goal - -Configure both Chronicle and Arc MongoDB so projection/read-model data uses camelCase names consistently. - -## Prerequisites - -- You already have an ASP.NET Core app using the Cratis meta package setup (`UseCratisArc()`). -- You already reference Chronicle and Arc MongoDB integration packages. - -## 1. Start from your Cratis meta package setup - -In `Program.cs`, keep your existing Cratis setup and then add Chronicle and MongoDB camel case configuration: - -```csharp -var builder = WebApplication.CreateBuilder(args) - .UseCratisArc(); - -builder.AddCratisChronicle( - configure: chronicleBuilder => chronicleBuilder.WithCamelCaseNamingPolicy()); - -builder.UseCratisMongoDB(configureMongoDB: mongoBuilder => - mongoBuilder.WithCamelCaseNamingPolicy()); - -var app = builder.Build(); -app.UseCratisChronicle(); -``` - -## 2. What this gives you - -With this setup: - -- Chronicle builds projection definitions using camelCase naming. -- MongoDB stores document field names as camelCase. -- Your read-model persistence naming stays consistent across Chronicle and Arc MongoDB. - -For example, a C# property such as `EmailAddress` is persisted as `emailAddress`. - -## 3. Verify in MongoDB - -After your app runs and projections update read models, inspect the stored documents. You should see camelCase fields. - -## Related reference pages - -- [Chronicle camel casing](https://www.cratis.io/chronicle/configuration/camel-casing.html) -- [Arc MongoDB naming policies](https://www.cratis.io/arc/backend/mongodb/naming-policies.html) diff --git a/web/src/content/docs/scenarios/chat/change-stream.md b/web/src/content/docs/scenarios/chat/change-stream.md deleted file mode 100644 index 0907fc14..00000000 --- a/web/src/content/docs/scenarios/chat/change-stream.md +++ /dev/null @@ -1,282 +0,0 @@ ---- -title: Real-Time Chat — Frontend-Managed State ---- - -# Real-Time Chat — Frontend-Managed State - -The two previous guides use `ForRoom.use()`, which applies Arc's delta `ChangeSet` transparently and always gives the component a complete `messagesResult.data` array. That is the right default for most UIs. - -This guide uses `ForRoom.useChangeStream()` instead. The component receives the raw `ChangeSet` — `{ added, replaced, removed }` — and maintains its own `useState` accumulator. **The backend is unchanged from the [in-memory guide](../in-memory).** Only the frontend differs. - -By the end you will have: - -- The same backend as the in-memory guide -- A React component that manages its own message list using `useState` -- A `useEffect` that appends incoming `ChangeSet.added` items to the local list -- A clear picture of when this pattern is preferable to the transparent `use()` hook - ---- - -## When to Use This Pattern - -**`use()` (transparent)** — the right choice for most list UIs. The component renders `messagesResult.data` directly. Arc handles the delta under the hood. - -**`useChangeStream()` (explicit)** — reach for this when you need to react to *what changed*, not just *what the current state is*: - -- Scroll to the bottom only when new messages arrive, not on every render -- Show a "new message" badge when the user is scrolled up -- Animate newly added items with an entry transition -- Track a separate `unreadCount` derived from `added.length` - -All of these require knowing *which* items just appeared. `useChangeStream()` gives you exactly that. - ---- - -## Backend — Unchanged - -The backend is identical to the [in-memory guide](../in-memory). No changes to `ChatRoom.cs` or `ChatRoomPage.cs`. - -```csharp -// Features/Chat/ChatRoomPage.cs — unchanged -[ReadModel] -public record ChatMessage(string User, DateTimeOffset SentAt, string Message) -{ - public static ISubject<IEnumerable<ChatMessage>> ForRoom( - string roomName, - ChatService chatService) - { - var room = chatService.GetChatRoom(roomName); - var relay = new BehaviorSubject<IEnumerable<ChatMessage>>(room.Messages.Value); - room.Messages.Subscribe(relay); - return relay; - } -} -``` - -The server still emits the full message list on each `OnNext()`. Arc still computes a `ChangeSet` server-side and sends only the diff. The difference is how the frontend consumes it. - ---- - -## How the ChangeSet Reaches the Frontend - -Arc's delta mode is always on by default. Here is what the frontend receives: - -| Emission | `ChangeSet` content | -| -------- | ------------------- | -| First connection | All existing messages appear in `added`; `replaced` and `removed` are empty | -| Each new message sent | The one new message appears in `added`; `replaced` and `removed` are empty | - -For chat, `replaced` and `removed` are always empty — messages are immutable and are never deleted. The component only ever needs to handle `added`. - ---- - -## The React Component - -```tsx -// Features/Chat/ChatRoomPage.tsx -import { useState, useEffect, useRef } from 'react'; -import { ForRoom } from './ForRoom'; -import { SendMessage } from './SendMessage'; -import type { ChatMessage } from './ChatMessage'; - -export const ChatRoomPage = () => { - const [roomName, setRoomName] = useState(''); - const [joinedRoom, setJoinedRoom] = useState(''); - const [user, setUser] = useState(''); - const [messageText, setMessageText] = useState(''); - const [messages, setMessages] = useState<ChatMessage[]>([]); - const [unreadCount, setUnreadCount] = useState(0); - const listRef = useRef<HTMLDivElement>(null); - const isAtBottomRef = useRef(true); - - const changes = ForRoom - .when(joinedRoom.length > 0) - .useChangeStream({ roomName: joinedRoom }); - - const [sendCommand, setSendValues] = SendMessage.use(); - - // Append incoming messages to local state. - // On first subscription, all existing messages arrive as changes.added. - // On each subsequent send, the single new message arrives as changes.added. - useEffect(() => { - if (!changes.added.length) return; - setMessages(prev => [...prev, ...changes.added]); - - if (!isAtBottomRef.current) { - setUnreadCount(prev => prev + changes.added.length); - } - }, [changes]); - - // Scroll to bottom when new messages arrive and the user is already at the bottom. - useEffect(() => { - if (!listRef.current || !isAtBottomRef.current) return; - listRef.current.scrollTop = listRef.current.scrollHeight; - }, [messages]); - - const handleScroll = () => { - const el = listRef.current; - if (!el) return; - const atBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 8; - isAtBottomRef.current = atBottom; - if (atBottom) setUnreadCount(0); - }; - - const handleScrollToBottom = () => { - if (!listRef.current) return; - listRef.current.scrollTop = listRef.current.scrollHeight; - isAtBottomRef.current = true; - setUnreadCount(0); - }; - - const handleJoin = () => { - if (roomName.trim() && user.trim()) { - setMessages([]); - setUnreadCount(0); - setJoinedRoom(roomName.trim()); - } - }; - - const handleSend = async () => { - if (!messageText.trim()) return; - setSendValues({ roomName: joinedRoom, user, message: messageText }); - await sendCommand.execute(); - setMessageText(''); - }; - - if (!joinedRoom) { - return ( - <div style={{ maxWidth: 400, margin: '80px auto', display: 'flex', flexDirection: 'column', gap: 12 }}> - <h2>Join a Chat Room</h2> - <input - placeholder="Room name" - value={roomName} - onChange={e => setRoomName(e.target.value)} - /> - <input - placeholder="Your name" - value={user} - onChange={e => setUser(e.target.value)} - /> - <button - onClick={handleJoin} - disabled={!roomName.trim() || !user.trim()} - > - Join - </button> - </div> - ); - } - - return ( - <div style={{ maxWidth: 600, margin: '40px auto', display: 'flex', flexDirection: 'column', gap: 16 }}> - <h2>{joinedRoom}</h2> - <p style={{ color: '#888', margin: 0 }}>Chatting as <strong>{user}</strong></p> - - <div style={{ position: 'relative' }}> - <div - ref={listRef} - onScroll={handleScroll} - style={{ - border: '1px solid #e0e0e0', - borderRadius: 8, - height: 400, - overflowY: 'auto', - padding: 16, - display: 'flex', - flexDirection: 'column', - gap: 8, - }} - > - {messages.length === 0 && ( - <p style={{ color: '#aaa', alignSelf: 'center', marginTop: 'auto', marginBottom: 'auto' }}> - No messages yet. Say hello! - </p> - )} - {messages.map((msg, index) => ( - <div - key={index} - style={{ - background: msg.user === user ? '#e8f4fd' : '#f5f5f5', - borderRadius: 8, - padding: '8px 12px', - alignSelf: msg.user === user ? 'flex-end' : 'flex-start', - maxWidth: '75%', - }} - > - <div style={{ fontSize: 12, color: '#888', marginBottom: 2 }}> - <strong>{msg.user}</strong> - {' · '} - {new Date(msg.sentAt).toLocaleTimeString()} - </div> - <div>{msg.message}</div> - </div> - ))} - </div> - - {unreadCount > 0 && ( - <button - onClick={handleScrollToBottom} - style={{ - position: 'absolute', - bottom: 12, - left: '50%', - transform: 'translateX(-50%)', - background: '#0078d4', - color: '#fff', - border: 'none', - borderRadius: 16, - padding: '6px 16px', - cursor: 'pointer', - fontSize: 13, - }} - > - {unreadCount} new {unreadCount === 1 ? 'message' : 'messages'} ↓ - </button> - )} - </div> - - <div style={{ display: 'flex', gap: 8 }}> - <input - style={{ flex: 1 }} - placeholder="Type a message…" - value={messageText} - onChange={e => setMessageText(e.target.value)} - onKeyDown={e => { if (e.key === 'Enter') handleSend(); }} - /> - <button - onClick={handleSend} - disabled={!messageText.trim()} - > - Send - </button> - </div> - </div> - ); -}; -``` - -### What is happening here? - -**`ForRoom.when(...).useChangeStream({ roomName: joinedRoom })`** — returns the raw `ChangeSet<ChatMessage>` on each push rather than the reconstructed full collection. The component receives `{ added, replaced, removed }` directly. - -**The first `useEffect`** appends `changes.added` to the local `messages` state. On the first subscription the entire room history arrives in `added`, populating the initial list. Every subsequent message arrives as a single item in `added`. The `replaced` and `removed` arrays are always empty for chat messages. - -**`unreadCount`** is incremented when new messages arrive while `isAtBottomRef.current` is `false` — meaning the user has scrolled up. The "new messages" button appears and resets the count when the user scrolls back to the bottom. This behaviour is only possible because `useChangeStream` exposes `added` explicitly. - -**`isAtBottomRef`** uses a `ref` rather than `useState` so that the scroll handler does not trigger re-renders on every scroll event. - -**`setMessages([])`** when joining resets local state. Without this, switching rooms would briefly show the previous room's messages before the new history arrives. - ---- - -## Key Difference from `use()` - -| | `use()` | `useChangeStream()` | -| - | ------- | ------------------- | -| Component receives | Full collection snapshot | `{ added, replaced, removed }` | -| Delta application | Automatic, inside the hook | Manual, in `useEffect` | -| Knowing what changed | Not directly visible | Explicit — `added`, `replaced`, `removed` | -| Typical use case | Render a list | React to specific additions or removals | -| Backend requirement | None — same `ISubject<IEnumerable<T>>` | None — same `ISubject<IEnumerable<T>>` | - -Both hooks subscribe to the same generated query proxy. Switching between them is a one-line change in the component. The backend and the generated proxy are identical in both cases. diff --git a/web/src/content/docs/scenarios/chat/in-memory.md b/web/src/content/docs/scenarios/chat/in-memory.md deleted file mode 100644 index 92a60de2..00000000 --- a/web/src/content/docs/scenarios/chat/in-memory.md +++ /dev/null @@ -1,357 +0,0 @@ ---- -title: Real-Time Chat — In-Memory ---- - -# Real-Time Chat — In-Memory - -This guide builds a real-time chat room backed entirely by an in-memory service. It demonstrates the core observable query pattern: the frontend subscribes once and receives updates as they arrive — no polling, no manual WebSocket setup. - -By the end you will have: - -- A `ChatRoom` class that holds per-room message history in a `BehaviorSubject` -- A `ChatService` singleton that manages named rooms in a `ConcurrentDictionary` -- A `ChatMessage` observable query that delivers the room's current history as the initial payload, then pushes updates in real time -- A `SendMessage` command that posts to a room and triggers a push to all subscribers -- A React page component that joins a room and renders a live chat thread - ---- - -## Folder Structure - -``` -Features/ -└── Chat/ - ├── ChatRoom.cs ← ChatRoom state holder + ChatService singleton - ├── ChatRoomPage.cs ← ChatMessage [ReadModel] + SendMessage [Command] - └── ChatRoomPage.tsx ← React component -``` - ---- - -## Step 1 — ChatRoom and ChatService - -`ChatRoom` owns the per-room state. A `BehaviorSubject` is the right tool here: it always holds the most recently emitted value and emits that value immediately to any new subscriber, which is exactly how each connecting client receives the room's current history. - -`ChatService` is a singleton that creates and tracks rooms by name. - -```csharp -// Features/Chat/ChatRoom.cs -using System.Collections.Concurrent; -using System.Reactive.Subjects; - -namespace MyApp.Chat; - -/// <summary> -/// Holds the message history and live subject for a single chat room. -/// </summary> -public class ChatRoom -{ - readonly List<ChatMessage> _history = []; - readonly BehaviorSubject<IEnumerable<ChatMessage>> _messages; - - /// <summary> - /// Initializes a new instance of the <see cref="ChatRoom"/> class. - /// </summary> - public ChatRoom() - { - // To pre-populate with persisted history, load it here before constructing - // the subject — see the RabbitMQ guide for a persistence-backed variant. - _messages = new BehaviorSubject<IEnumerable<ChatMessage>>([]); - } - - /// <summary> - /// Gets the reactive subject that always holds the full current message list. - /// New subscribers receive the current history immediately via BehaviorSubject semantics. - /// </summary> - public BehaviorSubject<IEnumerable<ChatMessage>> Messages => _messages; - - /// <summary> - /// Appends a message and pushes the updated history to all subscribers. - /// </summary> - /// <param name="user">The display name of the sender.</param> - /// <param name="message">The message text.</param> - public void Send(string user, string message) - { - var msg = new ChatMessage(user, DateTimeOffset.UtcNow, message); - _history.Add(msg); - _messages.OnNext([.._history]); - } -} - -/// <summary> -/// Singleton that creates and tracks chat rooms by name. -/// </summary> -public class ChatService -{ - readonly ConcurrentDictionary<string, ChatRoom> _rooms = new(); - - /// <summary> - /// Gets or creates the <see cref="ChatRoom"/> with the given name. - /// </summary> - /// <param name="name">The room name.</param> - /// <returns>The existing or newly created room.</returns> - public ChatRoom GetChatRoom(string name) => - _rooms.GetOrAdd(name, _ => new ChatRoom()); -} -``` - -### What is happening here? - -**`BehaviorSubject<IEnumerable<ChatMessage>>`** is a reactive subject with two properties that make it ideal for this use case: -- It always holds the most recently emitted value — the full accumulated history — so it acts as both the live stream and the current-state store. -- It immediately emits that value to any new subscriber. A client joining mid-conversation receives all past messages in the first push, with no separate history call. - -**`Send()`** appends the message to `_history`, then calls `OnNext()` with a snapshot of the complete list. Sending the full list on each update keeps backend code straightforward. Arc handles network efficiency automatically via delta mode (see [Step 3](#step-3--delta-mode)). - -**`ConcurrentDictionary`** makes `GetChatRoom()` safe under concurrent access. If two clients join the same room simultaneously, only one `ChatRoom` is created. - ---- - -## Step 2 — The Read Model and Command - -```csharp -// Features/Chat/ChatRoomPage.cs -using Cratis.Arc.Commands.ModelBound; -using Cratis.Arc.Queries.ModelBound; -using System.Reactive.Subjects; - -namespace MyApp.Chat; - -// ─── Read Model ─────────────────────────────────────────────────────────────── - -/// <summary> -/// Represents a single chat message. -/// </summary> -/// <param name="User">The display name of the sender.</param> -/// <param name="SentAt">The UTC time the message was sent.</param> -/// <param name="Message">The message text.</param> -[ReadModel] -public record ChatMessage(string User, DateTimeOffset SentAt, string Message) -{ - /// <summary> - /// Observes the live message feed for the given room. - /// The initial emission contains the room's complete current history. - /// Every call to <see cref="SendMessage.Handle"/> triggers a new emission. - /// </summary> - /// <param name="roomName">The name of the room to observe.</param> - /// <param name="chatService">The chat service, injected by the framework.</param> - /// <returns>An observable that emits the full message list on each change.</returns> - public static ISubject<IEnumerable<ChatMessage>> ForRoom( - string roomName, - ChatService chatService) - { - var room = chatService.GetChatRoom(roomName); - // BehaviorSubject emits its current value to each new subscriber, - // so subscribing here immediately seeds the relay with the room's history. - var relay = new BehaviorSubject<IEnumerable<ChatMessage>>(room.Messages.Value); - room.Messages.Subscribe(relay); - return relay; - } -} - -// ─── Command ────────────────────────────────────────────────────────────────── - -/// <summary> -/// Sends a chat message to a room. -/// </summary> -/// <param name="RoomName">The name of the room to post to.</param> -/// <param name="User">The display name of the sender.</param> -/// <param name="Message">The message text.</param> -[Command] -public record SendMessage(string RoomName, string User, string Message) -{ - /// <summary> - /// Posts the message to the room, which pushes the updated history - /// to all subscribers of <see cref="ChatMessage.ForRoom"/>. - /// </summary> - /// <param name="chatService">The chat service, injected by the framework.</param> - public void Handle(ChatService chatService) => - chatService.GetChatRoom(RoomName).Send(User, Message); -} -``` - -### What is happening here? - -**`ForRoom(string roomName, ChatService chatService)`** — the framework distinguishes the two parameters automatically: `roomName` is a query parameter from the HTTP request; `ChatService` is resolved from the DI container. - -The method creates a **relay** `BehaviorSubject` initialised with `room.Messages.Value` — the `BehaviorSubject`'s current value, which is the room's full history at the moment of connection. Subscribing to `room.Messages` then forwards every future `OnNext()` call to the relay. Each client gets its own relay instance — independent subscriptions that all start with the same snapshot. - -**`Handle()`** on `SendMessage` delegates to `ChatRoom.Send()`. Because `Send()` calls `_messages.OnNext()`, every active relay fires, pushing the updated list to every client subscribed to `ForRoom` for that room. - -> **Register `ChatService` as a singleton** in your `Program.cs`: -> -> ```csharp -> builder.Services.AddSingleton<ChatService>(); -> ``` -> -> **Run `dotnet build`** after saving these files. The [Arc proxy generator](/arc/backend/proxy-generation/) produces: -> - `ChatMessage.ts` — the TypeScript model type -> - `ForRoom.ts` — the observable query proxy with `use()` and `when()` hooks -> - `SendMessage.ts` — the command proxy with a `use()` hook - ---- - -## Step 3 — Delta Mode - -Arc observable queries use **delta mode by default**. Understanding this helps you reason about what crosses the network and how to get the most from it. - -**What happens on each emission:** - -| Emission | What is sent | -| -------- | ------------ | -| First | The complete collection — the room's full history as the initial payload | -| Subsequent | A [`ChangeSet`](/arc/backend/queries/change-stream/) with only the `added`, `replaced`, and `removed` arrays | - -The `use()` hook applies each `ChangeSet` transparently. `messagesResult.data` always holds the full current collection — the React component never sees raw deltas. - -**How Arc computes the ChangeSet.** The server compares successive `OnNext()` emissions. If the item type has an `id` property (case-insensitive), Arc uses identity-based comparison and can detect additions, replacements, and removals independently. Without an `id` property, Arc falls back to JSON-hash comparison, which can only detect additions and removals. - -`ChatMessage` has no `id` property, so Arc uses JSON-hash. Since chat messages are immutable — never edited after being sent — only `added` events occur, which JSON-hash handles correctly. For large histories, adding a `ChatMessageId` concept improves efficiency by letting Arc skip the full JSON comparison on unchanged items. - -> **Full mode.** To send the complete collection on every emission (useful during debugging), set `observableQueryTransferMode={ObservableQueryTransferMode.Full}` on the `<Arc>` provider. Delta mode is the default and is recommended for production. - ---- - -## Step 4 — The React Component - -```tsx -// Features/Chat/ChatRoomPage.tsx -import { useState } from 'react'; -import { ForRoom } from './ForRoom'; -import { SendMessage } from './SendMessage'; -import type { ChatMessage } from './ChatMessage'; - -export const ChatRoomPage = () => { - const [roomName, setRoomName] = useState(''); - const [joinedRoom, setJoinedRoom] = useState(''); - const [user, setUser] = useState(''); - const [messageText, setMessageText] = useState(''); - - const [messagesResult] = ForRoom - .when(joinedRoom.length > 0) - .use({ roomName: joinedRoom }); - - const [sendCommand, setSendValues] = SendMessage.use(); - - const handleJoin = () => { - if (roomName.trim() && user.trim()) { - setJoinedRoom(roomName.trim()); - } - }; - - const handleSend = async () => { - if (!messageText.trim()) return; - setSendValues({ roomName: joinedRoom, user, message: messageText }); - await sendCommand.execute(); - setMessageText(''); - }; - - if (!joinedRoom) { - return ( - <div style={{ maxWidth: 400, margin: '80px auto', display: 'flex', flexDirection: 'column', gap: 12 }}> - <h2>Join a Chat Room</h2> - <input - placeholder="Room name" - value={roomName} - onChange={e => setRoomName(e.target.value)} - /> - <input - placeholder="Your name" - value={user} - onChange={e => setUser(e.target.value)} - /> - <button - onClick={handleJoin} - disabled={!roomName.trim() || !user.trim()} - > - Join - </button> - </div> - ); - } - - const messages: ChatMessage[] = messagesResult.data ?? []; - - return ( - <div style={{ maxWidth: 600, margin: '40px auto', display: 'flex', flexDirection: 'column', gap: 16 }}> - <h2>{joinedRoom}</h2> - <p style={{ color: '#888', margin: 0 }}>Chatting as <strong>{user}</strong></p> - - <div style={{ - border: '1px solid #e0e0e0', - borderRadius: 8, - height: 400, - overflowY: 'auto', - padding: 16, - display: 'flex', - flexDirection: 'column', - gap: 8, - }}> - {messages.length === 0 && ( - <p style={{ color: '#aaa', alignSelf: 'center', marginTop: 'auto', marginBottom: 'auto' }}> - No messages yet. Say hello! - </p> - )} - {messages.map((msg, index) => ( - <div - key={index} - style={{ - background: msg.user === user ? '#e8f4fd' : '#f5f5f5', - borderRadius: 8, - padding: '8px 12px', - alignSelf: msg.user === user ? 'flex-end' : 'flex-start', - maxWidth: '75%', - }} - > - <div style={{ fontSize: 12, color: '#888', marginBottom: 2 }}> - <strong>{msg.user}</strong> - {' · '} - {new Date(msg.sentAt).toLocaleTimeString()} - </div> - <div>{msg.message}</div> - </div> - ))} - </div> - - <div style={{ display: 'flex', gap: 8 }}> - <input - style={{ flex: 1 }} - placeholder="Type a message…" - value={messageText} - onChange={e => setMessageText(e.target.value)} - onKeyDown={e => { if (e.key === 'Enter') handleSend(); }} - /> - <button - onClick={handleSend} - disabled={!messageText.trim()} - > - Send - </button> - </div> - </div> - ); -}; -``` - -### What is happening here? - -**`ForRoom.when(joinedRoom.length > 0).use({ roomName: joinedRoom })`** — `.when(condition)` prevents the subscription from opening until the user has joined a room. Once `joinedRoom` is set, Arc opens an SSE connection. The component immediately receives the room's full history as the first push. Every subsequent `SendMessage` command — from any user in that room — triggers a new push, and `messagesResult.data` updates automatically. - -**`setSendValues` then `execute()`** — `setSendValues` updates the command object with the room name, user name, and message text. `sendCommand.execute()` sends the HTTP POST. On the server, `Handle()` calls `ChatRoom.Send()`, which calls `_messages.OnNext()`, which fires every relay subscription, which pushes the updated list to every subscriber — including this browser. - -**Messages are keyed by array index** because `ChatMessage` has no unique identifier. In production, add a `ChatMessageId` concept to enable stable keys and identity-based delta computation. - ---- - -## Summary - -| Piece | What it does | -| ----- | ------------ | -| `ChatRoom` | Holds history in `_history`; `BehaviorSubject` emits the full list on every `Send()` | -| `ChatService` | Singleton — owns the `ConcurrentDictionary<string, ChatRoom>` | -| `ChatMessage.ForRoom()` | Creates a per-client relay seeded from `room.Messages.Value`; forwards all future emissions | -| `SendMessage.Handle()` | Calls `ChatRoom.Send()`, which triggers `OnNext()` to all active relays | -| Delta mode | First push = full history; subsequent pushes = `ChangeSet` (only new messages over the wire) | -| `ForRoom.ts` (generated) | TypeScript observable query proxy with `use()` and `when()` hooks | -| `SendMessage.ts` (generated) | TypeScript command proxy with a `use()` hook | -| `ChatRoomPage.tsx` | React component — subscribes on join, renders `messagesResult.data` | diff --git a/web/src/content/docs/scenarios/chat/incremental-pushes.md b/web/src/content/docs/scenarios/chat/incremental-pushes.md deleted file mode 100644 index 1c34028f..00000000 --- a/web/src/content/docs/scenarios/chat/incremental-pushes.md +++ /dev/null @@ -1,386 +0,0 @@ ---- -title: Real-Time Chat — Incremental Pushes ---- - -# Real-Time Chat — Incremental Pushes - -The three previous guides all emit the **full message history** on every `OnNext()` call. Arc's delta mode compresses this down to a `ChangeSet` over the wire, but the backend still accumulates and sends a growing list. - -This guide flips the model. The backend emits only what is **new** on each push. The first emission is the full history (initial payload); every subsequent emission contains only the messages that just arrived. The frontend accumulates them into its own local state. - -The result is a constant-size network payload per message regardless of how long the conversation has been running. - -By the end you will have: - -- A `ChatRoom` with a plain `Subject` — no history, no accumulated state, just a pub/sub channel -- A `ChatService` that tracks history separately and exposes a `Send()` method -- A `ForRoom` query that emits history once, then forwards only new messages via a `ReplaySubject` -- A React component that uses `use()` and a `useEffect` accumulator — **not** `useChangeStream()` - ---- - -## How This Differs from the Other Guides - -| | In-Memory / RabbitMQ | Frontend-Managed State | This guide | -| - | -------------------- | ---------------------- | ---------- | -| Each `OnNext()` emits | Full history list | Full history list | New message(s) only | -| History lives in | `ChatRoom` (`BehaviorSubject`) | `ChatRoom` (`BehaviorSubject`) | `ChatService` | -| Relay type | `BehaviorSubject` | `BehaviorSubject` | `ReplaySubject(1)` | -| Network per message | Grows with history | Grows with history | Constant | -| Frontend hook | `use()` | `useChangeStream()` | `use()` | -| Component accumulates | No — renders `data` directly | Yes — appends `added` | Yes — appends `data` | - ---- - -## Folder Structure - -``` -Features/ -└── Chat/ - ├── ChatRoom.cs ← ChatRoom (Subject only) + ChatService (history + send) - ├── ChatRoomPage.cs ← ChatMessage [ReadModel] + SendMessage [Command] - └── ChatRoomPage.tsx ← React component -``` - ---- - -## Step 1 — ChatRoom and ChatService - -`ChatRoom` is now a pure pub/sub channel. It holds no state and tracks no history. A plain `Subject<IEnumerable<ChatMessage>>` emits only when `Deliver()` is called. - -History tracking moves to `ChatService`, which also becomes the entry point for sending messages so that it can record each message before firing the room's subject. - -```csharp -// Features/Chat/ChatRoom.cs -using System.Collections.Concurrent; -using System.Reactive.Subjects; - -namespace MyApp.Chat; - -/// <summary> -/// A pure pub/sub channel for a single chat room. -/// Holds no history — delivers only the messages passed to <see cref="Deliver"/>. -/// </summary> -public class ChatRoom -{ - readonly Subject<IEnumerable<ChatMessage>> _messages = new(); - - /// <summary> - /// Gets the subject that emits each incoming delivery. - /// Each emission contains only the messages passed to <see cref="Deliver"/> in that call. - /// </summary> - public ISubject<IEnumerable<ChatMessage>> Messages => _messages; - - /// <summary> - /// Delivers a message to all subscribers. - /// </summary> - /// <param name="message">The message to deliver.</param> - internal void Deliver(ChatMessage message) => _messages.OnNext([message]); -} - -/// <summary> -/// Singleton that manages chat rooms and owns the per-room message history. -/// </summary> -public class ChatService -{ - readonly ConcurrentDictionary<string, ChatRoom> _rooms = new(); - readonly ConcurrentDictionary<string, List<ChatMessage>> _history = new(); - readonly object _lock = new(); - - /// <summary> - /// Gets or creates the <see cref="ChatRoom"/> for the given name. - /// </summary> - /// <param name="name">The room name.</param> - /// <returns>The pub/sub channel for the room.</returns> - public ChatRoom GetChatRoom(string name) => - _rooms.GetOrAdd(name, _ => new ChatRoom()); - - /// <summary> - /// Gets the full message history for the given room, oldest first. - /// </summary> - /// <param name="name">The room name.</param> - /// <returns>All messages posted so far.</returns> - public IEnumerable<ChatMessage> GetHistory(string name) => - _history.TryGetValue(name, out var msgs) ? msgs.AsReadOnly() : []; - - /// <summary> - /// Records a new message in the history and delivers it to the room's subscribers. - /// </summary> - /// <param name="name">The room name.</param> - /// <param name="user">The display name of the sender.</param> - /// <param name="message">The message text.</param> - public void Send(string name, string user, string message) - { - var msg = new ChatMessage(user, DateTimeOffset.UtcNow, message); - lock (_lock) - { - _history.GetOrAdd(name, _ => new List<ChatMessage>()).Add(msg); - } - GetChatRoom(name).Deliver(msg); - } -} -``` - -### What is happening here? - -**Plain `Subject<IEnumerable<ChatMessage>>`** only delivers values to subscribers that are currently active. Unlike a `BehaviorSubject`, it holds no current value and emits nothing to late subscribers. This is deliberate — history is the responsibility of `ChatService`, not the room. - -**`ChatService.Send()`** records the message in `_history` under a lock before delivering it to the room. The lock protects the per-room `List<ChatMessage>` from concurrent appends while remaining uncontested in typical usage. The message is added to history before the pub/sub delivery so that any concurrent `GetHistory()` call (e.g. a second client joining the room at the same moment) sees the new message in the initial payload. - ---- - -## Step 2 — The Read Model and Command - -```csharp -// Features/Chat/ChatRoomPage.cs -using Cratis.Arc.Commands.ModelBound; -using Cratis.Arc.Queries.ModelBound; -using System.Reactive.Subjects; - -namespace MyApp.Chat; - -// ─── Read Model ─────────────────────────────────────────────────────────────── - -/// <summary> -/// Represents a single chat message. -/// </summary> -/// <param name="User">The display name of the sender.</param> -/// <param name="SentAt">The UTC time the message was sent.</param> -/// <param name="Message">The message text.</param> -[ReadModel] -public record ChatMessage(string User, DateTimeOffset SentAt, string Message) -{ - /// <summary> - /// Observes the message feed for the given room. - /// The first emission contains the room's complete history. - /// Each subsequent emission contains only the new message(s) that just arrived. - /// </summary> - /// <param name="roomName">The name of the room to observe.</param> - /// <param name="chatService">The chat service, injected by the framework.</param> - /// <returns>An observable that emits history once, then individual new messages.</returns> - public static ISubject<IEnumerable<ChatMessage>> ForRoom( - string roomName, - ChatService chatService) - { - // ReplaySubject(1) stores the last emitted value and replays it to - // any new subscriber — including Arc's subscription which occurs after - // this method returns. - var relay = new ReplaySubject<IEnumerable<ChatMessage>>(1); - - // First payload: the full history. - relay.OnNext(chatService.GetHistory(roomName)); - - // Subsequent payloads: whatever ChatRoom.Deliver() fires — one message at a time. - chatService.GetChatRoom(roomName).Messages.Subscribe(relay); - - return relay; - } -} - -// ─── Command ────────────────────────────────────────────────────────────────── - -/// <summary> -/// Sends a chat message to a room. -/// </summary> -/// <param name="RoomName">The name of the room to post to.</param> -/// <param name="User">The display name of the sender.</param> -/// <param name="Message">The message text.</param> -[Command] -public record SendMessage(string RoomName, string User, string Message) -{ - /// <summary> - /// Records the message and delivers it to all subscribers. - /// </summary> - /// <param name="chatService">The chat service, injected by the framework.</param> - public void Handle(ChatService chatService) => - chatService.Send(RoomName, User, Message); -} -``` - -### What is happening here? - -**`ReplaySubject<IEnumerable<ChatMessage>>(1)`** is the right relay here for a specific reason. The method calls `OnNext(history)` and then subscribes to the room — but Arc subscribes to the returned relay *after* the method returns. A plain `Subject` would have already fired and lost the history emission by the time Arc subscribes. `ReplaySubject(1)` stores the last emitted value and replays it to each new subscriber immediately upon subscription, so Arc always receives the history as its first message. - -**Two emissions, two sources:** - -| Emission | Source | Content | -| -------- | ------ | ------- | -| First | `relay.OnNext(chatService.GetHistory(roomName))` | All persisted history | -| Subsequent | `chatService.GetChatRoom(roomName).Messages` → relay | One new `ChatMessage` per send | - -The `Subject` in `ChatRoom` fires once per `Deliver()` call with a single-element collection. The relay forwards each of these to Arc as a separate push. - -> **Register `ChatService` as a singleton** in your `Program.cs`: -> -> ```csharp -> builder.Services.AddSingleton<ChatService>(); -> ``` -> -> **Run `dotnet build`** after saving. The proxy generator produces `ForRoom.ts`, `SendMessage.ts`, and `ChatMessage.ts` — identical in shape to the other chat guides. - ---- - -## Step 3 — What the Frontend Receives - -With the backend emitting incremental payloads, this is what the frontend sees in Arc's delta mode: - -| Push | Backend emits | Arc ChangeSet sent | `messagesResult.data` | -| ---- | ------------- | ------------------ | --------------------- | -| 1st — history | `[msg1, msg2, msg3]` | `added: [msg1, msg2, msg3]` | `[msg1, msg2, msg3]` | -| 2nd — new msg | `[msg4]` | `removed: [msg1,msg2,msg3]`, `added: [msg4]` | `[msg4]` | -| 3rd — new msg | `[msg5]` | `removed: [msg4]`, `added: [msg5]` | `[msg5]` | - -Arc's ChangeSet computation compares successive emissions — it sees the previous full history disappear and a single new message appear. This looks odd internally, but `messagesResult.data` from `use()` accurately reflects what the backend emitted: the history on the first push, and only the new message on every subsequent push. - -This is why the frontend must **not** use `useChangeStream()` here. `useChangeStream()` would expose the `removed` side of the ChangeSet, making it appear that history was deleted on every new message. `use()` abstracts that away and gives the component the clean per-emission `data`. - ---- - -## Step 4 — The React Component - -```tsx -// Features/Chat/ChatRoomPage.tsx -import { useState, useEffect } from 'react'; -import { ForRoom } from './ForRoom'; -import { SendMessage } from './SendMessage'; -import type { ChatMessage } from './ChatMessage'; - -export const ChatRoomPage = () => { - const [roomName, setRoomName] = useState(''); - const [joinedRoom, setJoinedRoom] = useState(''); - const [user, setUser] = useState(''); - const [messageText, setMessageText] = useState(''); - const [messages, setMessages] = useState<ChatMessage[]>([]); - - const [messagesResult] = ForRoom - .when(joinedRoom.length > 0) - .use({ roomName: joinedRoom }); - - const [sendCommand, setSendValues] = SendMessage.use(); - - // Each push from the server contains either the full history (first push) - // or a single new message. Append it to local state in both cases. - useEffect(() => { - if (!messagesResult.data?.length) return; - setMessages(prev => [...prev, ...messagesResult.data!]); - }, [messagesResult.data]); - - const handleJoin = () => { - if (!roomName.trim() || !user.trim()) return; - setMessages([]); - setJoinedRoom(roomName.trim()); - }; - - const handleSend = async () => { - if (!messageText.trim()) return; - setSendValues({ roomName: joinedRoom, user, message: messageText }); - await sendCommand.execute(); - setMessageText(''); - }; - - if (!joinedRoom) { - return ( - <div style={{ maxWidth: 400, margin: '80px auto', display: 'flex', flexDirection: 'column', gap: 12 }}> - <h2>Join a Chat Room</h2> - <input - placeholder="Room name" - value={roomName} - onChange={e => setRoomName(e.target.value)} - /> - <input - placeholder="Your name" - value={user} - onChange={e => setUser(e.target.value)} - /> - <button - onClick={handleJoin} - disabled={!roomName.trim() || !user.trim()} - > - Join - </button> - </div> - ); - } - - return ( - <div style={{ maxWidth: 600, margin: '40px auto', display: 'flex', flexDirection: 'column', gap: 16 }}> - <h2>{joinedRoom}</h2> - <p style={{ color: '#888', margin: 0 }}>Chatting as <strong>{user}</strong></p> - - <div style={{ - border: '1px solid #e0e0e0', - borderRadius: 8, - height: 400, - overflowY: 'auto', - padding: 16, - display: 'flex', - flexDirection: 'column', - gap: 8, - }}> - {messages.length === 0 && ( - <p style={{ color: '#aaa', alignSelf: 'center', marginTop: 'auto', marginBottom: 'auto' }}> - No messages yet. Say hello! - </p> - )} - {messages.map((msg, index) => ( - <div - key={index} - style={{ - background: msg.user === user ? '#e8f4fd' : '#f5f5f5', - borderRadius: 8, - padding: '8px 12px', - alignSelf: msg.user === user ? 'flex-end' : 'flex-start', - maxWidth: '75%', - }} - > - <div style={{ fontSize: 12, color: '#888', marginBottom: 2 }}> - <strong>{msg.user}</strong> - {' · '} - {new Date(msg.sentAt).toLocaleTimeString()} - </div> - <div>{msg.message}</div> - </div> - ))} - </div> - - <div style={{ display: 'flex', gap: 8 }}> - <input - style={{ flex: 1 }} - placeholder="Type a message…" - value={messageText} - onChange={e => setMessageText(e.target.value)} - onKeyDown={e => { if (e.key === 'Enter') handleSend(); }} - /> - <button - onClick={handleSend} - disabled={!messageText.trim()} - > - Send - </button> - </div> - </div> - ); -}; -``` - -### What is happening here? - -**`useEffect` on `messagesResult.data`** — each time the server pushes a new value, `messagesResult.data` is a new array reference, triggering the effect. On the first push it contains the full history; on each subsequent push it contains one new message. Appending via `setMessages(prev => [...prev, ...data])` works correctly in both cases. - -**`setMessages([])` on join** — clears local state before changing rooms. Without this, the previous room's messages would remain visible for a moment after joining. - -**`use()` not `useChangeStream()`** — as explained in [Step 3](#step-3--what-the-frontend-receives), `useChangeStream()` would expose the Arc-internal ChangeSet where previous messages appear as `removed` on each new push, which is the wrong mental model for this pattern. - ---- - -## Summary - -| Piece | What it does | -| ----- | ------------ | -| `ChatRoom` | Pure pub/sub channel — `Subject<IEnumerable<ChatMessage>>`, no state | -| `ChatService` | Owns history per room; `Send()` records then delivers | -| `ChatMessage.ForRoom()` | `ReplaySubject(1)` — emits history once, then forwards single-message deliveries | -| `SendMessage.Handle()` | Delegates to `chatService.Send()` | -| Network per message | Constant — one `ChatMessage` per push after the initial history | -| Frontend hook | `use()` — `data` reflects each backend emission directly | -| Component state | Accumulated via `useEffect` — never replaced, only appended | diff --git a/web/src/content/docs/scenarios/chat/index.md b/web/src/content/docs/scenarios/chat/index.md deleted file mode 100644 index 9ec7830f..00000000 --- a/web/src/content/docs/scenarios/chat/index.md +++ /dev/null @@ -1,65 +0,0 @@ ---- -title: Real-Time Chat -description: These guides build a real-time multi-room chat application using Arc's observable queries. ---- - -# Real-Time Chat - -These guides build a real-time multi-room chat application using Arc's [observable queries](/arc/backend/queries/). They share a common shape — a `ChatMessage` read model with a `ForRoom` observable query, a `SendMessage` command, and a React page component — but each one explores a different dimension of the pattern. - ---- - -## The Four Guides - -### [In-Memory](./in-memory) - -The simplest starting point. Message history is held in a `BehaviorSubject` inside a singleton `ChatService`. No external dependencies required. - -Covers: -- `BehaviorSubject` as the backing store for live chat state -- The relay pattern for per-client observable subscriptions -- How Arc's **delta mode** works: the first emission delivers the full history; subsequent emissions deliver only the `ChangeSet` of new messages - -### [With RabbitMQ](./rabbitmq) - -Replaces the in-process state with two external systems: a persistence layer that loads message history on startup, and a RabbitMQ fanout exchange that delivers new messages to every server instance. The observable query and the React component are identical to the in-memory version. - -Covers: -- Loading initial history from a persistence layer -- A `BackgroundService` that consumes from RabbitMQ and routes messages to the correct `ChatRoom` -- Publishing from `SendMessage` rather than writing directly to the room -- Scaling across multiple server instances - -### [Frontend-Managed State](./change-stream) - -The backend is unchanged from the in-memory guide. The React component switches from `ForRoom.use()` to `ForRoom.useChangeStream()` to receive the raw `ChangeSet` — `{ added, replaced, removed }` — and manages its own `useState` accumulator. - -Covers: -- When to use `useChangeStream()` instead of `use()` -- Appending `ChangeSet.added` items to local state -- Deriving secondary state from the delta: scroll-to-bottom logic and an unread message counter - -### [Incremental Pushes](./incremental-pushes) - -The backend changes fundamentally. `ChatRoom` becomes a pure pub/sub channel with no history — a plain `Subject` that fires only new messages. `ChatService` owns the history. The `ForRoom` query uses a `ReplaySubject(1)` to emit the full history once as the initial payload, then forwards each new message individually. The network payload per message stays constant regardless of conversation length. - -Covers: -- Separating pub/sub (`ChatRoom`) from history (`ChatService`) -- Why `ReplaySubject(1)` is needed when the first payload is emitted before Arc subscribes -- Why `use()` — not `useChangeStream()` — is correct when the backend sends incremental payloads -- A `useEffect` accumulator that appends both the initial history and each new arrival - ---- - -## What All Four Share - -The `ISubject<IEnumerable<ChatMessage>>` return type on `ForRoom` is the contract between the query method and the Arc framework. It does not change regardless of how the backend sources or stages its data. The generated TypeScript proxy is identical across all four guides. - -| | In-Memory | RabbitMQ | Frontend State | Incremental Pushes | -| - | --------- | -------- | -------------- | ------------------ | -| Backend emits | Full history | Full history | Full history | History once, then single messages | -| History lives in | `ChatRoom` | `ChatRoom` | `ChatRoom` | `ChatService` | -| Relay type | `BehaviorSubject` | `BehaviorSubject` | `BehaviorSubject` | `ReplaySubject(1)` | -| Network per message | Grows | Grows | Grows | Constant | -| React hook | `use()` | `use()` | `useChangeStream()` | `use()` | -| Component accumulates | No | No | Yes — via `added` | Yes — via `data` | diff --git a/web/src/content/docs/scenarios/chat/rabbitmq.md b/web/src/content/docs/scenarios/chat/rabbitmq.md deleted file mode 100644 index a1189b59..00000000 --- a/web/src/content/docs/scenarios/chat/rabbitmq.md +++ /dev/null @@ -1,364 +0,0 @@ ---- -title: Real-Time Chat — With RabbitMQ ---- - -# Real-Time Chat — With RabbitMQ - -This guide extends the chat pattern from the [in-memory guide](../in-memory) by replacing the in-process state with two external systems: a **persistence layer** that loads message history on startup, and **RabbitMQ** that delivers new messages to all connected server instances in real time. - -The observable query and the React component are unchanged from the in-memory version. All the differences are in how the backend populates and pushes the `BehaviorSubject`. - -By the end you will have: - -- A `ChatRoom` that loads its initial history from a persistence layer -- A `ChatSubscriber` background service that consumes messages from a RabbitMQ exchange and routes them to the appropriate room -- A `SendMessage` command that publishes to RabbitMQ rather than writing directly to the room -- The same observable query and React component from the in-memory guide, unchanged - ---- - -## Architecture - -```mermaid -sequenceDiagram - participant Browser - participant Server as Server (ForRoom) - participant Room as ChatRoom - participant MQ as RabbitMQ exchange - participant Sub as ChatSubscriber - - Browser->>Server: subscribe (SSE) - Server->>Room: GetChatRoom(roomName) - Room-->>Server: Messages.Value (persisted history) - Server-->>Browser: initial payload — full history - - Browser->>Server: SendMessage (HTTP POST) - Server->>MQ: BasicPublish(envelope) - MQ->>Sub: deliver message - Sub->>Room: Receive(message) - Room->>Room: _history.Add · _messages.OnNext - Room-->>Server: relay fires - Server-->>Browser: ChangeSet push (SSE) -``` - -`SendMessage` publishes to RabbitMQ and returns. The `ChatSubscriber` background service, running on every server instance, receives the message from the queue and routes it to the correct `ChatRoom`. Because `ChatRoom` updates its `BehaviorSubject`, every active relay subscription fires and pushes the updated history to connected clients. - ---- - -## Folder Structure - -``` -Features/ -└── Chat/ - ├── ChatRoom.cs ← ChatRoom + ChatService (persistence-backed) - ├── ChatSubscriber.cs ← BackgroundService — RabbitMQ consumer - ├── IChatPersistence.cs ← Persistence abstraction - ├── ChatRoomPage.cs ← ChatMessage [ReadModel] + SendMessage [Command] - └── ChatRoomPage.tsx ← React component (unchanged from in-memory guide) -``` - ---- - -## Step 1 — Persistence Abstraction - -Define the persistence contract. The concrete implementation depends on your storage choice (SQL, MongoDB, Chronicle read model — any will do). - -```csharp -// Features/Chat/IChatPersistence.cs -namespace MyApp.Chat; - -/// <summary> -/// Loads and saves chat message history. -/// </summary> -public interface IChatPersistence -{ - /// <summary> - /// Loads all messages for the given room, ordered oldest first. - /// </summary> - /// <param name="roomName">The room to load history for.</param> - /// <returns>The persisted message history.</returns> - Task<IEnumerable<ChatMessage>> GetHistoryAsync(string roomName); -} -``` - ---- - -## Step 2 — ChatRoom and ChatService - -`ChatRoom` is initialized with a pre-loaded history and exposes a `Receive()` method that the `ChatSubscriber` calls when a new message arrives from RabbitMQ. - -```csharp -// Features/Chat/ChatRoom.cs -using System.Collections.Concurrent; -using System.Reactive.Subjects; - -namespace MyApp.Chat; - -/// <summary> -/// Holds the message history and live subject for a single chat room, -/// pre-populated from the persistence layer on creation. -/// </summary> -public class ChatRoom -{ - readonly List<ChatMessage> _history; - readonly BehaviorSubject<IEnumerable<ChatMessage>> _messages; - - /// <summary> - /// Initializes a new instance of the <see cref="ChatRoom"/> class - /// with history loaded from the persistence layer. - /// </summary> - /// <param name="history">The persisted message history, oldest first.</param> - public ChatRoom(IEnumerable<ChatMessage> history) - { - _history = history.ToList(); - _messages = new BehaviorSubject<IEnumerable<ChatMessage>>(_history); - } - - /// <summary> - /// Gets the reactive subject that always holds the full current message list. - /// New subscribers receive the current history immediately via BehaviorSubject semantics. - /// </summary> - public BehaviorSubject<IEnumerable<ChatMessage>> Messages => _messages; - - /// <summary> - /// Appends an incoming message and pushes the updated history to all subscribers. - /// Called by <see cref="ChatSubscriber"/> when a message arrives from RabbitMQ. - /// </summary> - /// <param name="message">The received message.</param> - public void Receive(ChatMessage message) - { - _history.Add(message); - _messages.OnNext([.._history]); - } -} - -/// <summary> -/// Singleton that creates and tracks chat rooms, loading history from the persistence layer -/// on first access. -/// </summary> -public class ChatService(IChatPersistence persistence) -{ - readonly ConcurrentDictionary<string, ChatRoom> _rooms = new(); - - /// <summary> - /// Gets or creates the <see cref="ChatRoom"/> with the given name. - /// History is loaded from the persistence layer on first access. - /// </summary> - /// <param name="name">The room name.</param> - /// <returns>The existing or newly created room.</returns> - public ChatRoom GetChatRoom(string name) => - _rooms.GetOrAdd(name, roomName => - { - // Block on first creation only. For production, consider pre-warming - // rooms on application startup via a hosted service. - var history = persistence.GetHistoryAsync(roomName).GetAwaiter().GetResult(); - return new ChatRoom(history); - }); -} -``` - ---- - -## Step 3 — The ChatSubscriber Background Service - -`ChatSubscriber` runs for the lifetime of the application. It declares a transient queue bound to the `chat.messages` fanout exchange, consumes every published message, and routes it to the correct `ChatRoom`. - -Using a fanout exchange with an exclusive, auto-delete queue means every server instance receives every message — the right behaviour for a live chat system where clients may be connected to any instance. - -```csharp -// Features/Chat/ChatSubscriber.cs -using System.Text.Json; -using Microsoft.Extensions.Hosting; -using RabbitMQ.Client; -using RabbitMQ.Client.Events; - -namespace MyApp.Chat; - -/// <summary> -/// Background service that consumes chat messages from RabbitMQ and routes them -/// to the appropriate <see cref="ChatRoom"/>. -/// </summary> -public class ChatSubscriber( - IConnectionFactory connectionFactory, - ChatService chatService) : BackgroundService -{ - /// <inheritdoc/> - protected override async Task ExecuteAsync(CancellationToken stoppingToken) - { - await using var connection = await connectionFactory.CreateConnectionAsync(stoppingToken); - await using var channel = await connection.CreateChannelAsync(cancellationToken: stoppingToken); - - // Fanout exchange — every bound queue receives every published message. - await channel.ExchangeDeclareAsync( - exchange: "chat.messages", - type: ExchangeType.Fanout, - durable: true, - cancellationToken: stoppingToken); - - // Exclusive, auto-delete queue — scoped to this server instance. - var queue = await channel.QueueDeclareAsync( - exclusive: true, - autoDelete: true, - cancellationToken: stoppingToken); - - await channel.QueueBindAsync( - queue: queue.QueueName, - exchange: "chat.messages", - routingKey: string.Empty, - cancellationToken: stoppingToken); - - var consumer = new AsyncEventingBasicConsumer(channel); - consumer.ReceivedAsync += async (_, ea) => - { - var envelope = JsonSerializer.Deserialize<ChatMessageEnvelope>(ea.Body.ToArray())!; - var message = new ChatMessage(envelope.User, envelope.SentAt, envelope.Message); - chatService.GetChatRoom(envelope.RoomName).Receive(message); - await channel.BasicAckAsync(ea.DeliveryTag, multiple: false, cancellationToken: stoppingToken); - }; - - await channel.BasicConsumeAsync( - queue: queue.QueueName, - autoAck: false, - consumer: consumer, - cancellationToken: stoppingToken); - - await Task.Delay(Timeout.Infinite, stoppingToken); - } -} - -/// <summary> -/// Wire format for a chat message published to RabbitMQ. -/// Includes the room name so the subscriber can route without inspecting headers. -/// </summary> -public record ChatMessageEnvelope(string RoomName, string User, DateTimeOffset SentAt, string Message); -``` - ---- - -## Step 4 — The Read Model and Command - -The observable query is structurally identical to the in-memory version. The only change is that `SendMessage` now publishes to RabbitMQ rather than calling `ChatService` directly. The `ChatSubscriber` will receive the message and call `ChatRoom.Receive()`, which fires the `BehaviorSubject`. - -```csharp -// Features/Chat/ChatRoomPage.cs -using Cratis.Arc.Commands.ModelBound; -using Cratis.Arc.Queries.ModelBound; -using System.Reactive.Subjects; -using System.Text.Json; -using RabbitMQ.Client; - -namespace MyApp.Chat; - -// ─── Read Model ─────────────────────────────────────────────────────────────── - -/// <summary> -/// Represents a single chat message. -/// </summary> -/// <param name="User">The display name of the sender.</param> -/// <param name="SentAt">The UTC time the message was sent.</param> -/// <param name="Message">The message text.</param> -[ReadModel] -public record ChatMessage(string User, DateTimeOffset SentAt, string Message) -{ - /// <summary> - /// Observes the live message feed for the given room. - /// The initial emission contains all persisted history for the room. - /// Subsequent emissions are triggered when <see cref="ChatSubscriber"/> routes a RabbitMQ message. - /// </summary> - /// <param name="roomName">The name of the room to observe.</param> - /// <param name="chatService">The chat service, injected by the framework.</param> - /// <returns>An observable that emits the full message list on each change.</returns> - public static ISubject<IEnumerable<ChatMessage>> ForRoom( - string roomName, - ChatService chatService) - { - var room = chatService.GetChatRoom(roomName); - var relay = new BehaviorSubject<IEnumerable<ChatMessage>>(room.Messages.Value); - room.Messages.Subscribe(relay); - return relay; - } -} - -// ─── Command ────────────────────────────────────────────────────────────────── - -/// <summary> -/// Sends a chat message to a room by publishing to RabbitMQ. -/// The <see cref="ChatSubscriber"/> delivers the message to all server instances. -/// </summary> -/// <param name="RoomName">The name of the room to post to.</param> -/// <param name="User">The display name of the sender.</param> -/// <param name="Message">The message text.</param> -[Command] -public record SendMessage(string RoomName, string User, string Message) -{ - /// <summary> - /// Publishes the message to the RabbitMQ exchange. - /// </summary> - /// <param name="channel">The RabbitMQ channel, injected by the framework.</param> - public async Task Handle(IChannel channel) - { - var envelope = new ChatMessageEnvelope(RoomName, User, DateTimeOffset.UtcNow, Message); - var body = JsonSerializer.SerializeToUtf8Bytes(envelope); - await channel.BasicPublishAsync( - exchange: "chat.messages", - routingKey: string.Empty, - body: body); - } -} -``` - -> **Register dependencies** in your `Program.cs`: -> -> ```csharp -> builder.Services.AddSingleton<ChatService>(); -> builder.Services.AddHostedService<ChatSubscriber>(); -> builder.Services.AddSingleton<IChatPersistence, YourChatPersistenceImplementation>(); -> -> // Register IConnectionFactory pointing at your RabbitMQ instance. -> builder.Services.AddSingleton<IConnectionFactory>(_ => -> new ConnectionFactory { HostName = "localhost" }); -> -> // Register a singleton IChannel for SendMessage to inject. -> builder.Services.AddSingleton<IConnection>(sp => -> sp.GetRequiredService<IConnectionFactory>() -> .CreateConnectionAsync().GetAwaiter().GetResult()); -> builder.Services.AddSingleton<IChannel>(sp => -> sp.GetRequiredService<IConnection>() -> .CreateChannelAsync().GetAwaiter().GetResult()); -> ``` - -> **Run `dotnet build`** after saving. The proxy generator produces the same `ForRoom.ts`, `SendMessage.ts`, and `ChatMessage.ts` as the in-memory version — the frontend does not change. - ---- - -## Step 5 — Delta Mode - -Delta mode works identically here. The server still emits the full list on each `OnNext()` call, but Arc only sends the difference to each client: - -- **First connection** — the client receives the full persisted history as the initial payload. -- **Each new message** — Arc computes the `ChangeSet` (one item in `added`, everything else unchanged) and sends only that diff. - -The `use()` hook applies each `ChangeSet` transparently, so `messagesResult.data` always holds the complete current collection. - -See [Delta Mode](/arc/backend/queries/change-stream/) and the [in-memory guide](../in-memory#step-3--delta-mode) for a full explanation of how the ChangeSet is computed and when to consider adding a `ChatMessageId` property. - ---- - -## Step 6 — The React Component - -The React component is **identical** to the one in the [in-memory guide](../in-memory#step-4--the-react-component). The observable query contract — `ForRoom.use({ roomName })` returning `messagesResult.data` — does not change regardless of how the backend sources its data. - ---- - -## What Changed from the In-Memory Version - -| Aspect | In-Memory | RabbitMQ | -| ------ | --------- | -------- | -| Initial history | Empty `[]` | Loaded from persistence layer | -| New messages enter via | `ChatRoom.Send()` directly | `ChatSubscriber` ← RabbitMQ | -| `SendMessage.Handle()` | Calls `chatService.GetChatRoom().Send()` | Publishes to `chat.messages` exchange | -| Scales across instances | No — state is process-local | Yes — every instance consumes from the exchange | -| Observable query | Unchanged | Unchanged | -| React component | Unchanged | Unchanged | - -The observable query and the frontend are unaffected by the backend data source. That is the point: `ISubject<IEnumerable<ChatMessage>>` is a contract between the query method and the framework, not between the query method and any specific storage technology. diff --git a/web/src/content/docs/scenarios/index.md b/web/src/content/docs/scenarios/index.md deleted file mode 100644 index f8b14ed5..00000000 --- a/web/src/content/docs/scenarios/index.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -title: Scenarios -description: End-to-end tutorials that show you how to build real software using the full Cratis stack — from event-sourced backend to reactive frontend. ---- - -# Scenarios - -Scenarios are end-to-end tutorials that show you how to build real software using the full Cratis stack — from event-sourced backend to reactive frontend — with concrete, working examples. - -Rather than covering individual APIs in isolation, each scenario builds a complete vertical slice of a real system. You will see how **[Chronicle](/chronicle/)**, **[Arc](/arc/)**, and **[Components](/components/)** fit together at every layer, and why that combination matters. - -## What you will find here - -| Section | Description | -| ------- | ----------- | -| [Camel Casing](/scenarios/camel-casing/) | How to configure camel casing consistently for Chronicle projections and Arc MongoDB documents from a Cratis meta package setup. | -| [Vertical Slices](/scenarios/vertical-slices/) | Step-by-step tutorials that build an event-sourced Library system one slice at a time, following Event Modeling patterns. | -| [Real-Time Chat](/scenarios/chat/) | How to use Arc observable queries with an in-memory service to build a live multi-room chat — no polling, no manual WebSocket setup. | - -## Approach - -Every scenario follows the same discipline: - -- **One behaviour at a time.** Each tutorial adds exactly one piece of functionality — a single vertical slice — so you can follow along without getting lost in unrelated complexity. -- **End-to-end.** Each slice goes all the way from the domain event in C# to the React component the user interacts with. -- **Real tooling.** The code uses the exact packages, conventions, and components you would use in a production Cratis project — not simplified toy APIs. -- **Explained, not just shown.** Each step explains *why* things are done a particular way, not just *what* to type. diff --git a/web/src/content/docs/scenarios/vertical-slices/automation.md b/web/src/content/docs/scenarios/vertical-slices/automation.md deleted file mode 100644 index cccb7269..00000000 --- a/web/src/content/docs/scenarios/vertical-slices/automation.md +++ /dev/null @@ -1,222 +0,0 @@ ---- -title: Automation — Cancel Expired Reservations ---- - -# Automation — Cancel Expired Reservations - -This tutorial builds the **Cancel Expired Reservations** slice of the Library system. It is an **Automation** — a pattern that runs entirely in the background, with no direct user interaction. - -A reactor watches the event log and automatically fires a command when a reservation has not been collected within the allowed window. No user clicks a button. No cron job. The system observes and acts. - -By the end you will have: - -- A `Reservation` read model that tracks pending reservations and their expiry dates -- A `ReservationExpired` event that marks a reservation as no longer valid -- A `CancelExpiredReservation` command that handles the cancellation -- A `ReservationExpiryReactor` that observes events and drives the process - ---- - -## What is Automation? - -In Event Modeling terms, an **Automation** slice answers the question: *"What should the system do on its own, without a human initiating it?"* - -The shape is: - -1. A read model acts as a **to-do list** — it contains items that the system needs to process -2. A **Reactor** watches the event stream -3. When the reactor sees a relevant event, it evaluates the to-do list and fires a **Command** for each item that needs action -4. That command produces another event, which closes the loop - -The key insight: Automation uses exactly the same building blocks as State Change and State View. There is no special scheduler, no background thread management, no cron expression. The event log drives everything. If Chronicle replays the event log (e.g. after a bug fix), the reactor runs again from where it left off — and only handles each event once. - -This is fundamentally different from a job queue. A job queue can lose items, process them twice, or fail silently. Chronicle's reactor guarantee: **at-least-once delivery, with idempotency controls**. - ---- - -## Domain Context - -The Reservation slice (not built in this tutorial, but required as a foundation) introduces: - -```csharp -[EventType] -public record BookReserved(ISBN Isbn, MemberId MemberId, DateTimeOffset ExpiresAt); - -[EventType] -public record ReservationCancelled(ISBN Isbn, MemberId MemberId); - -[EventType] -public record BookBorrowedFromReservation(ISBN Isbn, MemberId MemberId); -``` - -A `BookReserved` event starts the clock. If the member collects the book (`BookBorrowedFromReservation`), the reservation is closed. If they do not collect it before `ExpiresAt`, the system should cancel it and release the copy back to inventory. - ---- - -## Folder Structure - -``` -Features/ -└── Reservations/ - └── ExpiryManagement/ - └── ExpiryManagement.cs ← Read model + command + event + reactor (ALL backend) -``` - -There is no `.tsx` file for this slice — Automation has no UI. Everything happens in the background. - ---- - -## Step 1 — The Backend Slice - -```csharp -// Features/Reservations/ExpiryManagement/ExpiryManagement.cs -using Cratis.Arc.Commands.ModelBound; -using Cratis.Chronicle.Events; -using Cratis.Chronicle.Events.Projections; -using Cratis.Chronicle.Projections; -using Cratis.Chronicle.Read; -using Cratis.Chronicle.Reactors; -using MongoDB.Driver; -using Cratis.Extensions.MongoDB; -using Library.Reservations; - -namespace Library.Reservations.ExpiryManagement; - -// ─── Read Model ─────────────────────────────────────────────────────────────── - -[ReadModel] -[Passive] -public record PendingReservation( - [Key] ReservationId Id, - ISBN Isbn, - MemberId MemberId, - DateTimeOffset ExpiresAt); - -public class PendingReservationProjection : IProjectionFor<PendingReservation> -{ - public void Define(IProjectionBuilderFor<PendingReservation> builder) => builder - .From<BookReserved>(from => from - .Set(m => m.Isbn).To(e => e.Isbn) - .Set(m => m.MemberId).To(e => e.MemberId) - .Set(m => m.ExpiresAt).To(e => e.ExpiresAt)) - .RemovedWith<BookBorrowedFromReservation>() - .RemovedWith<ReservationCancelled>(); -} - -// ─── Event ──────────────────────────────────────────────────────────────────── - -[EventType] -public record ReservationExpired(ISBN Isbn, MemberId MemberId); - -// ─── Command ────────────────────────────────────────────────────────────────── - -[Command] -public record CancelExpiredReservation(ReservationId ReservationId) -{ - public ReservationExpired? Handle(PendingReservation? reservation) - { - if (reservation is null) return null; - if (reservation.ExpiresAt > DateTimeOffset.UtcNow) return null; - - return new ReservationExpired(reservation.Isbn, reservation.MemberId); - } -} - -// ─── Reactor ────────────────────────────────────────────────────────────────── - -public class ReservationExpiryReactor( - IMongoCollection<PendingReservation> reservations, - ICommandPipeline commandPipeline) : IReactor -{ - public async Task HandleBookReserved( - BookReserved @event, - EventContext context) - { - // A reservation was just created. We cannot cancel it immediately, - // but we record that we saw this event. A scheduler or a delayed - // trigger (e.g. Chronicle's scheduled reactors) would call back - // here at expiry time. For this walkthrough we fire a check - // eagerly — in production use a time-based trigger or an - // external scheduling mechanism that fires CancelExpiredReservation - // at the right moment. - - var reservationId = ReservationId.From(context.EventSourceId); - await commandPipeline.Execute(new CancelExpiredReservation(reservationId)); - } - - public async Task HandleDailyTick(DailyTick @event) - { - // In a real system you might fire a DailyTick event from a - // background service and sweep all pending reservations here. - var expired = await reservations - .Find(r => r.ExpiresAt <= DateTimeOffset.UtcNow) - .ToListAsync(); - - foreach (var reservation in expired) - { - await commandPipeline.Execute( - new CancelExpiredReservation(reservation.Id)); - } - } -} -``` - -### What is happening here? - -**`[Passive]`** on `PendingReservation` is important. This projection exists only to support decisions inside `CancelExpiredReservation.Handle()` — it is not intended to be queried by the frontend. `[Passive]` means [Chronicle](/chronicle/) will not proactively observe and replay the [projection](/chronicle/projections/); it is computed on demand when the command asks for it. - -**`PendingReservationProjection`** uses the fluent [`IProjectionFor<T>`](/chronicle/projections/) interface here instead of attribute convention, because the projection has a `.RemovedWith<T>()` rule: when the book is borrowed or the reservation is already cancelled, the document is removed from the collection. Attribute-based mapping can express `[RemovedWith<T>]` too — the fluent form is shown here to illustrate the alternative. - -**`CancelExpiredReservation.Handle(PendingReservation? reservation)`** demonstrates the **[Dynamic Consistency Boundary (DCB)](/chronicle/dynamic-consistency-boundary/)** pattern. When the [command pipeline](/arc/backend/commands/command-pipeline/) receives this command, it first resolves the `PendingReservation` for the given `ReservationId` — the `[Passive]` projection is computed from the event log at that moment — and injects it as a parameter into `Handle()`. If the reservation does not exist or has not yet expired, `Handle()` returns `null` (no event). Otherwise it returns `ReservationExpired`. - -This is a critical design: the command is the consistency boundary. It reads the current state **at the moment of execution**, decides whether to act, and either produces an event or does nothing. No stale state, no race conditions from separate read/check/write steps. - -**[`IReactor`](/chronicle/reactors/)** is a marker interface. [Chronicle](/chronicle/) discovers reactor methods by their first parameter type. `HandleBookReserved` is called every time a `BookReserved` event is appended to any event source. `EventContext` provides metadata like the event source ID, the sequence number, and the timestamp. - -**[`ICommandPipeline`](/arc/backend/commands/command-pipeline/)** is constructor-injected. The reactor calls `commandPipeline.Execute(new CancelExpiredReservation(...))` to trigger the cancellation command — going back through the full [Arc](/arc/) command pipeline, including validation. - ---- - -## Step 2 — Idempotency with `[OnceOnly]` - -If you only ever want a reactor method to fire the first time an event is seen (never during replays), annotate it with `[OnceOnly]`: - -```csharp -[OnceOnly] -public async Task HandleBookReserved(BookReserved @event, EventContext context) -{ - // Only executes on first processing — skipped during event log replay -} -``` - -Use `[OnceOnly]` for side effects that must happen exactly once regardless of how many times the event log is replayed — sending a notification, calling an external API, or registering a task in an external scheduler. - -For the cancellation check, you do *not* want `[OnceOnly]` — if you replay the log after fixing a bug, you want the check to run again so the read model reflects the correct final state. - ---- - -## Step 3 — Error Handling - -Chronicle guarantees delivery. If `HandleBookReserved` throws an unhandled exception, Chronicle pauses processing for that event source partition and retries. The partition will remain paused until the error is resolved. - -Design your reactor methods to be safe: - -- Catch exceptions from external dependencies and handle them gracefully -- Let business rule violations (e.g. reservation already cancelled) produce a `null` return from `Handle()`, not an exception -- Use `[OnceOnly]` when idempotency is not naturally guaranteed by read model state - ---- - -## Summary - -| Layer | Artifact | Technology | -| ----- | -------- | ---------- | -| Read model (internal) | `PendingReservation` with `[Passive]` | [Chronicle](/chronicle/) [projection](/chronicle/projections/) | -| Cancellation event | `ReservationExpired` | [Chronicle](/chronicle/) [`[EventType]`](/chronicle/events/) | -| Command + DCB rule | `CancelExpiredReservation` | [Arc](/arc/) [`[Command]`](/arc/backend/commands/model-bound/) + `Handle(ReadModel?)` | -| Automation driver | `ReservationExpiryReactor` | [Chronicle](/chronicle/) [`IReactor`](/chronicle/reactors/) | -| Command execution | `ICommandPipeline.Execute(...)` | [Arc](/arc/) [command pipeline](/arc/backend/commands/command-pipeline/) | - -No scheduled tasks. No database polling. No message queue workers. The event log is the engine; the reactor is the wiring. Every cancellation is an event — auditable, replayable, and traceable. - -**Next**: [Translation — Import Members from HR](../translator) diff --git a/web/src/content/docs/scenarios/vertical-slices/index.md b/web/src/content/docs/scenarios/vertical-slices/index.md deleted file mode 100644 index 9c04c9a6..00000000 --- a/web/src/content/docs/scenarios/vertical-slices/index.md +++ /dev/null @@ -1,105 +0,0 @@ ---- -title: Vertical Slices -description: Step-by-step tutorials that build an event-sourced Library system one slice at a time, following Event Modeling patterns. ---- - -# Vertical Slices - -This series of tutorials builds a **Library system** end to end, one behaviour at a time. Each tutorial corresponds to one of the four slice patterns from [Event Modeling](https://novanet.no/stop-guessing-start-modeling/), and each one builds on the previous. - -By the end you will have seen how every layer of the Cratis stack fits together: [Chronicle](/chronicle/) event sourcing, [Arc](/arc/)'s CQRS application model, and the purpose-built [Components](/components/) library. - ---- - -## Why Event Modeling? - -Most software projects don't fail because of bad code. They fail because the team built the wrong thing — or built the right thing but nobody agrees on what it actually does. A product owner, a developer, and a domain expert sit in a meeting, all walk out believing they understood each other, and three weeks later reality proves otherwise. - -[Event Modeling](https://eventmodeling.org) addresses this directly. It is a way to design and describe information systems using a **shared timeline** — a visual blueprint everyone on the team can read, from developers to domain experts to product owners. Unlike a traditional specification document, an event model is collaborative and alive. It uses only three building blocks and four patterns. You can explain the core concept in minutes; the rest you learn by doing. - -### The Three Building Blocks - -Every Event Model is made from exactly three concepts: - -| Building Block | What it is | Examples | -| ------------ | ---------- | ------- | -| **Events** | Facts — immutable records of things that have already happened | `AuthorRegistered`, `BookBorrowed`, `LoanOverdue` | -| **Commands** | Intentions — what a user (or system) is trying to do, which will cause an event | `RegisterAuthor`, `BorrowBook`, `CancelReservation` | -| **Read Models** | Outputs — how the system informs users about what is going on | The author list, the inventory dashboard, the borrowing history | - -Put them together and you have a complete picture of any workflow: a command comes in, gets validated, an event is recorded, the read model is updated — and the user sees the result. - -### The Four Patterns - -Three building blocks. Four ways to combine them. That is the entire vocabulary. - -#### State Change - -A user submits a command. It gets validated. An event is recorded. - -`RegisterAuthor` fires → `AuthorRegistered` is stored. The intent is explicit, the outcome is captured. This is the most common pattern — the write side of your system. - -In Cratis this is: a [`[Command]`](/arc/backend/commands/model-bound/) record with a `Handle()` method that returns a Chronicle [`[EventType]`](/chronicle/events/), optionally enforced by a [`CommandValidator<T>`](/arc/backend/commands/command-validation/) or an [`IConstraint`](/chronicle/constraints/). - -#### State View - -Events are **projected** into a read model that the UI displays. - -An `Author` read model gets built from `AuthorRegistered` events. It is always up to date, and you can rebuild it from scratch at any point just by replaying the events. This is the read side — fast, purpose-built, and completely independent from the write side. - -In Cratis this is: a [`[ReadModel]`](/chronicle/read-models/) record decorated with [`[FromEvent<T>]`](/chronicle/projections/) attributes and a static query method that returns an `ISubject<IEnumerable<T>>` for real-time reactivity. - -#### Automation - -A processor watches a read model (think: a to-do list), picks up items, and fires a command to handle each one — entirely behind the scenes. - -Sending an overdue notice when a loan passes its return date. Cancelling a reservation that was never collected. Triggering a payment. No human involved; the same building blocks, automated. - -In Cratis this is: an [`IReactor`](/chronicle/reactors/) that observes a Chronicle event stream and calls [`ICommandPipeline`](/arc/backend/commands/command-pipeline/) to fire commands back into your own system. - -#### Translation - -When an event comes from an external system — one you don't own — you translate its language into yours. You don't want raw payloads as domain events. You want `BookInformationReceived` and `MemberImported` — events that mean something in your own context. - -In Cratis this is: an [`IReactor`](/chronicle/reactors/) that listens for external events and fires commands in your own system, which in turn produce domain events with your own vocabulary. - ---- - -## How Cratis Maps to Event Modeling - -| Pattern | Chronicle | Arc | Components | -| ------- | --------- | --- | ---------- | -| **State Change** | [`[EventType]`](/chronicle/events/) records stored in the event log | [`[Command]`](/arc/backend/commands/model-bound/) + `Handle()`, [`CommandValidator<T>`](/arc/backend/commands/command-validation/), [`IConstraint`](/chronicle/constraints/) | [`CommandDialog`](/components/commanddialog/) for the form UI | -| **State View** | [Projections](/chronicle/projections/) (`[FromEvent<T>]`, `IProjectionFor<T>`) building [`[ReadModel]`](/chronicle/read-models/) | `IQueryFor<T>` / `IObservableQueryFor<T>` generated proxies | [`DataPage`](/components/datapage/) for the listing UI | -| **Automation** | [`IReactor`](/chronicle/reactors/) observing the event log | [`ICommandPipeline`](/arc/backend/commands/command-pipeline/) to fire commands | No UI — runs in the background | -| **Translation** | [`IReactor`](/chronicle/reactors/) on external event streams | [`ICommandPipeline`](/arc/backend/commands/command-pipeline/) bridging to domain commands | No UI — integration layer | - -The key insight: [Chronicle](/chronicle/) stores the facts (events), [Arc](/arc/) wires up the intent (commands) and the queries, [Components](/components/) renders the result. Each layer has one job and they compose cleanly. - ---- - -## The Library System - -All four tutorials build parts of a **Library** system with the following capabilities: - -- **Authors** — register and list authors -- **Members** — register and list library members -- **Book Catalog** — register books with ISBN and associate them with authors -- **Book Inventory** — track how many copies are in stock -- **Reservations** — reserve a book for a member, subject to availability -- **Lending** — lend out a book and track return dates - -The tutorials do not implement everything. Instead, each one picks the behaviour that best illustrates a single pattern, so the focus stays on the technique, not the domain complexity. - ---- - -## Tutorials - -Work through these in order — each one builds on the context from the previous. - -| Tutorial | Pattern | What you build | -| -------- | ------- | -------------- | -| [State Change — Register an Author](./state-change) | State Change | `RegisterAuthor` command, `AuthorRegistered` event, `AddAuthor` React form using `CommandDialog` | -| [State View — List Authors](./state-view) | State View | `Author` read model, projection from events, `AllAuthors` observable query, `Authors` listing page using `DataPage` | -| [Automation — Cancel Expired Reservations](./automation) | Automation | `PendingReservations` read model, `CancelReservation` reactor that fires automatically when a reservation expires | -| [Translation — Import Members from HR](./translator) | Translation | Reactor that listens for `HRMemberCreated` external events and fires `RegisterMember` in the library domain | diff --git a/web/src/content/docs/scenarios/vertical-slices/state-change.md b/web/src/content/docs/scenarios/vertical-slices/state-change.md deleted file mode 100644 index 36071167..00000000 --- a/web/src/content/docs/scenarios/vertical-slices/state-change.md +++ /dev/null @@ -1,277 +0,0 @@ ---- -title: State Change — Register an Author ---- - -# State Change — Register an Author - -This tutorial builds the **Register Author** slice of the Library system. It is a **State Change** — the most fundamental pattern in Event Modeling. - -A user fills in a form, that maps to a command, the command is validated, an event is recorded in Chronicle, and the UI confirms the result. - -By the end you will have: - -- A `RegisterAuthor` command with built-in validation -- An `AuthorRegistered` event stored in Chronicle -- A `CommandDialog`-based React form that calls the command and gives the user feedback -- A uniqueness constraint to prevent duplicate author names - ---- - -## What is a State Change? - -In Event Modeling terms, a **State Change** slice is the answer to the question: *"What can a user do that will permanently change the system?"* - -The shape is always the same: - -1. A user provides input -2. That input is turned into a **Command** — an explicit statement of intent -3. The command is validated (is this allowed right now?) -4. If valid, one or more **Events** are appended to the event log -5. The event log is the truth — the state has changed - -Nothing in the system reads from a mutable table. There is no `UPDATE authors SET ...`. Instead you ask: did `AuthorRegistered` ever happen for this ID? The answer to that question is in the event log, and it never changes. - ---- - -## Folder Structure - -Following the vertical slice convention, everything for this slice lives together: - -``` -Features/ -└── Authors/ - ├── AuthorId.cs ← Concept: strongly-typed author identity - ├── AuthorName.cs ← Concept: strongly-typed author name - └── Registration/ - ├── Registration.cs ← Command + event + constraint (ALL backend) - └── AddAuthor.tsx ← React component for the add-author form -``` - -One folder. One `.cs` file for all backend artefacts. One `.tsx` file for the UI. - ---- - -## Step 1 — Concepts - -Before writing the command, introduce strongly-typed value objects using [`ConceptAs<T>`](/fundamentals/). Raw strings and `Guid`s have no domain meaning; `AuthorName` and `AuthorId` do. - -```csharp -// Features/Authors/AuthorId.cs -using Cratis.Concepts; - -namespace Library.Authors; - -public record AuthorId(Guid Value) : ConceptAs<Guid>(Value) -{ - public static AuthorId New() => new(Guid.NewGuid()); -} -``` - -```csharp -// Features/Authors/AuthorName.cs -using Cratis.Concepts; - -namespace Library.Authors; - -public record AuthorName(string Value) : ConceptAs<string>(Value); -``` - -`ConceptAs<T>` gives you type safety, implicit conversion to and from the underlying primitive, and meaningful error messages. The framework recognises these types throughout — in Chronicle keys, in JSON serialisation, and in proxy generation. - ---- - -## Step 2 — The Backend Slice - -All backend artefacts for this slice live in a single file: `Registration.cs`. - -```csharp -// Features/Authors/Registration/Registration.cs -using Cratis.Arc.Commands.ModelBound; -using Cratis.Arc.Validation; -using Cratis.Chronicle.Constraints; -using Cratis.Chronicle.Events; -using FluentValidation; -using Library.Authors; - -namespace Library.Authors.Registration; - -// ─── Event ──────────────────────────────────────────────────────────────────── - -[EventType] -public record AuthorRegistered(AuthorName FirstName, AuthorName LastName); - -// ─── Validation ─────────────────────────────────────────────────────────────── - -public class RegisterAuthorValidator : CommandValidator<RegisterAuthor> -{ - public RegisterAuthorValidator() - { - RuleFor(c => c.FirstName) - .NotEmpty().WithMessage("First name is required"); - - RuleFor(c => c.LastName) - .NotEmpty().WithMessage("Last name is required"); - } -} - -// ─── Uniqueness Constraint ──────────────────────────────────────────────────── - -public class UniqueAuthorName : IConstraint -{ - public void Define(IConstraintBuilder builder) => builder - .Unique(_ => _ - .On<AuthorRegistered>(e => $"{e.FirstName} {e.LastName}") - .WithMessage("An author with that name is already registered")); -} - -// ─── Command ────────────────────────────────────────────────────────────────── - -[Command] -public record RegisterAuthor(AuthorName FirstName, AuthorName LastName) -{ - public (AuthorId, AuthorRegistered) Handle() - { - var authorId = AuthorId.New(); - return (authorId, new AuthorRegistered(FirstName, LastName)); - } -} -``` - -### What is happening here? - -**[`[EventType]`](/chronicle/events/)** marks the record as a [Chronicle](/chronicle/) event. The framework uses the type name as the event identifier — no GUID argument, no string argument. Every property is an immutable fact; there are no nullable fields. If first name and last name are both required, the event proves it. - -**[`CommandValidator<T>`](/arc/backend/commands/command-validation/)** extends FluentValidation. It runs automatically before `Handle()` is ever called. If any rule fails the [command pipeline](/arc/backend/commands/command-pipeline/) short-circuits and returns validation errors to the caller — no exception throwing required. - -**[`IConstraint`](/chronicle/constraints/)** is a [Chronicle](/chronicle/)-level uniqueness guard that spans across all event sources (i.e. all authors). It observes every `AuthorRegistered` event and builds an index of `"FirstName LastName"` values. If the combination already exists the command is rejected before `Handle()` runs. - -**[`[Command]` with `Handle()`](/arc/backend/commands/model-bound/)** is the [Arc](/arc/) model-bound command pattern. The return value is a tuple: the first element (`AuthorId`) becomes the `CommandResult.Response` value that the frontend receives; the second element (`AuthorRegistered`) is the [Chronicle](/chronicle/) event to append. The framework resolves the event source ID from the `AuthorId` return value automatically. - -> **Build before writing frontend code.** Run `dotnet build` after saving `Registration.cs`. This generates a TypeScript proxy (`RegisterAuthor.ts`) via [Arc's proxy generation](/arc/backend/proxy-generation/) in your frontend project — without it, the React component has nothing to import. - ---- - -## Step 3 — The React Component - -With the proxy generated, the frontend component is straightforward. - -```tsx -// Features/Authors/Registration/AddAuthor.tsx -import { DialogResult, useDialogContext } from '@cratis/arc.react/dialogs'; -import { CommandResult } from '@cratis/arc/commands'; -import { CommandDialog } from '@cratis/components/CommandDialog'; -import { InputTextField } from '@cratis/components/CommandForm'; -import { RegisterAuthor } from './commands/RegisterAuthor'; - -type RegisterAuthorResponse = { - authorId: string; -}; - -export const AddAuthor = () => { - const { closeDialog } = useDialogContext<CommandResult<RegisterAuthorResponse>>(); - - return ( - <CommandDialog<RegisterAuthor> - command={RegisterAuthor} - title="Register Author" - okLabel="Register" - onConfirm={async () => closeDialog(DialogResult.Ok)} - onCancel={() => closeDialog(DialogResult.Cancelled)} - > - <InputTextField<RegisterAuthor> - value={instance => instance.firstName} - title="First name" - /> - <InputTextField<RegisterAuthor> - value={instance => instance.lastName} - title="Last name" - /> - </CommandDialog> - ); -}; -``` - -The dialog component uses `useDialogContext` from [`@cratis/arc.react/dialogs`](/arc/frontend/react/) to get the `closeDialog` function. It does not receive props for visibility — all dialog lifecycle is managed by the framework. A parent component uses the [`useDialog`](/arc/frontend/react/) hook to show and await this dialog. - -[`CommandDialog`](/components/commanddialog/) from `@cratis/components` does the heavy lifting: - -- It reads the `RegisterAuthor` proxy to know what fields exist -- [`InputTextField`](/components/commandform/) renders typed form fields bound to command properties -- It runs the frontend-side validation defined in the proxy -- It calls the [Arc command pipeline](/arc/backend/commands/command-pipeline/) when the user confirms -- `onConfirm` fires only after command execution succeeds -- It surfaces any backend validation errors directly in the form -- It gives the user a success or error response without you writing any `fetch` calls - -The parent awaits the result: - -```tsx -const [AddAuthorDialog, showAddAuthor] = useDialog<CommandResult<RegisterAuthorResponse>>(AddAuthor); - -const handleAdd = async () => { - const [dialogResult, commandResult] = await showAddAuthor(); - if (dialogResult === DialogResult.Ok && commandResult?.isSuccess) { - // The author was registered; commandResult.response.authorId is available - } -}; -``` - ---- - -## Step 4 — Integration Specs - -For state-change slices, write integration specs that prove the events are correct. - -``` -Features/Authors/Registration/when_registering/ -├── and_author_does_not_exist.cs -└── and_author_name_already_exists.cs -``` - -```csharp -// when_registering/and_author_does_not_exist.cs -using Cratis.Chronicle.Testing; -using Library.Authors; -using Library.Authors.Registration; - -namespace when_registering; - -public class and_author_does_not_exist : given.an_event_store -{ - RegisterAuthor command; - - void Establish() => - command = new RegisterAuthor( - new AuthorName("Tolkien"), - new AuthorName("J.R.R.")); - - async Task Because() => await CommandPipeline.Execute(command); - - [Fact] void should_result_in_an_author_registered_event() => - Events.ShouldContainSingle<AuthorRegistered>(); - - [Fact] void should_have_correct_first_name() => - Events.Single<AuthorRegistered>().FirstName.Value.ShouldEqual("Tolkien"); - - [Fact] void should_have_correct_last_name() => - Events.Single<AuthorRegistered>().LastName.Value.ShouldEqual("J.R.R."); -} -``` - -These specs run against Chronicle's in-memory test harness — no Docker, no MongoDB, instant feedback. - ---- - -## Summary - -| Layer | Artifact | Technology | -| ----- | -------- | ---------- | -| Domain event | `AuthorRegistered` | [Chronicle](/chronicle/) [`[EventType]`](/chronicle/events/) | -| Command + handler | `RegisterAuthor` with `Handle()` | [Arc](/arc/) [`[Command]`](/arc/backend/commands/model-bound/) model-bound | -| Input validation | `RegisterAuthorValidator` | [Arc](/arc/) [`CommandValidator<T>`](/arc/backend/commands/command-validation/) + FluentValidation | -| Uniqueness constraint | `UniqueAuthorName` | [Chronicle](/chronicle/) [`IConstraint`](/chronicle/constraints/) | -| React form | `AddAuthor.tsx` | [`@cratis/components`](/components/) [`CommandDialog`](/components/commanddialog/) | - -The entire write side — event, validator, constraint, command — is in one file. The frontend is one component that imports one generated proxy. Zero boilerplate. Zero glue code. The framework wires it together. - -**Next**: [State View — List Authors](../state-view) diff --git a/web/src/content/docs/scenarios/vertical-slices/state-view.md b/web/src/content/docs/scenarios/vertical-slices/state-view.md deleted file mode 100644 index 4b84548b..00000000 --- a/web/src/content/docs/scenarios/vertical-slices/state-view.md +++ /dev/null @@ -1,227 +0,0 @@ ---- -title: State View — List Authors ---- - -# State View — List Authors - -This tutorial builds the **List Authors** slice of the Library system. It is a **State View** — the read side of Event Modeling. - -Events recorded by the [Register Author](../state-change) slice are projected into a purpose-built read model, exposed through an observable query, and rendered in a live-updating page. - -By the end you will have: - -- An `Author` read model automatically projected from `AuthorRegistered` events -- An `AllAuthors` observable query that pushes updates to the frontend in real time -- An `Authors` listing page using `DataPage` from `@cratis/components` - ---- - -## What is a State View? - -In Event Modeling terms, a **State View** slice answers the question: *"What does the user need to see right now?"* - -The shape is: - -1. Events that have been recorded are **projected** into a **Read Model** -2. The read model is a purpose-built view — not a generic table, but exactly the shape a specific UI needs -3. The frontend queries the read model and renders it - -This is the read side of CQRS. The read model never writes to the event log — it only reads from it. You can have as many projections as you like from the same events, each optimised for a different query. If you change what data the UI needs, you change the projection and replay; the event log is untouched. - -One of Chronicle's most powerful features is that projections are **rewindable**: drop the read model collection, replay the events, and the read model is reconstructed perfectly. Your data is always recoverable. - ---- - -## Folder Structure - -``` -Features/ -└── Authors/ - ├── AuthorId.cs ← Shared concept (from the State Change slice) - ├── AuthorName.cs ← Shared concept (from the State Change slice) - └── Listing/ - ├── Listing.cs ← Read model + projection + query (ALL backend) - └── Listing.tsx ← React component for the listing page -``` - ---- - -## Step 1 — The Backend Slice - -All backend artefacts for this slice live in `Listing.cs`. - -```csharp -// Features/Authors/Listing/Listing.cs -using Cratis.Chronicle.Events.Projections; -using Cratis.Chronicle.Projections; -using Cratis.Chronicle.Read; -using MongoDB.Driver; -using Cratis.Extensions.MongoDB; -using Library.Authors.Registration; -using Library.Authors; - -namespace Library.Authors.Listing; - -// ─── Read Model ─────────────────────────────────────────────────────────────── - -[ReadModel] -[FromEvent<AuthorRegistered>] -public record Author( - [Key] AuthorId Id, - AuthorName FirstName, - AuthorName LastName) -{ - public static ISubject<IEnumerable<Author>> AllAuthors( - IMongoCollection<Author> collection) => - collection.Observe(); -} -``` - -### What is happening here? - -**[`[ReadModel]`](/chronicle/read-models/)** registers the record with [Chronicle](/chronicle/) as a MongoDB-backed projection target. Chronicle automatically creates and maintains the collection. You never write a MongoDB query to update it — Chronicle does that from the event stream. - -**[`[FromEvent<AuthorRegistered>]`](/chronicle/projections/)** is a projection shorthand: *“when an `AuthorRegistered` event is appended, map its properties to this read model using convention.”* Chronicle matches properties by name. `FirstName` on the event maps to `FirstName` on the read model, `LastName` to `LastName`. No explicit mapping code needed. - -**`[Key]`** on `AuthorId` tells Chronicle which property is the read model's primary key, and how to correlate events to read model instances. Because `RegisterAuthor.Handle()` returns an `AuthorId` as the event source identity, Chronicle stores the `AuthorRegistered` event under that ID — and the projection updates the `Author` document with the same ID. - -**`AllAuthors`** is a static query method. Method parameters are automatically resolved from DI — `IMongoCollection<Author>` is provided because the type is a `[ReadModel]`. The return type `ISubject<IEnumerable<Author>>` is a reactive [observable query](/arc/backend/queries/): the frontend receives the current list immediately, and then receives a new emission whenever any document in the collection changes. No polling. No WebSockets to configure manually. - -> **Run `dotnet build`** after saving `Listing.cs`. This generates the `AllAuthors.ts` query proxy and the `Author.ts` model type via [Arc's proxy generation](/arc/backend/proxy-generation/) used by the frontend component. - ---- - -## Step 2 — Projection Mapping Options - -The example above uses attribute-based convention mapping, which works when event and read model property names match. For cases where they differ, or where you need arithmetic operations, use the full attribute vocabulary: - -| Attribute | What it does | -| --------- | ------------ | -| `[FromEvent<T>]` on the record | Auto-map all matching properties from event `T` | -| `[FromEvent<T>(key: nameof(...))]` | Map from a specific event property as the key | -| `[SetFrom<T>]` | Explicit property mapping from a named event | -| `[AddFrom<T>]` / `[SubtractFrom<T>]` | Accumulate values from an event | -| `[Increment<T>]` / `[Decrement<T>]` | Increment or decrement a counter | -| `[Count<T>]` | Count occurrences of an event type | -| `[RemovedWith<T>]` | Remove the read model document when this event occurs | -| `[Join<T>]` | Join properties from a second event stream | - -For the most complex cases — conditional updates, aggregations, computed properties — use the fluent `IProjectionFor<T>` interface instead: - -```csharp -public class AuthorProjection : IProjectionFor<Author> -{ - public void Define(IProjectionBuilderFor<Author> builder) => builder - .From<AuthorRegistered>(); -} -``` - -AutoMap is on by default. `.From<AuthorRegistered>()` alone is enough when names match. - ---- - -## Step 3 — The React Component - -```tsx -// Features/Authors/Listing/Listing.tsx -import { useState } from 'react'; -import { DialogResult, useDialog } from '@cratis/arc.react/dialogs'; -import { CommandResult } from '@cratis/arc/commands'; -import { Column } from 'primereact/column'; -import { DataPage, MenuItem, MenuItems, Columns } from '@cratis/components'; -import { AllAuthors } from './queries/AllAuthors'; -import { AddAuthor, type RegisterAuthorResponse } from '../Registration/AddAuthor'; -import type { Author } from './queries/Author'; - -export const Listing = () => { - const [AddAuthorDialog, showAddAuthor] = useDialog<CommandResult<RegisterAuthorResponse>>(AddAuthor); - const [selected, setSelected] = useState<Author | undefined>(undefined); - - return ( - <> - <DataPage - title="Authors" - query={AllAuthors} - emptyMessage="No authors registered yet" - dataKey="id" - onSelectionChange={setSelected} - > - <MenuItems> - <MenuItem - label="Add Author" - icon="pi pi-plus" - command={async () => { - const [dialogResult] = await showAddAuthor(); - // DataPage auto-refreshes via the observable query - }} - /> - </MenuItems> - - <Columns> - <Column field="firstName" header="First Name" sortable /> - <Column field="lastName" header="Last Name" sortable /> - </Columns> - </DataPage> - - <AddAuthorDialog /> - </> - ); -}; -``` - -### What is happening here? - -**`AllAuthors`** is the generated query proxy — an `IObservableQueryFor<Author[]>` implementation. [`DataPage`](/components/datapage/) calls it once, subscribes to its observable, and re-renders whenever the backend pushes a new list. If another user registers an author in another browser tab, this list updates without any manual refresh. - -**[`DataPage`](/components/datapage/)** from `@cratis/components` provides the complete page chrome: title, action menu bar, a data table with sorting and filtering, and pagination. You declare columns as children using PrimeReact's `Column` and the component does everything else. - -**`useDialog<CommandResult<RegisterAuthorResponse>>(AddAuthor)`** from [`@cratis/arc.react/dialogs`](/arc/frontend/react/) returns a tuple: `AddAuthorDialog` is a wrapper component that you render in JSX, and `showAddAuthor` is an async function that opens the dialog and returns `[dialogResult, commandResult]` when it closes. The type parameter `CommandResult<RegisterAuthorResponse>` flows end-to-end — the dialog uses `useDialogContext` with the same type, so close-data is fully typed. - -**`MenuItem`** in the `MenuItems` slot adds an action to the toolbar. The `command` handler `await`s the dialog — you can inspect the result if needed, but since `DataPage` subscribes to the observable query, the list updates automatically after a successful registration. - -The `AddAuthor` component is imported from the Registration slice — slices within the same feature compose naturally because they share the `AuthorId` and `AuthorName` concepts from the parent folder. - ---- - -## Step 4 — Wiring to the Feature Page - -Each feature has a composition page that assembles its slices. - -```tsx -// Features/Authors/Authors.tsx -import { Listing } from './Listing/Listing'; - -export const Authors = () => <Listing />; -``` - -In larger features this page will host a navigation menu that switches between slices. For now, the listing is the whole feature. - ---- - -## Step 5 — Registering the Route - -Register `Authors` in your application's router: - -```tsx -// App.tsx (ASP.NET Core Vite integration) -import { Route } from 'react-router-dom'; -import { Authors } from './Features/Authors/Authors'; - -// ...inside your <Routes> -<Route path="/authors" element={<Authors />} /> -``` - ---- - -## Summary - -| Layer | Artifact | Technology | -| ----- | -------- | ---------- | -| Read model | `Author` record | [Chronicle](/chronicle/) [`[ReadModel]`](/chronicle/read-models/) + [`[FromEvent<T>]`](/chronicle/projections/) | -| Query | `AllAuthors` static method | [Chronicle](/chronicle/) `ISubject<IEnumerable<T>>` | -| Generated proxy | `AllAuthors.ts` | [Arc proxy generation](/arc/backend/proxy-generation/) | -| Listing page | `Listing.tsx` | [`@cratis/components`](/components/) [`DataPage`](/components/datapage/) | - -The read model and its query fit in one record. The projection is zero-configuration convention mapping. The frontend subscribes to a live stream, not a static snapshot. The UI automatically reflects every state change appended anywhere in the system — including changes from the [Register Author](../state-change) slice. - -**Next**: [Automation — Cancel Expired Reservations](../automation) diff --git a/web/src/content/docs/scenarios/vertical-slices/translator.md b/web/src/content/docs/scenarios/vertical-slices/translator.md deleted file mode 100644 index 4ac18449..00000000 --- a/web/src/content/docs/scenarios/vertical-slices/translator.md +++ /dev/null @@ -1,239 +0,0 @@ ---- -title: Translation — Import Members from HR ---- - -# Translation — Import Members from HR - -This tutorial builds the **Member Import** slice of the Library system. It is a **Translation** — a pattern for integrating with external systems without letting their language leak into your domain. - -An HR system delivers staff records. The Library does not own those events, does not control their schema, and does not want to be coupled to how the HR system names things. A translator reactor listens for the external events, translates them into the Library's own vocabulary, and fires commands that produce proper domain events. - -By the end you will have: - -- A `HRMemberCreated` event type representing what the HR system sends -- A `RegisterMember` command — the Library's own domain command -- A `MemberImportReactor` that bridges the two, doing nothing except translate -- A clear understanding of why this boundary matters - ---- - -## What is Translation? - -In Event Modeling terms, a **Translation** slice answers the question: *"How do we accept information from outside our boundary without letting that outside world shape our inside world?"* - -The two systems have different needs: - -| HR System | Library System | -| --------- | -------------- | -| `PersonnelRecord` with 40 fields | `Member` with `FirstName`, `LastName` | -| Employee ID as string `"EMP-00247"` | `MemberId` as a domain concept | -| `status: "ACTIVE"` / `"INACTIVE"` | `MemberRegistered` / `MemberDeactivated` events | -| Events owned by HR | Events owned by the Library | - -If the Library stored `HRMemberCreated` events directly, every part of the codebase would need to know HR's schema. When HR adds a field, renames a value, or changes a status code, every Library projection and reactor breaks. - -Translation keeps these concerns completely separate: - -1. The Translator observes the external event stream -2. It extracts only the data the Library cares about -3. It fires a standard Library command — `RegisterMember` — using that data -4. The Library's own command pipeline validates and records a `MemberRegistered` event - -The Library side never sees the HR event. It only ever sees its own, clean, domain events. - ---- - -## Folder Structure - -``` -Features/ -└── Members/ - ├── MemberId.cs ← Concept: strongly-typed member identity - ├── MemberName.cs ← Concept: strongly-typed member name - ├── Registration/ - │ └── Registration.cs ← RegisterMember command + MemberRegistered event - └── HRIntegration/ - └── HRIntegration.cs ← External event type + translator reactor -``` - -The integration concern lives in its own slice folder. If the HR integration is ever replaced with a different HR system, you delete or replace `HRIntegration/` without touching `Registration/`. - ---- - -## Step 1 — The Member Registration Slice - -First, the domain side — the Library's own vocabulary. This follows the same State Change pattern from [Register an Author](../state-change). - -```csharp -// Features/Members/MemberId.cs -using Cratis.Concepts; - -namespace Library.Members; - -public record MemberId(Guid Value) : ConceptAs<Guid>(Value) -{ - public static MemberId New() => new(Guid.NewGuid()); - public static MemberId From(string value) => new(Guid.Parse(value)); -} -``` - -```csharp -// Features/Members/MemberName.cs -using Cratis.Concepts; - -namespace Library.Members; - -public record MemberName(string Value) : ConceptAs<string>(Value); -``` - -```csharp -// Features/Members/Registration/Registration.cs -using Cratis.Arc.Commands.ModelBound; -using Cratis.Chronicle.Constraints; -using Cratis.Chronicle.Events; -using Library.Members; - -namespace Library.Members.Registration; - -[EventType] -public record MemberRegistered(MemberName FirstName, MemberName LastName); - -public class UniqueMemberName : IConstraint -{ - public void Define(IConstraintBuilder builder) => builder - .Unique(_ => _ - .On<MemberRegistered>(e => $"{e.FirstName} {e.LastName}") - .WithMessage("A member with that name is already registered")); -} - -[Command] -public record RegisterMember(MemberName FirstName, MemberName LastName) -{ - public (MemberId, MemberRegistered) Handle() - { - var memberId = MemberId.New(); - return (memberId, new MemberRegistered(FirstName, LastName)); - } -} -``` - -Notice that `RegisterMember` is a perfectly ordinary Library command. It knows nothing about HR. It can be called from the UI, from an API, or — as in this tutorial — from a reactor. - ---- - -## Step 2 — The Translator Slice - -Now the integration side. This is the only place in the codebase that knows anything about the HR system's shape. - -```csharp -// Features/Members/HRIntegration/HRIntegration.cs -using Cratis.Arc.Commands; -using Cratis.Chronicle.Events; -using Cratis.Chronicle.Reactors; -using Library.Members; -using Library.Members.Registration; - -namespace Library.Members.HRIntegration; - -// ─── External Event ─────────────────────────────────────────────────────────── -// This represents what the HR system sends. We define it here in our codebase -// because Chronicle needs a strongly-typed record to deserialise the event from -// the external event stream. It is NOT a Library domain event — it is just a -// data transfer type that mirrors the HR system's schema. - -[EventType] -public record HRMemberCreated( - string EmployeeId, - string GivenName, - string FamilyName, - string Status); - -// ─── Translator Reactor ─────────────────────────────────────────────────────── - -public class MemberImportReactor(ICommandPipeline commandPipeline) : IReactor -{ - [OnceOnly] - public async Task HandleHRMemberCreated( - HRMemberCreated @event, - EventContext context) - { - // Only import active staff as library members - if (@event.Status != "ACTIVE") return; - - await commandPipeline.Execute(new RegisterMember( - FirstName: new MemberName(@event.GivenName), - LastName: new MemberName(@event.FamilyName))); - } -} -``` - -### What is happening here? - -**[`[EventType]`](/chronicle/events/)** on `HRMemberCreated` does not make this a Library event. It is just the C# type that [Chronicle](/chronicle/) uses to deserialise the incoming event from the external stream. Chronicle needs strongly-typed events; this is the representation of the HR system's payload in our codebase. A better name for this concept is an *integration event* — it lives at the boundary and has no standing in the domain. - -**`[OnceOnly]`** is essential here. This [reactor](/chronicle/reactors/) has an external side effect: it calls `RegisterMember`, which appends a `MemberRegistered` event to the Library's own event log. If the event log were ever replayed, you do not want to fire `RegisterMember` again for every HR event that already produced a successful import — that would create duplicate members. `[OnceOnly]` ensures this method runs exactly once per HR event, even across replays. - -**Status filtering** shows how the translator makes decisions. The Library does not care about contract staff, secondees, or inactive records — it only wants active personnel. That filter lives here, at the integration boundary. The `RegisterMember` command never needs to know that the Library has an HR integration; it just registers members. - -**[`ICommandPipeline.Execute`](/arc/backend/commands/command-pipeline/)** routes through the full [Arc](/arc/) command pipeline — validation, [constraints](/chronicle/constraints/), [Chronicle](/chronicle/) event append — exactly as if a user had clicked a button on a form. The `UniqueMemberName` constraint from `Registration.cs` will fire here too. If the member was already imported (because, for example, the HR system sent the event twice), the constraint will reject the duplicate `RegisterMember` and nothing bad happens. - ---- - -## Step 3 — Comparing to Automation - -At a glance, Automation and Translation look similar — both use `IReactor` and both call `ICommandPipeline`. The difference is in *who owns the events*: - -| | Automation | Translation | -| --- | --------- | ----------- | -| What triggers it | Library events (`BookReserved`) | External events (`HRMemberCreated`) | -| Who owns the trigger event | The Library | The HR system | -| What the reactor knows | Library read models | External payload structure | -| Output | Library domain events | Library domain events | - -Automation reacts to things the Library itself did. Translation reacts to things another system did and maps them into the Library's language. - ---- - -## Step 4 — Wiring the External Event Stream - -Chronicle supports multiple event stream namespaces and external integrations. In your host setup, configure Chronicle to receive events from the HR system's stream: - -```csharp -// Program.cs or Startup.cs -builder.Services - .AddChronicle(chronicle => chronicle - .AddEventTypes<HRMemberCreated>() - // Additional configuration for external stream connection - ); -``` - -The exact configuration depends on how the external system publishes events (Kafka, Azure Service Bus, a Chronicle-to-Chronicle bridge, etc.). The key point is that once events flow in from the HR stream and are typed as `HRMemberCreated`, Chronicle delivers them to `MemberImportReactor.HandleHRMemberCreated` automatically. - ---- - -## Summary - -| Layer | Artifact | Technology | -| ----- | -------- | ---------- | -| External event (HR mirror) | `HRMemberCreated` | [Chronicle](/chronicle/) [`[EventType]`](/chronicle/events/) (integration type) | -| Domain command | `RegisterMember` | [Arc](/arc/) [`[Command]`](/arc/backend/commands/model-bound/) | -| Domain event | `MemberRegistered` | [Chronicle](/chronicle/) [`[EventType]`](/chronicle/events/) | -| Translator | `MemberImportReactor` | [Chronicle](/chronicle/) [`IReactor`](/chronicle/reactors/) + `[OnceOnly]` | -| Bridge | `ICommandPipeline.Execute(...)` | [Arc](/arc/) [command pipeline](/arc/backend/commands/command-pipeline/) | - -The HR system's vocabulary stops at the edge of `HRIntegration/`. Everything inside `Registration/` is pure Library domain, ignorant of HR entirely. Swap the HR system for a different one and you only touch `HRIntegration.cs`. - ---- - -## What You Have Built - -Over the four tutorials in this series you have built: - -| Tutorial | Pattern | What you built | -| -------- | ------- | -------------- | -| [State Change](../state-change) | State Change | Register an author — command, event, validation, constraint, React form | -| [State View](../state-view) | State View | List authors — reactive projection, observable query, live data page | -| [Automation](../automation) | Automation | Cancel expired reservations — passive read model, DCB command, reactor | -| Translation | Translation | Import members from HR — external event mirror, domain command, translator reactor | - -Four patterns. Three building blocks. One coherent framework that covers every layer — [Chronicle](/chronicle/) for the event log, [Arc](/arc/) for the application model, [Components](/components/) for the UI. This is how consistent Information Systems are built on the Cratis stack. diff --git a/web/src/content/docs/security.mdx b/web/src/content/docs/security.mdx deleted file mode 100644 index d9c91f9b..00000000 --- a/web/src/content/docs/security.mdx +++ /dev/null @@ -1,41 +0,0 @@ ---- -title: Security -description: How to report Cratis security vulnerabilities and what to include in a responsible disclosure. ---- - -import { CardGrid, LinkCard, Aside } from '@astrojs/starlight/components'; - -Security reports need a private path. Please do not report suspected vulnerabilities in public GitHub issues, pull requests, Discord threads, or public discussions. - -<CardGrid> - <LinkCard title="Report a vulnerability" description="Email the maintainers with the affected product, impact, reproduction steps, and any proof of concept." href="mailto:oss@cratis.io?subject=Security%20report%20for%20Cratis" /> - <LinkCard title="Check production guidance" description="Review the operational checklist for TLS, secrets, storage, observability, and deployment." href="/production-readiness/" /> - <LinkCard title="Browse product repositories" description="Find the repository that owns the affected code once a fix is ready to be coordinated." href="https://github.com/cratis" /> -</CardGrid> - -## Report privately - -Use the private vulnerability reporting feature in the affected GitHub repository if it is enabled. If you are not sure where the issue belongs, email [oss@cratis.io](mailto:oss@cratis.io?subject=Security%20report%20for%20Cratis) with a subject that starts with `Security:`. - -Include: - -- The affected product: Chronicle, Arc, Components, CLI, Fundamentals, AuthProxy, Studio, tools, or documentation. -- The affected version, Docker image tag, package version, branch, or commit if known. -- The kind of issue: authentication, authorization, data exposure, remote execution, injection, dependency vulnerability, unsafe default, or another category. -- Reproduction steps and any configuration required to trigger the issue. -- Impact: what an attacker can read, change, disrupt, or escalate. -- Any proof of concept, logs, screenshots, or sample repository you can safely share. - -## What not to share publicly - -Do not post exploit details, secrets, tokens, customer data, private logs, or reproduction repositories in public channels. If you have already opened a public issue, remove sensitive details and email the maintainers with a link to the issue. - -## Supported versions - -Security fixes normally target actively maintained releases and the current development line. If a vulnerability affects an older release, include the version you are using so we can assess the practical fix path. - -For package and runtime compatibility, see [Version compatibility](/compatibility/). For operational hardening, see [Production readiness](/production-readiness/). - -<Aside type="note" title="No bounty program"> -Cratis does not currently run a bug bounty program. We still appreciate responsible disclosure and will coordinate fixes as quickly as practical. -</Aside> diff --git a/web/src/content/docs/showcase.mdx b/web/src/content/docs/showcase.mdx deleted file mode 100644 index c3261106..00000000 --- a/web/src/content/docs/showcase.mdx +++ /dev/null @@ -1,33 +0,0 @@ ---- -title: Showcase and architectures -description: Runnable samples and reference architecture shapes for adopting Chronicle, Arc, Components, AuthProxy, CLI, and the full Cratis stack. ---- - -import { CardGrid, LinkCard } from '@astrojs/starlight/components'; - -The best way to understand Cratis is to follow a working shape. This page collects runnable samples and architecture patterns that show how the pieces fit together. - -<CardGrid> - <LinkCard title="Run the samples" description="Clone runnable examples from minimal Chronicle quickstarts to a full-stack library app." href="/samples/" /> - <LinkCard title="Build the full-stack feature" description="Walk through command, event, projection, query, and React screen in one feature." href="/build-a-full-app/" /> - <LinkCard title="Choose your adoption path" description="Use the adoption guide to decide whether to start full stack, Arc-only, Chronicle-only, or one bounded slice." href="/adopting-cratis/" /> -</CardGrid> - -## Reference shapes - -| Shape | Use when | Start with | -|---|---|---| -| Full-stack Cratis | You are building a new information system and want the full command, event, read model, and React loop. | [Build a full-stack feature](/build-a-full-app/) | -| Chronicle-first service | You need a durable event history before you need a frontend. | [Chronicle getting started](/chronicle/get-started/) | -| Arc over current-state storage | You want typed commands, queries, and generated React proxies without event sourcing yet. | [CQRS without event sourcing](/arc/arc-without-event-sourcing/) | -| Components frontend | You already have Arc proxies and want forms, dialogs, tables, and styling conventions. | [Components getting started](/components/getting-started/) | -| Existing system adoption | You want to introduce Cratis one bounded context or frontend/backend boundary at a time. | [Adopting Cratis](/adopting-cratis/) | -| Operational inspection | You need to inspect events, observers, failed partitions, projections, and jobs. | [CLI](/cli/) | - -## Sample applications - -Use the [Samples roster](/samples/) to choose by learning goal, frontend experience, Cratis products, runtime shape, and local prerequisites. Each entry links to the sample-owned README and source. - -## Add your example - -If you have a sample, architecture note, migration story, or teaching example that would help other teams, use [Feedback and suggestions](/feedback/) or open a documentation issue. diff --git a/web/src/content/docs/specifications.mdx b/web/src/content/docs/specifications.mdx deleted file mode 100644 index 15b2ee26..00000000 --- a/web/src/content/docs/specifications.mdx +++ /dev/null @@ -1,263 +0,0 @@ ---- -title: Specifications -description: Write BDD-style executable specifications with Cratis.Specifications, xUnit or NUnit, Establish/Because/Destroy lifecycle methods, readable folder structure, exception capture, and Should assertions. ---- - -import { CardGrid, LinkCard, Aside } from '@astrojs/starlight/components'; - -A good spec should read like the behavior it protects. When a new developer opens the test project, they should not have to reverse-engineer a large `FooTests` file. They should be able to follow the folders: for this class, when this behavior happens, under this condition, these facts should be true. - -`Cratis.Specifications` is the small BDD-style layer we use for that shape. It keeps the familiar xUnit or NUnit runner, but adds the Specification by Example structure: `Establish()` for given, `Because()` for when, `[Fact]` or `[Test]` methods for then, and `Destroy()` for cleanup. - -<CardGrid> - <LinkCard title="Testing with Cratis" description="See how Specifications fit together with Arc command scenarios and Chronicle in-process testing." href="/testing-with-cratis/" /> - <LinkCard title="Specifications source" description="Read the package source and samples in the Cratis Specifications repository." href="https://github.com/Cratis/Specifications" /> - <LinkCard title="xUnit package" description="Use Cratis.Specifications.XUnit for xUnit-based spec projects." href="https://www.nuget.org/packages/Cratis.Specifications.XUnit" /> - <LinkCard title="NUnit package" description="Use Cratis.Specifications.NUnit for NUnit-based spec projects." href="https://www.nuget.org/packages/Cratis.Specifications.NUnit" /> -</CardGrid> - -## Packages - -| Package | Use it for | -|---|---| -| `Cratis.Specifications` | Shared lifecycle discovery and helpers such as `Catch.Exception`. | -| `Cratis.Specifications.XUnit` | xUnit `Specification` base class plus `Should*` assertion extensions over xUnit assertions. | -| `Cratis.Specifications.NUnit` | NUnit `Specification` base class plus `Should*` assertion extensions over NUnit assertions. | - -Most Cratis repositories use central package versions. In that setup, a spec project only needs the package reference: - -```xml -<ItemGroup> - <PackageReference Include="Cratis.Specifications.XUnit" /> -</ItemGroup> -``` - -Use the NUnit package when the project runs on NUnit: - -```xml -<ItemGroup> - <PackageReference Include="Cratis.Specifications.NUnit" /> -</ItemGroup> -``` - -Both packages use the `Cratis.Specifications` namespace. - -## The shape - -The lifecycle methods are discovered by convention. They are optional, take no arguments, and can be `void` or `Task`. - -| Method | BDD word | Purpose | -|---|---|---| -| `Establish()` | Given | Build the context: inputs, services, mocks, existing state. | -| `Because()` | When | Perform the single behavior under specification. | -| `[Fact]` or `[Test]` | Then | Assert one fact about the result. | -| `Destroy()` | Cleanup | Release resources created by the spec. | - -The base class runs lifecycle methods across the inheritance chain, so reusable contexts can live in `given/` classes and still participate in the same `Establish()` / `Because()` flow. - -<Aside type="tip" title="One action per spec"> -Put the behavior under test in `Because()`, store the result in a field, and let each fact assert one thing. If you need a different action, create another `when_...` spec. -</Aside> - -## xUnit example - -Start with the behavior. A security service authenticates a user and returns a token with the user's role and session id. - -```csharp -using Cratis.Specifications; -using Xunit; - -namespace MyApp.Security.for_SecurityService; - -public class when_authenticating_an_admin_user : Specification -{ - SecurityService _subject = default!; - UserToken _result = default!; - - void Establish() => - _subject = new SecurityService(); - - void Because() => - _result = _subject.Authenticate("admin", "correct-password"); - - [Fact] void should_indicate_the_users_role() => - _result.Role.ShouldEqual(Roles.Admin); - - [Fact] void should_have_a_session_id() => - _result.SessionId.ShouldNotBeNull(); -} -``` - -The class name, folder path, lifecycle methods, and facts form the sentence: - -`for_SecurityService / when_authenticating_an_admin_user / should_indicate_the_users_role`. - -## Exception specs - -Use `Catch.Exception` when the behavior is expected to fail. That keeps the exception as the observable result of `Because()`. - -```csharp -using Cratis.Specifications; -using Xunit; - -namespace MyApp.Security.for_SecurityService; - -public class when_authenticating_without_a_user : Specification -{ - SecurityService _subject = default!; - Exception _result = default!; - - void Establish() => - _subject = new SecurityService(); - - void Because() => - _result = Catch.Exception(() => _subject.Authenticate(null!, null!)); - - [Fact] void should_require_a_user() => - _result.ShouldBeOfExactType<UserMustBeSpecified>(); -} -``` - -For async behavior, `Catch.Exception(Func<Task>)` returns the exception from the awaited callback. - -## Reusable contexts - -When several specs share setup, move the setup into a `given/` context and inherit from it. - -```text -for_SecurityService/ - given/ - no_user_authenticated.cs - when_authenticating_an_admin_user.cs - when_authenticating_without_a_user.cs -``` - -```csharp -using Cratis.Specifications; - -namespace MyApp.Security.for_SecurityService.given; - -public class no_user_authenticated : Specification -{ - protected SecurityService _subject = default!; - - void Establish() => - _subject = new SecurityService(); -} -``` - -```csharp -using Cratis.Specifications; -using Xunit; - -namespace MyApp.Security.for_SecurityService; - -public class when_authenticating_without_a_user : given.no_user_authenticated -{ - Exception _result = default!; - - void Because() => - _result = Catch.Exception(() => _subject.Authenticate(null!, null!)); - - [Fact] void should_require_a_user() => - _result.ShouldBeOfExactType<UserMustBeSpecified>(); -} -``` - -Use reusable contexts for meaningful givens, not just to avoid a few repeated lines. The folder should still read like documentation. - -## NUnit differences - -The BDD shape is the same with NUnit. The visible difference is the assertion method attribute. - -```csharp -using Cratis.Specifications; -using NUnit.Framework; - -namespace MyApp.Security.for_SecurityService; - -public class when_authenticating_an_admin_user : Specification -{ - SecurityService _subject = default!; - UserToken _result = default!; - - void Establish() => - _subject = new SecurityService(); - - void Because() => - _result = _subject.Authenticate("admin", "correct-password"); - - [Test] public void should_indicate_the_users_role() => - _result.Role.ShouldEqual(Roles.Admin); -} -``` - -| xUnit | NUnit | -|---|---| -| Reference `Cratis.Specifications.XUnit`. | Reference `Cratis.Specifications.NUnit`. | -| Facts use `[Fact]`. | Facts use `[Test]`. | -| The package integrates with xUnit through `IAsyncLifetime`. | The package integrates with NUnit through `[TestFixture]`, `[OneTimeSetUp]`, and `[OneTimeTearDown]`. | -| Fact methods can be non-public in the Cratis style. | NUnit test methods should be `public`. | - -## Assertions - -The framework-specific packages expose assertion extension methods with the same names, so the spec reads the same whether it runs on xUnit or NUnit. - -| Assertion | Use it for | -|---|---| -| `ShouldEqual(expected)` / `ShouldNotEqual(expected)` | Value equality. | -| `ShouldBeTrue()` / `ShouldBeFalse()` | Boolean facts. | -| `ShouldBeNull()` / `ShouldNotBeNull()` | Null checks. | -| `ShouldBeSame(expected)` / `ShouldNotBeSame(expected)` | Reference identity. | -| `ShouldBeOfExactType<T>()` | Exact runtime type. | -| `ShouldBeAssignableFrom<T>()` | Assignable runtime type. | -| `ShouldContain(...)` / `ShouldNotContain(...)` | Strings, collections, and dictionaries. | -| `ShouldBeEmpty()` / `ShouldNotBeEmpty()` | Collections. | -| `ShouldContainSingleItem()` | Collections that should have exactly one item. | -| `ShouldBeGreaterThan(...)`, `ShouldBeLessThan(...)`, and range helpers | Comparable values. | - -Use raw framework assertions only when there is no `Should*` helper for the fact you need. - -## Naming and folders - -Specs favor readable names over normal production-code naming conventions. - -| Level | Pattern | Example | -|---|---|---| -| Unit folder | `for_<TypeOrConcept>` | `for_SecurityService` | -| Shared context | `given/<context>.cs` | `given/no_user_authenticated.cs` | -| Behavior folder | `when_<action>` | `when_authenticating` | -| Condition file | `and_<condition>.cs` or `with_<state>.cs` | `and_the_user_is_an_admin.cs` | -| Simple behavior file | `when_<action>.cs` | `when_authenticating_without_a_user.cs` | -| Fact method | `should_<outcome>` | `should_require_a_user` | - -This naming deliberately triggers some analyzer and style warnings. Spec projects commonly suppress warnings such as underscore naming and missing XML documentation for spec classes. - -## When to use Specifications - -Use Specifications when the behavior benefits from an executable sentence: - -- Business rules. -- Domain services. -- Command handlers and validation. -- Event-sourced slices. -- Projection, reducer, and reactor behavior. -- Edge cases that future readers need to understand. - -Use plain xUnit or NUnit tests when the test is pure infrastructure smoke, generated-code verification, or a very small assertion where the BDD structure would add noise. - -For Cratis application tests, Specifications are usually the outer shape, while Arc and Chronicle provide the scenario objects inside it: - -- `CommandScenario<TCommand>` for Arc command behavior. -- `EventScenario` for event append and constraint behavior. -- `ReadModelScenario<TReadModel>` for projections and reducers. -- `ReactorScenario<TReactor>` for event-driven side effects. - -## Where to go next - -<CardGrid> - <LinkCard title="Testing with Cratis" description="Use Specifications with Arc command scenarios, Chronicle in-process scenarios, and full-stack slice specs." href="/testing-with-cratis/" /> - <LinkCard title="Arc testing" description="Drive commands through the real Arc command pipeline without HTTP." href="/arc/backend/testing/" /> - <LinkCard title="Chronicle testing" description="Test events, read models, and reactors in-process." href="/chronicle/testing/" /> - <LinkCard title="Event modeling" description="Turn event-model columns into given/when/then examples." href="/event-modeling/" /> -</CardGrid> diff --git a/web/src/content/docs/studio.mdx b/web/src/content/docs/studio.mdx deleted file mode 100644 index ac3ed712..00000000 --- a/web/src/content/docs/studio.mdx +++ /dev/null @@ -1,83 +0,0 @@ ---- -title: Studio -description: Cratis Studio is a collaborative Event Modeling and Event Storming platform — design your domain on a shared canvas, align your team, and generate type-safe C# straight from the model. Coming soon. -tableOfContents: false ---- - -import TopicHero from '@components/TopicHero.astro'; -import SimpleCard from '@components/SimpleCard.astro'; -import StackDiagram from '@components/StackDiagram.astro'; -import { Aside } from '@astrojs/starlight/components'; - -<TopicHero - eyebrow="Studio · Coming soon" - title="Model your domain. Ship with confidence." - tagline="Cratis Studio is a collaborative platform for event modeling — align your whole team around how your system behaves before a single line of production code is written, then generate type-safe C# straight from the model." - primaryHref="https://cratis.studio" - primaryText="Visit cratis.studio" - secondaryHref="/why-cratis/" - secondaryText="Why choose Cratis?" -/> - -<Aside type="note" title="Coming soon"> - Studio is in active development. This page previews where it's headed — visit - [cratis.studio](https://cratis.studio) to follow along. Studio stands on its own as a design tool; - when you build on the Cratis Stack, it closes the loop by generating the code your model describes. -</Aside> - -## Design before you build - -Most teams write the code first and discover the domain later — by the time everyone finally agrees on -what "an order" really is, half of it is already built the wrong way. Event modeling flips that around: -you map out *what happens* in the system — the commands people issue, the events those produce, the read -models people look at — as a shared picture, **before** committing anyone to an implementation. - -Studio is the canvas for that conversation. Developers, domain experts, and product owners shape the same -living model together, in real time — no merge conflicts, no stale diagrams rotting in a wiki. - -## What Studio will do - -<div class="sl-card-grid"> - <SimpleCard icon="🧠" title="Event Brainstorming"> - Run a digital Event Storming session — drop sticky notes on an infinite canvas, cluster ideas, and - discover the events that really drive your domain. - </SimpleCard> - <SimpleCard icon="🗺️" title="Event Modeling"> - Lay out commands, events, and read models on a collaborative timeline, and visualize every state - change in your system before you write a line of code. - </SimpleCard> - <SimpleCard icon="🔗" title="Read model projections"> - Connect events to read models on the canvas, so everyone can see exactly which events feed every - projection — by design, not by archaeology. - </SimpleCard> - <SimpleCard icon="👥" title="Team collaboration"> - Invite developers, domain experts, and product owners to edit the same model in real time. One - shared source of truth, always current. - </SimpleCard> - <SimpleCard icon="⚙️" title="Code generation"> - Generate type-safe C# commands, events, and projections directly from your model — so your design - and your Cratis codebase stay perfectly in step. - </SimpleCard> - <SimpleCard icon="🧩" title="Spec-driven development"> - The model *is* the spec. Start from a shared design, generate the slices, and build on a foundation - the whole team already agreed on. - </SimpleCard> -</div> - -## Where it fits - -Studio is an **independent top layer** — a design surface you can use on its own to model any event-driven -system. Used *with* the Cratis Stack, it closes the loop: the commands, events, and projections you model -become the type-safe C# you build on with [Arc](/arc/) and [Chronicle](/chronicle/). - -<StackDiagram current="studio" /> - -It's the *front* of the development funnel — design and align in Studio, generate the slices, then build -them out. To *inspect and operate* a system that's already running, you reach for the [CLI](/cli/) and -Workbench instead. - -## Until then - -- Visit [cratis.studio](https://cratis.studio) to follow Studio's progress. -- New to event modeling? Start with [Why developers choose Cratis](/why-cratis/) and the [Chronicle tutorial](/chronicle/tutorial/). -- Want to model the Cratis way by hand today? See [vertical slices](/arc/vertical-slices/) and [modeling events](/chronicle/concepts/modeling-events/). diff --git a/web/src/content/docs/testing-with-cratis.mdx b/web/src/content/docs/testing-with-cratis.mdx deleted file mode 100644 index 42213d17..00000000 --- a/web/src/content/docs/testing-with-cratis.mdx +++ /dev/null @@ -1,226 +0,0 @@ ---- -title: Testing with Cratis -description: How Cratis Specifications, Arc command scenarios, and Chronicle in-process scenarios turn event models into fast BDD-style tests. ---- - -import { CardGrid, Aside } from '@astrojs/starlight/components'; -import SimpleCard from '@components/SimpleCard.astro'; -import TopicHero from '@components/TopicHero.astro'; - -<TopicHero icon="approve-check" eyebrow="Testing" title="Turn the model into executable examples"> -Cratis applications already describe behavior as a timeline: a screen sends a command, the command records an event, projections build read models, and reactors act on facts. The testing story follows the same shape. You write a small specification for the behavior, seed the facts that already happened, perform the command or event, and assert the new facts, read models, or side effects. -</TopicHero> - -## The testing stack - -The packages are layered the same way the runtime is layered: - -| Package | Use it for | -|---|---| -| `Cratis.Specifications.XUnit` | BDD-style specifications on top of xUnit: `Establish()` for given, `Because()` for when, `[Fact]` methods for then, plus `Should*` assertions and `Catch.Exception`. | -| `Cratis.Specifications.NUnit` | The same specification style for NUnit projects. | -| `Cratis.Arc.Testing` | `CommandScenario<TCommand>` for running Arc commands through the real command pipeline without HTTP. | -| `Cratis.Chronicle.Testing` | `EventScenario`, `ReadModelScenario<TReadModel>`, and `ReactorScenario<TReactor>` for in-process Chronicle tests without a server or database. | -| `Cratis.Arc.Chronicle.Testing` | Automatically extends `CommandScenario<TCommand>` with an in-memory Chronicle event log when the package is referenced. | -| `Cratis.Testing` | Convenience package for Arc + Chronicle application tests. Use it with `Cratis.Specifications.XUnit`. | - -For most Cratis stack slices, reference: - -```xml -<PackageReference Include="Cratis.Specifications.XUnit" /> -<PackageReference Include="Cratis.Testing" /> -``` - -Use `Cratis.Chronicle.Testing` directly for Chronicle-only libraries, and `Cratis.Arc.Testing` directly when a slice deliberately has no event-sourced write side. - -## Why BDD fits event sourcing - -The given/when/then shape is not an arbitrary testing style here. It is the same grammar as event sourcing: - -| Specification word | Event-sourced meaning | Cratis test API | -|---|---|---| -| **Given** | Facts that already happened, or read model state that already exists | `EventScenario.Given`, `ReadModelScenario.Given`, reusable `given` base contexts | -| **When** | The command, append, projection input, or reactor trigger under test | `CommandScenario.Execute`, `EventLog.Append`, `ReactorScenario.Given.ForEventSource(...).Events(...)` | -| **Then** | The observable result: appended events, command result, projected read model, or side effect | `[Fact]` methods with `ShouldBeSuccessful`, `ShouldHaveAppendedEvent`, `ShouldEqual`, or mock assertions | - -That makes specs readable in the same language as the event model. A domain expert can read "given an author already exists, when registering the same name, then it should not append another event" and understand the rule without knowing the implementation. - -<Aside type="tip" title="One act, many facts"> -`Specification` runs `Establish()` and `Because()` before each `[Fact]`. Put the behavior under test in `Because()` once, store the result in fields, and let each fact assert one thing about the same behavior. -</Aside> - -## The event model becomes the spec - -Start with one column from an event model: - -```mermaid -eventmodeling - -tf 01 ui Authors.RegisterAuthorScreen -tf 02 cmd Authors.RegisterAuthor { authorId: uuid, name: string } -tf 03 evt Authors.AuthorRegistered { name: string } -tf 04 rmo Authors.Author ->> 03 -tf 05 ui Authors.AuthorsScreen ->> 04 -``` - -Read it as a testable behavior: - -| Given | When | Then | -|---|---|---| -| no author exists for this id | `RegisterAuthor` is executed | the command succeeds | -| no author exists for this id | `RegisterAuthor` is executed | `AuthorRegistered` is appended for the author id | -| `AuthorRegistered` exists | the projection handles it | the `Author` read model contains the name the screen needs | - -The slice can stay small: - -```csharp -using Cratis.Arc.Commands; -using Cratis.Chronicle.Events; -using Cratis.Chronicle.Keys; -using Cratis.Chronicle.Projections; -using Cratis.Chronicle.ReadModels; - -[Command] -public record RegisterAuthor([property: Key] EventSourceId AuthorId, string Name) -{ - public AuthorRegistered Handle() => new(Name); -} - -[EventType] -public record AuthorRegistered(string Name); - -[ReadModel] -[FromEvent<AuthorRegistered>] -public record Author([property: Key] EventSourceId Id, string Name); -``` - -And the stack spec follows the same order: - -```csharp -using Cratis.Arc.Chronicle.Testing.Commands; -using Cratis.Arc.Commands; -using Cratis.Arc.Testing.Commands; -using Cratis.Chronicle.Events; -using Cratis.Chronicle.Testing.ReadModels; -using Cratis.Specifications; -using Xunit; - -public class when_registering_a_new_author : Specification -{ - readonly EventSourceId _authorId = EventSourceId.New(); - readonly CommandScenario<RegisterAuthor> _command = new(); - readonly ReadModelScenario<Author> _projection = new(); - CommandResult _result = default!; - - async Task Because() - { - _result = await _command.Execute(new RegisterAuthor(_authorId, "Jane Austen")); - - await _projection.Given - .ForEventSource(_authorId) - .Events(_command.AppendedEvents.Select(_ => _.Event.Content).ToArray()); - } - - [Fact] void should_accept_the_command() => - _result.ShouldBeSuccessful(); - - [Fact] Task should_record_the_fact() => - _command.ShouldHaveAppendedEvent<RegisterAuthor, AuthorRegistered>( - _authorId, - e => e.Name == "Jane Austen"); - - [Fact] void should_project_the_author_for_the_screen() => - _projection.Instance!.Name.ShouldEqual("Jane Austen"); -} -``` - -There is no HTTP server in that spec and no Chronicle server to start. Arc runs the command through the real command pipeline. The Chronicle extension captures the events appended by the command. The read model scenario then feeds those event contents through the projection in-process, so the final assertion checks the same state the React query would read. - -## How to test Arc - -Use Arc tests for command behavior: validation, authorization, injected services, and whether the command succeeds. `CommandScenario<TCommand>` builds the real Arc command pipeline lazily, so you register services before `Because()` calls `Execute()` or `Validate()`. - -```csharp -public class when_adding_item_to_cart : Specification -{ - readonly IInventoryService _inventory = Substitute.For<IInventoryService>(); - readonly CommandScenario<AddItemToCart> _scenario = new(); - CommandResult _result = default!; - - void Establish() - { - _inventory.IsInStock("SKU-123").Returns(true); - _scenario.Services.AddSingleton(_inventory); - } - - async Task Because() => - _result = await _scenario.Execute(new AddItemToCart("SKU-123", 2)); - - [Fact] void should_succeed() => - _result.ShouldBeSuccessful(); -} -``` - -Use `Validate()` when you want validation and authorization without running the handler. Use the Chronicle extension when the command returns events and you want to assert which facts were recorded. - -## How to test Chronicle - -Chronicle tests are fastest when you test the part of the event pipeline you own: - -| You need to prove | Use | -|---|---| -| Code appends the right event, or rejects an append because of constraints | `EventScenario` | -| A projection or reducer builds the read model correctly | `ReadModelScenario<TReadModel>` | -| A reactor performs the right side effect when an event arrives | `ReactorScenario<TReactor>` | - -For example, a projection spec is just given events, then read model state: - -```csharp -public class when_projecting_a_registered_author : Specification -{ - readonly EventSourceId _authorId = EventSourceId.New(); - readonly ReadModelScenario<Author> _scenario = new(); - - Task Because() => - _scenario.Given - .ForEventSource(_authorId) - .Events(new AuthorRegistered("Jane Austen")); - - [Fact] void should_set_the_author_name() => - _scenario.Instance!.Name.ShouldEqual("Jane Austen"); -} -``` - -Use a real Chronicle integration test when you are testing hosting, storage, subscriptions, observer recovery, or the boundary between processes. Keep the bulk of slice behavior in the in-process scenarios so the suite stays fast. - -## How to test a Cratis stack slice - -A full slice usually has three useful spec levels: - -| Level | What it proves | Typical tool | -|---|---|---| -| Command spec | The command validates, authorizes, and records the right facts | `CommandScenario<TCommand>` + `Cratis.Arc.Chronicle.Testing` | -| Read side spec | The event history builds the read model the screen needs | `ReadModelScenario<TReadModel>` | -| Workflow spec | The app boundary, identity, tenant routing, real Chronicle host, and projections work together | xUnit fixture + `Cratis.Specifications.XUnit` + HTTP/client helpers | - -Start with command and read-side specs for every meaningful event-model column. Add workflow specs around the edges where the wiring matters: AuthProxy headers, tenant isolation, a real Chronicle server, or browser-visible behavior. - -## Where to go next - -<CardGrid> - <SimpleCard title="Specifications" icon="approve-check" link="/specifications/"> - Understand the BDD package itself: lifecycle methods, xUnit and NUnit packages, exception capture, assertions, and spec folder structure. - </SimpleCard> - <SimpleCard title="Arc testing" icon="puzzle" link="/arc/backend/testing/"> - Test commands through the Arc command pipeline, with or without Chronicle. - </SimpleCard> - <SimpleCard title="Chronicle testing" icon="seti:db" link="/chronicle/testing/"> - Test events, read models, and reactors in-process. - </SimpleCard> - <SimpleCard title="Event modeling" icon="list-format" link="/event-modeling/"> - Draw the flow that becomes the specification. - </SimpleCard> - <SimpleCard title="Build the full slice" icon="rocket" link="/build-a-full-app/"> - See the command, event, read model, and React screen together. - </SimpleCard> -</CardGrid> diff --git a/web/src/content/docs/tools/lens.mdx b/web/src/content/docs/tools/lens.mdx deleted file mode 100644 index 23d1a522..00000000 --- a/web/src/content/docs/tools/lens.mdx +++ /dev/null @@ -1,72 +0,0 @@ ---- -title: Lens (browser extension) -description: Lens is a browser extension that brings Cratis developer tools into your browser — detect a running Arc app, browse its commands and queries by namespace, execute them with structured input, and switch the active user and tenant, all from a popup. ---- - -import { CardGrid, Steps } from '@astrojs/starlight/components'; -import SimpleCard from '@components/SimpleCard.astro'; -import TopicHero from '@components/TopicHero.astro'; - -<TopicHero icon="seti:html" eyebrow="Tools" title="Your Arc app, inspectable in the browser"> -You're testing an Arc app in the browser and you want to fire a command with a specific payload, or check what a query returns for a different tenant — without writing a throwaway page. **Lens** is a browser extension that does exactly that: it detects the running Arc app in your current tab, lets you browse its commands and queries by namespace, run them with a structured form, and switch the active user and tenant on the fly. -</TopicHero> - -## What it does - -Lens opens as a popup with four tabs. When the current tab is a running Arc application, it detects it automatically and lights up the command and query browsers. - -<CardGrid> - <SimpleCard title="Commands" icon="rocket"> - Browse the app's commands by namespace and execute them with a schema-driven form; see the formatted result or validation errors. - </SimpleCard> - <SimpleCard title="Queries" icon="list-format"> - Browse queries by namespace and perform them, with results shown in a data table — no scratch UI needed. - </SimpleCard> - <SimpleCard title="Context" icon="seti:lock"> - Choose the active user and tenant for this browser profile; Lens applies them as request headers when it calls the app. - </SimpleCard> - <SimpleCard title="Settings" icon="right-arrow"> - Manage users and tenants by hand, or sync them straight from the connected Arc app, and set the tenant header name. - </SimpleCard> -</CardGrid> - -Under the hood it's a Manifest V3 extension built with React, Vite, and PrimeReact — the same UI stack as [Components](/components/) — and it talks to the Arc backend's introspection endpoints to discover the command and query catalog. - -## Install from the Chrome Web Store - -The easiest way to install Lens is directly from the [Chrome Web Store](https://chromewebstore.google.com/detail/cratis-lens/hfdpoljbaknodcgejjohafbfjejijklj). Click **Add to Chrome** and the extension is ready to use. - -## Install it (side-load) - -You can also build Lens from source and load it unpacked — useful for development or trying unreleased changes. The source is at [github.com/Cratis/Lens](https://github.com/Cratis/Lens). - -<Steps> - -1. Clone the repo, then from the `Source` folder install and build: - - ```bash - cd Source - yarn - yarn build - ``` - - This produces `Source/dist/` (including `manifest.json`). - -2. Open `chrome://extensions`, enable **Developer mode**, choose **Load unpacked**, and select the `Source/dist` folder. Lens appears as **Lens – Cratis Developer Tools**. - -3. Navigate to your running Arc app, open the Lens popup once so it detects the app, then open its settings, confirm the **Tenant Header Name**, and save. - -</Steps> - -For an active edit loop, run `yarn dev` to rebuild on change, and reload the extension from `chrome://extensions` after each rebuild. - -## Where to go next - -<CardGrid> - <SimpleCard title="Queries in Arc" icon="puzzle" link="/arc/backend/queries/"> - What Lens is browsing — how Arc exposes queries and observable queries over your read models. - </SimpleCard> - <SimpleCard title="VS Code extension (Narrator)" icon="laptop" link="/tools/vscode-extension/"> - The editor-side companion — explore the Chronicle event store from VS Code. - </SimpleCard> -</CardGrid> diff --git a/web/src/content/docs/tools/vscode-extension.mdx b/web/src/content/docs/tools/vscode-extension.mdx deleted file mode 100644 index 0894495f..00000000 --- a/web/src/content/docs/tools/vscode-extension.mdx +++ /dev/null @@ -1,80 +0,0 @@ ---- -title: VS Code extension (Narrator) -description: Narrator is the Cratis VS Code extension — a Chronicle Explorer in your editor's activity bar. Browse event stores, namespaces, and observers, page through an event sequence, and open event and read-model schemas without leaving your code. ---- - -import { CardGrid, Aside, Steps } from '@astrojs/starlight/components'; -import SimpleCard from '@components/SimpleCard.astro'; -import TopicHero from '@components/TopicHero.astro'; - -<TopicHero icon="laptop" eyebrow="Tools" title="Explore Chronicle from your editor"> -When you're deep in a slice and something looks wrong, the question is usually "what's actually *in* the event log?" **Narrator** answers it without a context switch: it adds a Chronicle Explorer to the VS Code activity bar, so you can browse your event store, page through a sequence, and inspect event and read-model schemas right next to the code you're writing. -</TopicHero> - -## Install it - -Narrator is published on the Visual Studio Marketplace as **[Narrator – Chronicle Explorer](https://marketplace.visualstudio.com/items?itemName=cratis.narrator)** (publisher `cratis`). - -<Steps> - -1. Open the **Extensions** view in VS Code (`⇧⌘X` / `Ctrl+Shift+X`). - -2. Search for **Narrator** and install **Narrator – Chronicle Explorer** by Cratis — or install it from the [Marketplace listing](https://marketplace.visualstudio.com/items?itemName=cratis.narrator). - -3. Open the **Narrator** icon in the activity bar. It connects to your active Chronicle context on startup. - -</Steps> - -The source lives at [github.com/Cratis/Narrator](https://github.com/Cratis/Narrator). - -## How it connects - -Narrator reads the **same configuration the [CLI](/cli/getting-started/) uses** — `~/.cratis/config.json` — so if you've already pointed the CLI at a store, the extension just works. A config looks like: - -```json -{ - "activeContext": "default", - "contexts": { - "default": { - "server": "chronicle://localhost:35000", - "managementPort": 8080 - } - } -} -``` - -It auto-connects to the active context on startup and watches the file, reloading when it changes. To use a config in a non-standard location, set the `narrator.configPath` setting; leave it empty to auto-detect from `~/.cratis/config.json`. - -## What you can do - -<CardGrid> - <SimpleCard title="Browse the store" icon="seti:db"> - Navigate event stores, namespaces, and observers in the Chronicle Explorer tree. - </SimpleCard> - <SimpleCard title="Page through events" icon="list-format"> - Open a sequence (Event Log, Outbox, System) into a paged event viewer with keyboard navigation. - </SimpleCard> - <SimpleCard title="Inspect schemas" icon="seti:json"> - Open an event type's or read model's JSON schema, and open a projection's declaration. - </SimpleCard> - <SimpleCard title="Switch contexts" icon="right-arrow"> - Add, edit, and switch between multiple Chronicle server contexts without editing JSON by hand. - </SimpleCard> -</CardGrid> - -A **Details** pane sits below the explorer and shows the selected node's context and content as key/value tables — so selecting an event or read model shows its data inline. - -<Aside type="note" title="Inspect, not mutate"> -Like the CLI and the [Chronicle MCP server](/ai/), Narrator is an *explore-and-inspect* tool. It reads the log and schemas; it doesn't change application state. To change state you still go through commands and events. -</Aside> - -## Where to go next - -<CardGrid> - <SimpleCard title="CLI getting started" icon="rocket" link="/cli/getting-started/"> - Set up the `~/.cratis/config.json` contexts Narrator reads, and operate the store from the terminal. - </SimpleCard> - <SimpleCard title="Lens browser extension" icon="seti:html" link="/tools/lens/"> - The in-browser companion — browse and run an Arc app's commands and queries from your browser. - </SimpleCard> -</CardGrid> diff --git a/web/src/content/docs/whats-new.mdx b/web/src/content/docs/whats-new.mdx deleted file mode 100644 index 9b6442d0..00000000 --- a/web/src/content/docs/whats-new.mdx +++ /dev/null @@ -1,33 +0,0 @@ ---- -title: "What's new" -description: Curated release digests and links to Cratis product releases across Chronicle, Arc, Components, CLI, Fundamentals, AuthProxy, and tools. ---- - -import { CardGrid, LinkCard } from '@astrojs/starlight/components'; - -Cratis moves across several repositories, so a single product release page rarely tells the whole story. The release digests collect changes across the stack and link back to the source releases and pull requests. - -<CardGrid> - <LinkCard title="Latest release digest" description="Changes collected from May 25, 2026 to June 1, 2026." href="/contributing/release-digests/2026-05-25-to-2026-06-01/" /> - <LinkCard title="Previous release digest" description="Changes collected from May 18, 2026 to May 25, 2026." href="/contributing/release-digests/2026-05-18-to-2026-05-25/" /> - <LinkCard title="Roadmap" description="What is stable, what is being hardened, what is being explored, and where feedback helps." href="/roadmap/" /> -</CardGrid> - -## Product releases - -| Product | Releases | -|---|---| -| Chronicle | [Chronicle releases](https://github.com/Cratis/Chronicle/releases) | -| Arc | [Arc releases](https://github.com/Cratis/Arc/releases) | -| Components | [Components releases](https://github.com/Cratis/Components/releases) | -| CLI | [CLI releases](https://github.com/Cratis/cli/releases) | -| Fundamentals | [Fundamentals releases](https://github.com/Cratis/Fundamentals/releases) | -| AuthProxy | [AuthProxy releases](https://github.com/Cratis/AuthProxy/releases) | -| Samples | [Samples repository](https://github.com/Cratis/Samples) | - -## How to read changes - -- Use release digests when you want the cross-stack picture. -- Use product releases when you need exact package tags or repository-specific details. -- Use [Version compatibility](/compatibility/) when deciding which runtime and package versions belong together. -- Use [Feedback and suggestions](/feedback/) when a release note leaves an adoption question unanswered. diff --git a/web/src/content/docs/why-cratis.mdx b/web/src/content/docs/why-cratis.mdx deleted file mode 100644 index a8be5f8f..00000000 --- a/web/src/content/docs/why-cratis.mdx +++ /dev/null @@ -1,219 +0,0 @@ ---- -title: Why developers choose Cratis -description: "The practical reasons teams choose Cratis: one stack that fits together, strong conventions, open Chronicle boundaries, storage choice, full-stack contracts, end-to-end foundations, and codebases that stay predictable for people and AI." ---- - -import { CardGrid } from '@astrojs/starlight/components'; -import SimpleCard from '@components/SimpleCard.astro'; -import TopicHero from '@components/TopicHero.astro'; - -<TopicHero icon="open-book" eyebrow="Why developers choose Cratis" title="Build the system you modeled"> -Developers choose Cratis when they want the domain model, backend, frontend, identity boundary, UI, and operations view to line up. Chronicle, Arc, Components, AuthProxy, the CLI, and the AI tooling are opinionated in the same direction: write the behavior once, keep the contract typed, choose the right storage, handle the hard cross-cutting concerns, and make the running system inspectable. [Get started →](/chronicle/get-started/) · [Build a full-stack feature →](/build-a-full-app/) -</TopicHero> - -## The developer payoff - -<CardGrid> - <SimpleCard title="The pieces fit together" icon="puzzle" link="/cratis-stack/"> - Chronicle, Arc, Components, AuthProxy, Studio, and the tools use the same domain language and conventions, so the stack feels designed as one platform. - </SimpleCard> - <SimpleCard title="Less glue between layers" icon="approve-check" link="/arc/understanding-the-proxy-boundary/"> - Commands and queries become HTTP endpoints and generated TypeScript proxies. You do not hand-write controllers, DTO mirrors, or fetch wrappers for every feature. - </SimpleCard> - <SimpleCard title="A compiler-enforced boundary" icon="seti:typescript" link="/arc/understanding-the-proxy-boundary/"> - The frontend imports code generated from the backend. If C# changes, TypeScript changes with it, and drift becomes a build error. - </SimpleCard> - <SimpleCard title="Predictable by convention" icon="approve-check" link="/code-analysis/"> - Cratis is opinionated about names, folders, attributes, and discovery. The conventions make codebases consistent, and analyzers catch convention drift at build time. - </SimpleCard> - <SimpleCard title="A feature you can read top to bottom" icon="seti:folder" link="/arc/vertical-slices/"> - A slice keeps intent, state, screen, and specs together. You change one behavior without spelunking through layers. - </SimpleCard> - <SimpleCard title="End-to-end foundations" icon="seti:lock" link="/authproxy/"> - Authentication, tenant resolution, identity enrichment, authorization, and tenant-isolated data are first-class parts of the platform, not scattered application boilerplate. - </SimpleCard> - <SimpleCard title="History you can trust" icon="seti:db" link="/chronicle/why-event-sourcing/"> - We treat event sourcing as the default for information systems. Chronicle records immutable facts and derives read models from them, so audit and replay are part of the design. - </SimpleCard> - <SimpleCard title="Screens that follow the model" icon="laptop" link="/components/"> - Components renders generated commands and observable queries as forms, dialogs, and tables, so UI code stays close to the model. - </SimpleCard> - <SimpleCard title="AI can work with it" icon="rocket" link="/ai/"> - The same conventions that help developers navigate the codebase are packaged as AI skills, rules, and diagnostics, so agents build with the grain of the framework. - </SimpleCard> - <SimpleCard title="Specs read like the model" icon="approve-check" link="/testing-with-cratis/"> - Given/when/then specifications line up with event modeling: existing facts, a command, and the facts or read models that should result. - </SimpleCard> - <SimpleCard title="Operations without guesswork" icon="rocket" link="/cli/"> - Inspect events, observers, read models, replay, and diagnostics from the CLI and Workbench instead of guessing what the runtime is doing. - </SimpleCard> -</CardGrid> - -## The big win: the platform fit - -The point is not that Cratis has many packages. The point is that the packages agree about how an application is shaped. A command, an event, a read model, a tenant, an identity, a React form, and an operating tool are not separate islands with separate conventions. They are parts of one model. - -| Concern | Where Cratis carries it | -|---|---| -| Domain behavior | Studio models it, Arc turns it into commands and queries, Chronicle records the facts as the default source of truth for information systems | -| Frontend contract | Arc generates TypeScript proxies from C#, and Components renders those proxies as forms, dialogs, and live tables | -| Chronicle client boundary | Chronicle exposes gRPC/protobuf contracts, with .NET as the first-class client and TypeScript and Elixir clients/contracts for other runtimes | -| Chronicle storage | MongoDB, PostgreSQL, Microsoft SQL Server, and SQLite implementations let the event store fit the deployment | -| Runtime scale | Chronicle runs its kernel on .NET Orleans, so event sequences, observers, jobs, and long-running processing live on a distributed actor runtime | -| Authentication and tenant resolution | AuthProxy handles the edge and forwards trusted identity and tenant context into the app | -| Authorization | Arc gates commands and queries at the boundary, with identity details available in backend and React | -| Tenant isolation | Arc tenancy and Chronicle namespaces keep one tenant's state away from another's by construction | -| Operations and observability | Workbench, CLI, OpenTelemetry, recommendations, replay, jobs, failed partitions, and diagnostics expose what the runtime is doing | -| Testing | Cratis Specifications, Arc command scenarios, and Chronicle in-process scenarios turn an event-model column into executable given/when/then specs | -| AI assistance | `.ai` skills, editor rules, analyzers, the CLI catalog, and the Chronicle MCP server give agents the same rails developers use | - -That is why the opinions matter. They make codebases consistent. Consistency makes onboarding faster, reviews sharper, and AI assistance far less speculative. - -## Chronicle as an architecture choice - -Chronicle is more than a .NET package. It is a client-server event platform with an open protocol boundary and a kernel built for long-running event processing. - -<CardGrid> - <SimpleCard title="Language-neutral boundary" icon="puzzle" link="/chronicle/architecture/"> - Chronicle speaks gRPC/protobuf at the kernel boundary. The .NET client is the first-class, most mature experience, and the repo also ships TypeScript and Elixir clients/contracts. - </SimpleCard> - <SimpleCard title="Storage choice" icon="seti:db" link="/chronicle/hosting/configuration/storage/"> - The event store is storage-agnostic: MongoDB by default, with PostgreSQL, Microsoft SQL Server, and SQLite implementations when those fit better. - </SimpleCard> - <SimpleCard title="Built on Orleans" icon="rocket" link="/chronicle/architecture/"> - The kernel is built on .NET Orleans, giving Chronicle a distributed actor foundation for event sequences, observers, jobs, reminders, and recovery. - </SimpleCard> - <SimpleCard title="Operable by design" icon="approve-check" link="/cli/chronicle/"> - Workbench, CLI, OpenTelemetry, server recommendations, observer replay, failed partitions, jobs, and read-model inspection are part of the product, not an afterthought. - </SimpleCard> -</CardGrid> - -## What changes day to day - -Cratis is not just a set of libraries. It changes the shape of the work: - -| Instead of... | You work with... | -|---|---| -| Duplicating request and response shapes in C# and TypeScript | One C# command/query model, generated into typed frontend proxies | -| Spreading one feature across controllers, handlers, clients, and UI folders | A vertical slice organized by behavior | -| Rebuilding screens by hand after a write | Observable queries and components that follow the read model | -| Rebuilding authentication, tenancy, and identity plumbing per service | AuthProxy at the edge, Arc identity and tenancy inside the app, Chronicle namespaces underneath | -| Teaching every developer and AI assistant a project-specific architecture from scratch | Strong conventions, analyzers, and `.ai` guidance that make each feature look like the rest | -| Locking event history to one application language or database engine | Chronicle's gRPC boundary, .NET/TypeScript/Elixir clients/contracts, and MongoDB/PostgreSQL/SQL Server/SQLite storage implementations | -| Debugging from logs alone | Events, observers, read models, and replay visible through the tools | -| Treating CQRS and event sourcing as the same decision | Arc for the command/query boundary, Chronicle for the event-sourced backbone — independent, but strongest together | - -The result is a workflow that stays close to the language developers already use: "what happened?", "what state does this screen need?", "what command changes it?", and "what did the runtime do with it?" - -## The products behind it - -<CardGrid> - <SimpleCard title="Chronicle" icon="seti:db" link="/chronicle/"> - The event sourcing platform. gRPC/protobuf at the boundary, .NET-first client experience, storage choice underneath, and Orleans-powered processing inside. - </SimpleCard> - <SimpleCard title="Arc" icon="puzzle" link="/arc/"> - The full-stack framework. Turns commands and queries into a CQRS app and generates TypeScript proxies so React stays in lockstep with C#. - </SimpleCard> - <SimpleCard title="Components" icon="laptop" link="/components/"> - The React library. Command forms, data tables, and dialogs that consume Arc's proxies — a screen is a few lines, not a few files. - </SimpleCard> - <SimpleCard title="AuthProxy" icon="seti:lock" link="/authproxy/"> - The edge gateway. Handles authentication, tenant resolution, identity enrichment, routing, and invite-based onboarding. - </SimpleCard> - <SimpleCard title="Studio" icon="open-book" link="/studio/"> - The modeling surface. Capture the domain shape with the team, then generate type-safe C# from the model. Coming soon. - </SimpleCard> - <SimpleCard title="CLI" icon="rocket" link="/cli/"> - A terminal window into a running store — inspect events, watch observers, and diagnose issues. - </SimpleCard> -</CardGrid> - -## Use them on their own — or together - -The core pieces are built to stand alone. Each solves a complete problem by itself, so you can adopt exactly the part you need and nothing more: - -- **Chronicle on its own** is an event-sourcing engine you can run from *any* .NET host — a worker, a console app, a different web framework. The .NET client is the most mature path, but the kernel boundary is gRPC/protobuf and the repo also ships TypeScript and Elixir clients/contracts. Append events, build projections, react to them. No Arc, no React required. -- **Arc on its own** is a full-stack CQRS framework with **generated, typed C# → TypeScript proxies**. Its commands and queries can persist straight to **MongoDB** or **EF Core / SQL** when you deliberately want CQRS without an event log ([here's the standalone shape](/arc/arc-without-event-sourcing/)). You still get the typed frontend, the command forms, and live queries. -- **Components on its own** is a React library that renders Arc's generated proxies as forms, tables, and dialogs. -- **AuthProxy on its own** is a gateway for authentication, tenancy, identity enrichment, routing, and invites in front of any backend and frontend you point it at. - -We think event sourcing is the default architecture for information systems. We also think CQRS is the right way to shape information going into and out of a system. Those ideas fit extremely well together and are often associated, but they are not co-dependent: you can use Chronicle without Arc, and Arc can run without Chronicle. - -The dependency only runs one way. Arc is a layer that can sit **on top of** Chronicle — but Chronicle never knows Arc exists, which is why each works without the other. AuthProxy sits at the edge and can front the app whether Arc is backed by MongoDB, EF Core, or Chronicle. What the full combination changes is how much of the application is handled by one coherent set of conventions. - -```mermaid -flowchart LR - A["Arc<br/>command / query<br/>+ typed proxies"] - A --> M[("MongoDB")] - A --> E[("EF Core / SQL")] - A --> C[("Chronicle<br/>event sourcing")] - style C stroke-width:3px -``` - -| You want… | Reach for | Event sourcing? | -| --- | --- | --- | -| History as the source of truth, from any backend or client runtime | **Chronicle** on its own | Yes | -| An event store over MongoDB, PostgreSQL, Microsoft SQL Server, or SQLite | **Chronicle** with the matching storage backend | Yes | -| A typed full-stack app over a traditional database | **Arc + Components** over MongoDB / EF Core | Not required | -| Authentication, tenancy, identity enrichment, and routing at the edge | **AuthProxy** in front of your services | Optional | -| A typed full-stack app *and* a full event history | **Arc + Chronicle + Components** — the domain loop | Yes | -| A product-grade SaaS shape with edge, identity, tenant isolation, typed UI, history, and operations | **AuthProxy + Arc + Components + Chronicle + CLI** | Default | - -The last row is where Cratis is at its best, and it is the default recommendation for a new information system. New to the stack? [Choosing where to start](/adopting-cratis/) walks through how to start there, or how to adopt one piece at a time in an existing system. - -## Together: the whole loop - -Put AuthProxy at the edge, pick Chronicle as Arc's persistence, and add Components. A single user action now carries identity and tenant context through the whole stack with no manual API layer in between: - -```mermaid -flowchart LR - User["Browser"] --> AP["AuthProxy<br/>auth + tenant + identity"] - AP --> UI["React UI<br/>(Components)"] - UI -->|typed proxy via AuthProxy| CMD["Command<br/>(Arc)"] - AP -.->|tenant + identity| CMD - CMD -->|appends| EV["Event<br/>(Chronicle)"] - EV -->|projected into| RM["Read model<br/>(Chronicle)"] - RM -->|served by query| Q["Query<br/>(Arc)"] - Q -->|typed proxy| UI -``` - -You write the command, the event, and the projection once in C#. AuthProxy resolves identity and tenant. Arc generates the typed client and enforces authorization at the boundary. Components renders it. Chronicle keeps the facts and tenant isolation underneath. When the command's shape changes, the frontend types change with it — the compiler tells you what to fix instead of production telling your users. - -## The principles behind it - -Cratis is opinionated on purpose. The opinions are what make it productive: - -- **Events are facts.** Immutable, past-tense, single-purpose. We use them as the default source of truth for information systems; if you reach for a nullable field on an event, you need a second event. -- **Open boundaries keep options open.** Chronicle is a kernel with gRPC/protobuf contracts. .NET is first-class, but the protocol lets other client runtimes participate. -- **Storage is a deployment choice.** Chronicle supports MongoDB, PostgreSQL, Microsoft SQL Server, and SQLite without changing the domain model. -- **High cohesion through [vertical slices](/arc/vertical-slices/).** Everything for one behavior — command, events, projection, UI, specs — lives in one folder, backend and frontend together. -- **Full-stack type safety.** Models flow from C# through proxy generation to TypeScript, with no manual synchronization — [the proxy boundary](/arc/understanding-the-proxy-boundary/) is what keeps the two languages honest. -- **Cross-cutting concerns are first-class.** [Identity and access](/arc/understanding-identity-and-access/), [authorization](/arc/backend/core/authorization/), [tenancy](/arc/backend/tenancy/), [AuthProxy](/authproxy/), and [Chronicle namespaces](/chronicle/concepts/namespaces/) are part of the platform story. -- **Operability is architecture.** Workbench, CLI, OpenTelemetry, recommendations, replay, jobs, and failed-partition recovery are part of how you run the event store. -- **Specifications are executable models.** [Testing with Cratis](/testing-with-cratis/) maps event modeling's given/when/then flow to Arc command specs and Chronicle event/read-model/reactor scenarios. -- **Easy to do the right thing.** Convention over configuration and artifact discovery by naming mean less boilerplate and fewer ways to get it wrong. -- **Predictable code helps humans and AI.** The conventions are documented, packaged as [AI tooling](/ai/), and enforced by [code analysis](/code-analysis/), so a new feature looks like the rest of the system. - -## When Cratis is a good fit — and when it isn't - -Because the products are separable, "is Cratis a good fit?" is really a set of connected questions. - -**Is Arc a good fit?** Reach for it whenever you're building a .NET backend with a TypeScript/React frontend and you're tired of hand-writing the layer between them — controllers, DTOs, fetch wrappers, validation duplicated on both sides. Arc is the CQRS boundary: commands for things entering the system, queries for information leaving it, with generated contracts between C# and TypeScript. - -**Is event sourcing a good fit?** For information systems, our default answer is yes. Reach for Chronicle when the system records decisions, process, responsibility, tenant state, compliance, integrations, or any place where "how did we get here?" will become a real question. The exception is a genuinely current-state-only slice — reference data, settings, small admin surfaces — where Arc over a database can be the simpler boundary. [Why Event Sourcing](/chronicle/why-event-sourcing/) explains why we think the default pays off. - -When *neither* fits — a couple of static pages, a non-.NET backend, or a throwaway prototype — Cratis is more than you need, and that's fine. - -## Where to start - -<CardGrid> - <SimpleCard title="New to event sourcing?" icon="approve-check" link="/chronicle/why-event-sourcing/"> - Begin with the why — what facts buy you and when to reach for them — then the Chronicle getting started. - </SimpleCard> - <SimpleCard title="Just want a typed full-stack app?" icon="puzzle" link="/arc/"> - Use Arc over a database when a bounded slice or adoption step needs CQRS and generated React contracts without an event log. - </SimpleCard> - <SimpleCard title="Adding to an existing system?" icon="right-arrow" link="/adopting-cratis/"> - Greenfield or brownfield — how to choose an entry point and adopt one piece at a time. - </SimpleCard> -</CardGrid> diff --git a/web/src/content/docs/work-with-us.mdx b/web/src/content/docs/work-with-us.mdx deleted file mode 100644 index 82519e62..00000000 --- a/web/src/content/docs/work-with-us.mdx +++ /dev/null @@ -1,85 +0,0 @@ ---- -title: Work with us -description: Work directly with the people who build Cratis — event modeling, adoption, architecture review, implementation help, AI-ready foundations, and training. ---- - -import { CardGrid, LinkCard, Aside } from '@astrojs/starlight/components'; - -The way software gets built is changing. Assistants now write, refactor, and review a growing share of the code, and teams ship faster than their architecture was designed to absorb. What survives that pressure is an explicit model: facts the system actually recorded, intent the code states out loud, and conventions a new developer and an AI agent can both follow without guessing. - -That is what Cratis is built for, and it is what we help teams build. - -We maintain Chronicle, Arc, Components, and the rest of the stack. If you are evaluating event sourcing, adopting Cratis, or trying to get an existing system into a shape you can move quickly in, you can work with us directly. - -<CardGrid> - <LinkCard title="Contact us" description="Email the Cratis maintainers with your context, the kind of help you need, and the timeframe." href="mailto:oss@cratis.io?subject=Working%20with%20Cratis" /> - <LinkCard title="Ask in the community first" description="Discord is open, free, and often faster when the question can be discussed publicly." href="/community/" /> - <LinkCard title="How dedicated help works" description="What separates a private engagement from community support, and how to prepare an inquiry." href="/professional-help/" /> -</CardGrid> - -## If you need help - -You do not need a finished design or an approved budget to start a conversation. These are the moments teams usually reach out: - -- **You are evaluating event sourcing** and want an honest answer about whether it fits your system — including the cases where it does not. -- **You are adopting Cratis** and want the first slices modeled right, because the early ones set the pattern everything after them copies. -- **Your model has drifted** — projections that no longer answer the questions the business asks, or events that record mechanics instead of facts. -- **You want a codebase agents are productive in**, with conventions and analyzers that keep generated code on the rails instead of quietly off them. -- **You need a second opinion before you commit** to consistency boundaries, tenancy, storage, or a migration path. -- **Your team knows the framework but not the modeling.** The syntax is learnable in a week. The modeling is the part worth teaching. - -## What we can help with - -| Need | What it looks like | What you end up with | -|---|---|---| -| Event modeling | A working session over your domain — commands, events, read models, and boundaries on a shared canvas | An event model your team agrees on, and the slices to build first | -| Cratis adoption | Slice selection, product choice, and a path from the system you run today | A sequenced adoption plan with the first slice specified | -| Architecture review | A read of your model, consistency boundaries, tenancy, storage, and failure modes | A written review with the risks ranked and the fixes named | -| Implementation help | Pairing, pull-request guidance, or focused development on a bounded slice | Working slices in your codebase, built the way the stack intends | -| AI-ready foundations | Conventions, analyzers, and `.ai` guidance set up so assistants build to your standards | A repository where developers and agents follow the same rails | -| Teaching event sourcing | Domain events, consistency, projections, replay, versioning, and the operational trade-offs | A team that can model without us in the room | -| Teaching the stack | Chronicle, Arc, Components, and the CLI, taught through the full-stack loop | A team productive in the stack without shadowing someone | -| Production readiness | A review of TLS, storage, secrets, deployment, observability, replay, and backup plans | A checked list of what must be true before you go live | - -## Building for the AI age - -There is a reason we lead with the model rather than the tooling. An assistant is only as good as the structure it works inside: give it a vague codebase and it produces vague code, faster. Give it recorded facts, typed contracts that span the stack, and conventions a compiler or analyzer can check, and its output starts landing inside the lines. - -Cratis ships that structure. Generated proxies break the build when a contract drifts, analyzers catch the shapes that are wrong before review does, and the `.ai` rules and skills are written to be read by assistants directly. A large part of any engagement is putting those rails in place, so that moving faster does not quietly cost you the model. - -For what this looks like in the products themselves, see [AI-native development](/ai/), the [editor and assistant plugins](/plugins/), and [code analysis](/code-analysis/). - -## How we work - -- **Start with a conversation.** Describe the context and what you need. We will tell you whether we are the right fit, and say so plainly when we are not. -- **Scoped and time-boxed.** A workshop, a review, or a bounded piece of development with an agreed outcome — not an open-ended retainer. -- **Private when it needs to be.** Your domain, your code, and your roadmap stay yours. -- **Open when it does not.** If the answer would help everyone, it belongs in the documentation or in Discord, and that is where we would rather put it. - -## What to include when you contact us - -Enough context for us to tell you honestly whether we can help: - -- What you are building or evaluating. -- Which Cratis products you use, or expect to use. -- Whether this is greenfield, brownfield, or a system already in production. -- The kind of help you want: modeling, review, development, training, or troubleshooting. -- The rough timeframe, and whether the work has to be private. - -## When we are not the right fit - -Being direct about the limits saves everyone a meeting: - -- **The question can be answered in public.** [Community and help](/community/) is free, open, and usually faster. -- **You found a vulnerability.** Use [Security](/security/) for responsible disclosure instead. -- **You want a general .NET or React consultancy.** We work on the model, the stack, and the architecture around them. - -<Aside type="note" title="Availability is finite"> -We build and maintain Cratis, so engagements depend on availability, fit, and the relevant consultancy, employer, and conflict-of-interest arrangements. Community support on Discord stays open and best-effort regardless. -</Aside> - -## Contact us - -Email [oss@cratis.io](mailto:oss@cratis.io?subject=Working%20with%20Cratis) with a subject that starts with `Working with Cratis`, and include the context above. We will come back to you on whether we can help, what shape the work would take, and when we could start. - -If you are still deciding where to begin, [Adopting Cratis](/adopting-cratis/) lays out the paths from an existing system, and [Learning paths](/learning-paths/) routes you through the documentation by role. diff --git a/web/src/public-pages/404.md b/web/src/public-pages/404.md new file mode 100644 index 00000000..189feaa9 --- /dev/null +++ b/web/src/public-pages/404.md @@ -0,0 +1,21 @@ +--- +title: Page not found +description: The requested documentation page does not exist. +template: splash +editUrl: false +hero: + title: "404" + tagline: Choose one of the current canonical product paths. + actions: + - text: Documentation home + link: / + icon: right-arrow + - text: Arc + link: /arc/ + variant: minimal +--- + +- [Chronicle](/chronicle/) +- [Arc](/arc/) +- [Components](/components/) +- [Cratis CLI](/cli/) diff --git a/web/src/public-pages/index.mdx b/web/src/public-pages/index.mdx new file mode 100644 index 00000000..dea7d26b --- /dev/null +++ b/web/src/public-pages/index.mdx @@ -0,0 +1,107 @@ +--- +title: Cratis documentation +description: Canonical technical documentation for Chronicle, Arc, Components, and the Cratis CLI. +template: splash +hero: + title: Cratis documentation + tagline: "Choose a product, follow its technical documentation, and keep each product's current limits visible." + image: + dark: ../../assets/cratis-mark-dark.svg + light: ../../assets/cratis-mark-light.svg + actions: + - text: Explore Arc + link: /arc/ + icon: right-arrow + - text: Explore Components + link: /components/ + variant: minimal + icon: right-arrow + - text: Explore the CLI + link: /cli/ + variant: minimal + icon: right-arrow + - text: Company + link: https://cratis.no + variant: minimal +--- + +import { Card, CardGrid, LinkCard } from '@astrojs/starlight/components'; + +## Product documentation + +Each product repository owns its technical documentation and released behavior. This site converts those sources into one navigable documentation surface. + +<CardGrid> + <Card title="Chronicle" icon="seti:db"> + Chronicle uses a .NET/Orleans actor-based kernel behind gRPC/HTTP surfaces and supports multiple event stores, namespaces, and persistent event-store subscriptions with outbox/inbox sequences. + + Chronicle and its bundled local Workbench are available as MIT-licensed self-hosted software; authorized local use is separate from paid Cratis support, hosted coordination, or managed operational responsibility. + + [Open the Chronicle documentation](/chronicle/). + </Card> + + <Card title="Arc" icon="puzzle"> + Arc is an opinionated CQRS application framework for ASP.NET Core with commands, queries, validation, authorization, and TypeScript proxy generation. + + [Open the Arc documentation](/arc/). + </Card> + + <Card title="Components" icon="laptop"> + Components is a React component library aligned with Arc application patterns. + + [Open the Components documentation](/components/). + </Card> + + <Card title="Cratis CLI" icon="rocket"> + The Cratis CLI provides terminal workflows for inspecting and diagnosing Chronicle. + + [Open the CLI documentation](/cli/). + </Card> +</CardGrid> + +## How the current products relate + +- **Event sourcing is not required to use Arc.** Arc's exact command, query, validation, authorization, and generated-contract behavior is documented in the Arc product pages. +- **Chronicle has separate browser and terminal inspection surfaces.** Chronicle Workbench provides a bundled local browser surface for authorized inspection of Chronicle runtime state and preview of supported projection behavior. The CLI provides terminal inspection and diagnosis. + +These relationships do not imply compatibility with every product version, frontend, runtime, client, provider, browser, environment, or topology. Use the exact package manifests, current product documentation, and exercised profile for the combination you evaluate. + +## Start with one bounded path + +<CardGrid> + <LinkCard + title="Build a CQRS feature with Arc" + description="Follow Arc's command, query, validation, authorization, and generated TypeScript proxy path." + href="/arc/" + /> + <LinkCard + title="Render an Arc command with Components" + description="Install Components, mount the provider, and render a typed command form." + href="/components/" + /> + <LinkCard + title="Inspect Chronicle from the terminal" + description="Install the CLI, connect to a Chronicle server, and follow its documented diagnostic workflows." + href="/cli/" + /> + <LinkCard + title="Browse the Chronicle architecture" + description="Understand the current kernel, protocol surfaces, stores, namespaces, and subscription model." + href="/chronicle/architecture/" + /> +</CardGrid> + +## What this site treats as authoritative + +- Product repositories own source, examples, technical documentation, package metadata, and release evidence. +- cratis.io is the canonical rendered technical navigation surface. +- [cratis.no](https://cratis.no) owns company, fit, trust, and commercial information. +- Repository READMEs and package pages summarize one product or package and link back to canonical technical documentation. + +A repository, package, example, successful build, or polished page does not establish maturity, support, security, performance, compatibility, or production suitability. Those statements require their own current evidence and approval. + +## Help improve the documentation + +- [Ask a technical question or join the community](https://discord.gg/kt4AMpV8WV) +- [Report a documentation problem](https://github.com/Cratis/Documentation/issues) +- [Browse the Cratis repositories](https://github.com/Cratis) From 3fe50b0269d9ed37174a820faa15812cf1e58bc3 Mon Sep 17 00:00:00 2001 From: woksin <sindrewilting@gmail.com> Date: Tue, 25 Aug 2026 13:12:39 +0200 Subject: [PATCH 2/6] Restore the claim-safe MDX documentation experience --- .ai/rules/astro-starlight-site.md | 65 +++ .ai/rules/documentation-rendering-and-qa.md | 60 +++ web/.markdownlint-cli2.jsonc | 6 +- web/package-lock.json | 525 ++++++++++++++++++++ web/package.json | 3 + web/public-surface.json | 46 +- web/scripts/lint-markdown.mjs | 39 +- web/scripts/sync-public-content.mjs | 28 +- web/src/public-pages/404.md | 2 +- web/src/public-pages/index.mdx | 126 +++-- 10 files changed, 788 insertions(+), 112 deletions(-) create mode 100644 .ai/rules/astro-starlight-site.md create mode 100644 .ai/rules/documentation-rendering-and-qa.md diff --git a/.ai/rules/astro-starlight-site.md b/.ai/rules/astro-starlight-site.md new file mode 100644 index 00000000..aa654cdc --- /dev/null +++ b/.ai/rules/astro-starlight-site.md @@ -0,0 +1,65 @@ +--- +applyTo: "web/**/*.{js,mjs,ts,astro,json,md,mdx}" +paths: + - "web/**" +--- + +# Astro/Starlight exact public-surface rules + +The deployed cratis.io site is an exact projection, not a broad documentation +crawl. + +## Publication source + +- `web/public-surface.json` owns the admitted route, source repository/path, + reviewed revision, SHA-256, claim IDs, sitemap flag, and exact static files. +- `web/scripts/sync-public-content.mjs` validates and materializes only that + manifest. Unknown routes, source/hash drift, Draft content, CLM-010 matrix + wording, symlink escapes, search/machine output, and policy differences fail. +- `web/src/content/docs/`, `web/src/generated/`, and `web/.public-approved/` are + disposable outputs. Never edit or commit them. +- Site-owned source lives in `web/src/public-pages/`; product source stays in the + owning product repository. +- `npm run sync:authoring` is broad, nondeployable authoring only. + +## Native Markdown and MDX contract + +Exact-production source is copied byte-for-byte. It does not receive the legacy +DocFX conversion pipeline. + +Every admitted page must therefore: + +- use Starlight frontmatter with at least `title` and a bounded `description`; +- omit a body H1 because Starlight renders the frontmatter title; +- start body sections at H2; +- use native Starlight aside syntax and root-relative trailing-slash links; +- use `.mdx` when importing Starlight or custom components; +- avoid DocFX alerts, xrefs, includes, `.md` URL suffixes, and relative product + assets; and +- keep every component-emitted link inside the manifest route set. + +## Product landing pages + +Product overviews use the tour pattern where it helps readers: + +- `TopicHero` for exact product identity and one bounded reader job; +- Starlight `CardGrid`, `Card`, `LinkCard`, and `Steps`; +- `SimpleCard` only with page-supplied wording and admitted links; +- `FullStackTabs` only for source-verified paired examples. + +Do not use `RotatingHero`, `StackJourney`, `StackDiagram`, or Storybook embeds in +the current eight-route manifest. They own excluded claims/routes/artifacts. + +## Build and deployment + +- `npm ci && npm run check` is the authoritative local and CI gate. +- Astro/Starlight is the authoritative MDX parser and component resolver. +- Rendered-link validation catches links emitted from MDX components. +- Pagefind, llms files, page actions, raw Markdown mirrors, Storybooks, and API + sites remain disabled until separately admitted. +- Pull requests validate reviewed product revisions with read-only credentials. +- Main deployment checks product `main` bytes and fails on hash drift. +- Pages/OIDC permissions exist only in the main-only deploy job. + +Never weaken a route, claim, hash, artifact, or deployment check merely to make a +build green. diff --git a/.ai/rules/documentation-rendering-and-qa.md b/.ai/rules/documentation-rendering-and-qa.md new file mode 100644 index 00000000..85301aae --- /dev/null +++ b/.ai/rules/documentation-rendering-and-qa.md @@ -0,0 +1,60 @@ +--- +applyTo: "web/**/*.{astro,css,md,mdx,js,mjs,ts}" +paths: + - "web/**" +--- + +# Documentation rendering and visual QA + +A successful build proves syntax and route integrity, not a professional reader +experience. Review rendered output for every changed admitted route. + +## Required rendered checks + +1. Run the exact production build: + + ```bash + cd web + npm ci + npm run check + ``` + +2. Serve the built site with `npm run preview`. +3. Capture every admitted route in light and dark mode. +4. Capture `/`, each product landing, and `/404.html` at a mobile width. +5. Verify keyboard traversal, visible focus, heading order, descriptive links, + table/code overflow, diagram readability, and no image-dependent meaning. +6. Verify reduced-motion mode and no carousel/ambient motion requirement. +7. Check first load for font/image/layout shift. + +Use `web/scripts/screenshot.mjs` for reproducible screenshots when available. +Screenshots are local review evidence and are not committed unless their exact +public asset/provenance path is separately admitted. + +## Preserve current rendering safeguards + +- Keep the custom `Head.astro` font preload and `font-display: optional` behavior. +- Keep Mermaid build-time rendering and `autoTheme: false`; light/dark colors are + handled by CSS variables. +- Keep the theme-adaptive Cratis marks and an empty alt only when the mark is + decorative and equivalent text is present. +- Keep product pages useful without JavaScript. + +## MDX component review + +- `TopicHero` and `SimpleCard` are render-only and safe when all text/links come + from the page. +- Starlight `Card`, `CardGrid`, `LinkCard`, `Steps`, `Tabs`, and `Aside` are + preferred for native behavior. +- Validate icon names during Astro build. +- Inspect component output in final HTML; source-only Markdown link scans are not + sufficient. +- Do not hide claim text, links, or product relationships inside imported + components that are not included in the manifest review. + +## Failure handling + +Block the candidate on broken layout, unreadable contrast, inaccessible focus, +mobile overflow, missing content without JavaScript, diagram failure, unexpected +route/link output, or material layout shift. Fix the source or component; do not +waive the public-surface gate. diff --git a/web/.markdownlint-cli2.jsonc b/web/.markdownlint-cli2.jsonc index ed6a3559..1b82ac11 100644 --- a/web/.markdownlint-cli2.jsonc +++ b/web/.markdownlint-cli2.jsonc @@ -1,13 +1,11 @@ { - // markdownlint-cli2 configuration. Activated by scripts/lint-markdown.mjs when the tool is - // installed (run `npm i -D markdownlint-cli2`, or install globally); skipped gracefully otherwise. - // Lints the synced Markdown under src/content/docs. .mdx is ignored — its JSX trips the parser. + // Fail-closed lint for exact-production plain Markdown. MDX is intentionally + // excluded because Astro/Starlight is its authoritative parser. "globs": ["src/content/docs/**/*.md"], "ignores": ["**/*.mdx", "src/generated/**"], "config": { "default": true, "MD013": false, // line length — our prose intentionally uses long lines - "MD033": false, // inline HTML — common across the content "MD041": false, // first-line heading — pages start with frontmatter "MD025": false, // single H1 — the title lives in frontmatter "MD024": { "siblings_only": true }, // duplicate headings are fine across different sections diff --git a/web/package-lock.json b/web/package-lock.json index 80feeb83..042543aa 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -23,6 +23,9 @@ "starlight-typedoc": "^0.23.0", "typedoc": "^0.28.19", "typedoc-plugin-markdown": "^4.11.0" + }, + "devDependencies": { + "markdownlint-cli2": "^0.23.2" } }, "node_modules/@antfu/install-pkg": { @@ -1435,6 +1438,44 @@ "node": "^22.20 || ^24.12 || >=25" } }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/@oslojs/encoding": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@oslojs/encoding/-/encoding-1.1.0.tgz", @@ -1991,6 +2032,19 @@ "integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==", "license": "MIT" }, + "node_modules/@sindresorhus/merge-streams": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz", + "integrity": "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@types/d3": { "version": "7.4.3", "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz", @@ -2289,6 +2343,13 @@ "integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==", "license": "MIT" }, + "node_modules/@types/katex": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@types/katex/-/katex-0.16.8.tgz", + "integrity": "sha512-trgaNyfU+Xh2Tc+ABIb44a5AYUpicB3uwirOioeOkNPPbmgRNtcWyDeeFRzjPZENO9Vq8gvVqfhaaXWLlevVwg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/mdast": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", @@ -2387,6 +2448,19 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/ansi-regex": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.3.0.tgz", + "integrity": "sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, "node_modules/anymatch": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", @@ -2675,6 +2749,19 @@ "node": "20 || >=22" } }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/ccount": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", @@ -3871,6 +3958,23 @@ "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", "license": "MIT" }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, "node_modules/fast-string-truncated-width": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/fast-string-truncated-width/-/fast-string-truncated-width-3.0.3.tgz", @@ -3904,6 +4008,16 @@ "strictdom": "^1.0.1" } }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -3921,6 +4035,19 @@ } } }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/flattie": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/flattie/-/flattie-1.1.1.tgz", @@ -3965,6 +4092,19 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/get-east-asian-width": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz", + "integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-tsconfig": { "version": "5.0.0-beta.4", "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-5.0.0-beta.4.tgz", @@ -3986,6 +4126,40 @@ "integrity": "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==", "license": "ISC" }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/globby": { + "version": "16.2.2", + "resolved": "https://registry.npmjs.org/globby/-/globby-16.2.2.tgz", + "integrity": "sha512-NLvV9ubZ6NDsJaOpKPy3cQeJpKi9DcWiyCiFUpJPA0YihRqiE6RWaLUmgNNPr8MgPpLZjnBjSmou7uZBRJv9wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/merge-streams": "^4.0.0", + "fast-glob": "^3.3.3", + "ignore": "^7.0.5", + "is-path-inside": "^4.0.0", + "slash": "^5.1.0", + "unicorn-magic": "^0.4.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/h3": { "version": "1.15.11", "resolved": "https://registry.npmjs.org/h3/-/h3-1.15.11.tgz", @@ -4446,6 +4620,16 @@ "node": ">=0.10.0" } }, + "node_modules/ignore": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.6.tgz", + "integrity": "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/import-meta-resolve": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.2.0.tgz", @@ -4529,6 +4713,29 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-hexadecimal": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", @@ -4572,6 +4779,29 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-path-inside": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-4.0.0.tgz", + "integrity": "sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-plain-obj": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", @@ -4627,6 +4857,16 @@ "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", "license": "MIT" }, + "node_modules/jsonpointer": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-5.0.1.tgz", + "integrity": "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/katex": { "version": "0.16.47", "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.47.tgz", @@ -4803,6 +5043,106 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/markdownlint": { + "version": "0.41.1", + "resolved": "https://registry.npmjs.org/markdownlint/-/markdownlint-0.41.1.tgz", + "integrity": "sha512-qHKeU2E1bdyNAT077go2FVTNXvYcktN5IHtF6XyeD1l0PClxzSp2tUApAV14ORI8DGX4H9bNKZEzelZp4qn8IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "micromark": "4.0.2", + "micromark-core-commonmark": "2.0.3", + "micromark-extension-directive": "4.0.0", + "micromark-extension-gfm-autolink-literal": "2.1.0", + "micromark-extension-gfm-footnote": "2.1.0", + "micromark-extension-gfm-table": "2.1.1", + "micromark-extension-math": "3.1.0", + "micromark-util-types": "2.0.2", + "string-width": "8.2.1" + }, + "engines": { + "node": ">=22" + }, + "funding": { + "url": "https://github.com/sponsors/DavidAnson" + } + }, + "node_modules/markdownlint-cli2": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/markdownlint-cli2/-/markdownlint-cli2-0.23.2.tgz", + "integrity": "sha512-eUhcnkSpzURo/o4htSqc7LPDszgOOTknhU4eY/sPHvMCLxnTCYscv1gw1/js/idmaZPisv9ECVEIORcllqjTUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "globby": "16.2.2", + "js-yaml": "5.2.2", + "jsonc-parser": "3.3.1", + "jsonpointer": "5.0.1", + "markdown-it": "14.3.0", + "markdownlint": "0.41.1", + "markdownlint-cli2-formatter-default": "0.0.6", + "micromatch": "4.0.8", + "smol-toml": "1.7.0" + }, + "bin": { + "markdownlint-cli2": "markdownlint-cli2-bin.mjs" + }, + "engines": { + "node": ">=22" + }, + "funding": { + "url": "https://github.com/sponsors/DavidAnson" + } + }, + "node_modules/markdownlint-cli2-formatter-default": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/markdownlint-cli2-formatter-default/-/markdownlint-cli2-formatter-default-0.0.6.tgz", + "integrity": "sha512-VVDGKsq9sgzu378swJ0fcHfSicUnMxnL8gnLm/Q4J/xsNJ4e5bA6lvAz7PCzIl0/No0lHyaWdqVD2jotxOSFMQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/DavidAnson" + }, + "peerDependencies": { + "markdownlint-cli2": ">=0.0.4" + } + }, + "node_modules/markdownlint-cli2/node_modules/js-yaml": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-5.2.2.tgz", + "integrity": "sha512-dayzUzKkJ1MkuUtZglSebU43utNXH0OWQByK9rKOOuYIO8M5TV1y+n8ALMdG0rdzBnfNkOmZEqrURepb0ejqBw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.mjs" + } + }, + "node_modules/markdownlint-cli2/node_modules/smol-toml": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.7.0.tgz", + "integrity": "sha512-aqVvWoyO21L23mb+drl4RmMXbf6N7FdHjAhTRA9ZBL7apWBgfWC16KjrASI+1p9GAroljyMHj6fK67i0UiTNvQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 18" + }, + "funding": { + "url": "https://github.com/sponsors/cyyynthia" + } + }, "node_modules/marked": { "version": "16.4.2", "resolved": "https://registry.npmjs.org/marked/-/marked-16.4.2.tgz", @@ -5150,6 +5490,16 @@ "integrity": "sha512-1+HBaOx0zi/dQWht8rNv9MYf9qqpqL/kxI0hXImU6Y547zM6Sni8BQibt7ifgMcYtQg41ao3Ivd6cnSM86inpg==", "license": "MIT" }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, "node_modules/mermaid": { "version": "11.17.1", "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-11.17.1.tgz", @@ -5389,6 +5739,26 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/micromark-extension-math": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-math/-/micromark-extension-math-3.1.0.tgz", + "integrity": "sha512-lvEqd+fHjATVs+2v/8kg9i5Q0AP2k85H0WUOwpIVvUML8BapsMvh1XAogmQjOCsLpoKRCVQqEkQBB3NhVBcsOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/katex": "^0.16.0", + "devlop": "^1.0.0", + "katex": "^0.16.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/micromark-extension-mdx-expression": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/micromark-extension-mdx-expression/-/micromark-extension-mdx-expression-3.0.1.tgz", @@ -5916,6 +6286,33 @@ ], "license": "MIT" }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/minimatch": { "version": "10.2.6", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", @@ -6328,6 +6725,27 @@ "node": ">=6" } }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/radix3": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/radix3/-/radix3-1.1.2.tgz", @@ -6718,6 +7136,17 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, "node_modules/robust-predicates": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.3.tgz", @@ -6781,6 +7210,30 @@ "points-on-path": "^0.2.1" } }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, "node_modules/rw": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", @@ -6902,6 +7355,19 @@ "npm": ">=10.8.2" } }, + "node_modules/slash": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz", + "integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/smol-toml": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.8.0.tgz", @@ -7017,6 +7483,23 @@ "integrity": "sha512-cEmp9QeXXRmjj/rVp9oyiqcvyocWab/HaoN4+bwFeZ7QzykJD6L3yD4v12K1x0tHpqRqVpJevN3gW7kyM39Bqg==", "license": "MIT" }, + "node_modules/string-width": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.1.tgz", + "integrity": "sha512-IIaP0g3iy9Cyy18w3M9YcaDudujEAVHKt3a3QJg1+sr/oX96TbaGUubG0hJyCjCBThFH+tFpcIyoUHUn1ogaLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.5.0", + "strip-ansi": "^7.1.2" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/stringify-entities": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", @@ -7031,6 +7514,22 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/style-to-js": { "version": "1.1.21", "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.21.tgz", @@ -7136,6 +7635,19 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, "node_modules/trim-lines": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", @@ -7260,6 +7772,19 @@ "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", "license": "MIT" }, + "node_modules/unicorn-magic": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.4.0.tgz", + "integrity": "sha512-wH590V9VNgYH9g3lH9wWjTrUoKsjLF6sGLjhR4sH1LWpLmCOH0Zf7PukhDA8BiS7KHe4oPNkcTHqYkj7SOGUOw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/unified": { "version": "11.0.5", "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", diff --git a/web/package.json b/web/package.json index cb9e9be3..c13255a2 100644 --- a/web/package.json +++ b/web/package.json @@ -44,5 +44,8 @@ "starlight-typedoc": "^0.23.0", "typedoc": "^0.28.19", "typedoc-plugin-markdown": "^4.11.0" + }, + "devDependencies": { + "markdownlint-cli2": "^0.23.2" } } diff --git a/web/public-surface.json b/web/public-surface.json index 957bf176..49802ff8 100644 --- a/web/public-surface.json +++ b/web/public-surface.json @@ -16,7 +16,7 @@ "repository": "Documentation", "revision": "self", "path": "web/src/public-pages/index.mdx", - "sha256": "4c27c3b9d6de9611562df46551f8a89e57df870b4a4fefc60967a2db0f42bf6f" + "sha256": "5474530c30aee0ac6bcfae7532dc69b9face773d1c8ce8c066ced6e9b8ae8436" }, "claims": ["CLM-011", "CLM-012", "CLM-013", "CLM-014", "CLM-028", "CLM-032"], "search": false, @@ -30,7 +30,7 @@ "repository": "Documentation", "revision": "self", "path": "web/src/public-pages/404.md", - "sha256": "9b12f8538f82e7245d9589d8e47dfde8bf42eb27ee7bdf560c76efbc5ee4b211" + "sha256": "011ad14e139708a88bf3663300459270137d4cf8012b1c04ccaa174eb95c9ef6" }, "claims": [], "search": false, @@ -42,9 +42,9 @@ "contentPath": "chronicle/index.mdx", "source": { "repository": "Chronicle", - "revision": "1b523b3ab70d58b487b840782992c317dd31e8bb", + "revision": "bbc01a8e18bda6f2d81d370ae91a8b324800773e", "path": "Documentation/index.mdx", - "sha256": "df5778f606efc39e324e29cf733bb21a4606008e7df9aa9403f8df3de313beb8" + "sha256": "ce5e8a6ce7d17ba02023439fc90d3a2250fa71f75b3a584f312388edbcea57e4" }, "claims": ["CLM-013", "CLM-014", "CLM-028", "CLM-032"], "search": false, @@ -53,12 +53,12 @@ }, { "route": "/chronicle/architecture/", - "contentPath": "chronicle/architecture.md", + "contentPath": "chronicle/architecture.mdx", "source": { "repository": "Chronicle", - "revision": "1b523b3ab70d58b487b840782992c317dd31e8bb", - "path": "Documentation/architecture.md", - "sha256": "3163d6221ab9583583c9ddc8ad6a1f192f854503553daf193fb24d73a844d856" + "revision": "bbc01a8e18bda6f2d81d370ae91a8b324800773e", + "path": "Documentation/architecture.mdx", + "sha256": "0f79925a1e1130edfff89680a627660cb64a8749454562dacc7e73333cdabb30" }, "claims": ["CLM-013", "CLM-014", "CLM-028"], "search": false, @@ -67,12 +67,12 @@ }, { "route": "/chronicle/workbench/", - "contentPath": "chronicle/workbench/index.md", + "contentPath": "chronicle/workbench/index.mdx", "source": { "repository": "Chronicle", - "revision": "1b523b3ab70d58b487b840782992c317dd31e8bb", - "path": "Documentation/workbench/index.md", - "sha256": "b542732dee52bce5d545a7099d58e45bbb223ed6ddd67fc31b60c7ac6cfd14a4" + "revision": "bbc01a8e18bda6f2d81d370ae91a8b324800773e", + "path": "Documentation/workbench/index.mdx", + "sha256": "52ada440bafa53727a031e3b4faa92c7b7a4b13750ffc24c919a2788390eaa23" }, "claims": ["CLM-013", "CLM-014", "CLM-032"], "search": false, @@ -84,9 +84,9 @@ "contentPath": "arc/index.mdx", "source": { "repository": "Arc", - "revision": "f840015238b48ed9aad9998469d2fdaf2370b8df", + "revision": "3b54436c95004ab989a8788dc960b87f4ff40f5c", "path": "Documentation/index.mdx", - "sha256": "55e249cffbc3c6af1991f79ee8d2aab9904904aa2fb4322f0b463d2d04fd4c04" + "sha256": "c4bde7b68e35c2506453f19867f415f1abf4e3a234678a3daf33ba0b98410d18" }, "claims": ["CLM-011", "CLM-012"], "search": false, @@ -98,9 +98,9 @@ "contentPath": "components/index.mdx", "source": { "repository": "Components", - "revision": "2f7dd5046b7c5f2addb98ad4204d609a0f11d8fe", + "revision": "a1e4da5b36b3ffad67ab8b2e3a67098242dbbee2", "path": "Documentation/index.mdx", - "sha256": "945a3326dba9756a8eb809254da7c74a4dd25e4457dbb6ba0f77fd97e1e3e313" + "sha256": "1c966f3140ec1e854eef23d46257e75517a1b8224a5488346ab72db474702202" }, "claims": ["CLM-011", "CLM-012"], "search": false, @@ -112,9 +112,9 @@ "contentPath": "cli/index.mdx", "source": { "repository": "cli", - "revision": "db7d2339569042ff10896d0ae3219ad8e4505dd3", + "revision": "711013ff87ec05dc8574ee133f56c1df230e253d", "path": "Documentation/index.mdx", - "sha256": "f721d93f78f65174ee4ea681df1ea65199a42c3efca7b4890ae2aca041c7987f" + "sha256": "cfe3c0c14547130f06ebd37d378bdaa4259a3dfd6f8955583f8187ac0e80e7cf" }, "claims": ["CLM-013", "CLM-014"], "search": false, @@ -122,6 +122,16 @@ "sitemap": true } ], + "renderDependencies": [ + { + "path": "web/src/components/TopicHero.astro", + "sha256": "a67bf5552c44b7eb53d86a1b367340d7ca9027222e4226f16e6f58be7ee262ac" + }, + { + "path": "web/src/components/SimpleCard.astro", + "sha256": "49d34c3d9b2dfbf12fd99fb4ecd6797ce7e34379e3434ca50be286e7d19ceda5" + } + ], "generatedArtifacts": { "prefix": "_astro/", "extensions": [".css", ".js", ".png", ".svg", ".woff2"], diff --git a/web/scripts/lint-markdown.mjs b/web/scripts/lint-markdown.mjs index 00396dbc..95c21221 100644 --- a/web/scripts/lint-markdown.mjs +++ b/web/scripts/lint-markdown.mjs @@ -1,9 +1,5 @@ -// Optional markdownlint pass. Runs markdownlint-cli2 over the synced Markdown when it's -// installed, and skips gracefully when it isn't — so it never breaks `npm run check` in an -// environment without it (CI installs it; local may not). Mirrors the optional Vale layer -// in lint-prose.mjs. Advisory: reports structural issues but does not fail the gate. -// -// Configuration lives in .markdownlint-cli2.jsonc. To enable locally: `npm i -D markdownlint-cli2`. +// Fail-closed markdownlint pass for exact-production plain Markdown. +// MDX is validated by Astro/Starlight and rendered-link checks instead. import { spawnSync } from 'node:child_process'; import { existsSync } from 'node:fs'; @@ -15,24 +11,23 @@ const webRoot = path.resolve(here, '..'); const binName = process.platform === 'win32' ? 'markdownlint-cli2.cmd' : 'markdownlint-cli2'; const localBin = path.join(webRoot, 'node_modules', '.bin', binName); -function globalHas(bin) { - const probe = spawnSync(process.platform === 'win32' ? 'where' : 'command', ['-v', bin], { - shell: true, - stdio: 'ignore', - }); - return probe.status === 0; +if (!existsSync(localBin)) { + console.error('[lint:markdown] local markdownlint-cli2 is missing; run npm ci'); + process.exit(1); } -const cmd = existsSync(localBin) ? localBin : (globalHas('markdownlint-cli2') ? 'markdownlint-cli2' : null); +const result = spawnSync(localBin, [], { + cwd: webRoot, + stdio: 'inherit', + shell: false, +}); -if (!cmd) { - console.log('[lint:markdown] markdownlint-cli2 not installed — skipping (run `npm i -D markdownlint-cli2` to enable).'); - process.exit(0); +if (result.error) { + console.error(`[lint:markdown] failed to start markdownlint-cli2: ${result.error.message}`); + process.exit(1); } - -// markdownlint-cli2 reads .markdownlint-cli2.jsonc (globs + rules) automatically. -const result = spawnSync(cmd, [], { cwd: webRoot, stdio: 'inherit', shell: true }); -if (result.status && result.status !== 0) { - console.log('[lint:markdown] markdownlint reported issues (advisory — not failing the build).'); +if (result.status === null) { + console.error('[lint:markdown] markdownlint-cli2 exited without a status'); + process.exit(1); } -process.exit(0); +process.exit(result.status); diff --git a/web/scripts/sync-public-content.mjs b/web/scripts/sync-public-content.mjs index 71daacec..a41244bf 100644 --- a/web/scripts/sync-public-content.mjs +++ b/web/scripts/sync-public-content.mjs @@ -23,12 +23,17 @@ const allowedClaims = new Set([ 'CLM-032', ]); +const expectedRenderDependencies = [ + ['web/src/components/TopicHero.astro', 'a67bf5552c44b7eb53d86a1b367340d7ca9027222e4226f16e6f58be7ee262ac'], + ['web/src/components/SimpleCard.astro', '49d34c3d9b2dfbf12fd99fb4ecd6797ce7e34379e3434ca50be286e7d19ceda5'], +]; + const expectedRoutePolicy = [ ['/', 'index.mdx', 'Documentation', 'web/src/public-pages/index.mdx', ['CLM-011', 'CLM-012', 'CLM-013', 'CLM-014', 'CLM-028', 'CLM-032'], true], ['/404.html', '404.md', 'Documentation', 'web/src/public-pages/404.md', [], false], ['/chronicle/', 'chronicle/index.mdx', 'Chronicle', 'Documentation/index.mdx', ['CLM-013', 'CLM-014', 'CLM-028', 'CLM-032'], true], - ['/chronicle/architecture/', 'chronicle/architecture.md', 'Chronicle', 'Documentation/architecture.md', ['CLM-013', 'CLM-014', 'CLM-028'], true], - ['/chronicle/workbench/', 'chronicle/workbench/index.md', 'Chronicle', 'Documentation/workbench/index.md', ['CLM-013', 'CLM-014', 'CLM-032'], true], + ['/chronicle/architecture/', 'chronicle/architecture.mdx', 'Chronicle', 'Documentation/architecture.mdx', ['CLM-013', 'CLM-014', 'CLM-028'], true], + ['/chronicle/workbench/', 'chronicle/workbench/index.mdx', 'Chronicle', 'Documentation/workbench/index.mdx', ['CLM-013', 'CLM-014', 'CLM-032'], true], ['/arc/', 'arc/index.mdx', 'Arc', 'Documentation/index.mdx', ['CLM-011', 'CLM-012'], true], ['/components/', 'components/index.mdx', 'Components', 'Documentation/index.mdx', ['CLM-011', 'CLM-012'], true], ['/cli/', 'cli/index.mdx', 'cli', 'Documentation/index.mdx', ['CLM-013', 'CLM-014'], true], @@ -149,6 +154,11 @@ if (!Array.isArray(manifest.routes) || manifest.routes.length === 0) { throw new Error('manifest.routes must be a non-empty array'); } if (!Array.isArray(manifest.staticFiles)) throw new Error('manifest.staticFiles must be an array'); +if (!Array.isArray(manifest.renderDependencies)) throw new Error('manifest.renderDependencies must be an array'); +const renderDependencies = manifest.renderDependencies.map((entry) => [entry.path, entry.sha256]); +if (JSON.stringify(renderDependencies) !== JSON.stringify(expectedRenderDependencies)) { + throw new Error('manifest.renderDependencies differs from the owning-surface render dependency policy'); +} const generatedArtifacts = assertObject(manifest.generatedArtifacts, 'manifest.generatedArtifacts'); const expectedGeneratedExtensions = ['.css', '.js', '.png', '.svg', '.woff2']; if (generatedArtifacts.prefix !== '_astro/' || @@ -242,6 +252,20 @@ for (const [index, rawRoute] of manifest.routes.entries()) { console.log(`[public-sync] ${route.route} <- ${source.repository}:${sourcePath}`); } +const realDocumentationRoot = await fs.realpath(documentationRoot); +for (const [relative, expectedHash] of expectedRenderDependencies) { + const source = path.join(documentationRoot, assertSafeRelative(relative, 'render dependency path')); + const realSource = await fs.realpath(source); + if (!realSource.startsWith(`${realDocumentationRoot}${path.sep}`)) { + throw new Error(`render dependency escapes Documentation root: ${relative}`); + } + const content = await fs.readFile(realSource); + const actualHash = sha256(content); + if (actualHash !== expectedHash) { + throw new Error(`render dependency hash drift for ${relative}; expected ${expectedHash}, got ${actualHash}`); + } +} + await fs.rm(approvedPublicRoot, { recursive: true, force: true }); await fs.mkdir(approvedPublicRoot, { recursive: true }); for (const [index, rawStatic] of manifest.staticFiles.entries()) { diff --git a/web/src/public-pages/404.md b/web/src/public-pages/404.md index 189feaa9..4d26d4fb 100644 --- a/web/src/public-pages/404.md +++ b/web/src/public-pages/404.md @@ -4,7 +4,7 @@ description: The requested documentation page does not exist. template: splash editUrl: false hero: - title: "404" + title: Page not found tagline: Choose one of the current canonical product paths. actions: - text: Documentation home diff --git a/web/src/public-pages/index.mdx b/web/src/public-pages/index.mdx index dea7d26b..158b34d9 100644 --- a/web/src/public-pages/index.mdx +++ b/web/src/public-pages/index.mdx @@ -4,104 +4,100 @@ description: Canonical technical documentation for Chronicle, Arc, Components, a template: splash hero: title: Cratis documentation - tagline: "Choose a product, follow its technical documentation, and keep each product's current limits visible." + tagline: "Technical documentation for the current public Chronicle, Arc, Components, and Cratis CLI paths." image: dark: ../../assets/cratis-mark-dark.svg light: ../../assets/cratis-mark-light.svg + alt: "" actions: - - text: Explore Arc + - text: Chronicle + link: /chronicle/ + icon: right-arrow + - text: Arc link: /arc/ + variant: minimal icon: right-arrow - - text: Explore Components + - text: Components link: /components/ variant: minimal icon: right-arrow - - text: Explore the CLI + - text: Cratis CLI link: /cli/ variant: minimal icon: right-arrow - - text: Company - link: https://cratis.no - variant: minimal --- import { Card, CardGrid, LinkCard } from '@astrojs/starlight/components'; -## Product documentation +## Choose a product -Each product repository owns its technical documentation and released behavior. This site converts those sources into one navigable documentation surface. - -<CardGrid> - <Card title="Chronicle" icon="seti:db"> - Chronicle uses a .NET/Orleans actor-based kernel behind gRPC/HTTP surfaces and supports multiple event stores, namespaces, and persistent event-store subscriptions with outbox/inbox sequences. - - Chronicle and its bundled local Workbench are available as MIT-licensed self-hosted software; authorized local use is separate from paid Cratis support, hosted coordination, or managed operational responsibility. - - [Open the Chronicle documentation](/chronicle/). - </Card> - - <Card title="Arc" icon="puzzle"> - Arc is an opinionated CQRS application framework for ASP.NET Core with commands, queries, validation, authorization, and TypeScript proxy generation. - - [Open the Arc documentation](/arc/). - </Card> - - <Card title="Components" icon="laptop"> - Components is a React component library aligned with Arc application patterns. - - [Open the Components documentation](/components/). - </Card> - - <Card title="Cratis CLI" icon="rocket"> - The Cratis CLI provides terminal workflows for inspecting and diagnosing Chronicle. - - [Open the CLI documentation](/cli/). - </Card> -</CardGrid> - -## How the current products relate - -- **Event sourcing is not required to use Arc.** Arc's exact command, query, validation, authorization, and generated-contract behavior is documented in the Arc product pages. -- **Chronicle has separate browser and terminal inspection surfaces.** Chronicle Workbench provides a bundled local browser surface for authorized inspection of Chronicle runtime state and preview of supported projection behavior. The CLI provides terminal inspection and diagnosis. - -These relationships do not imply compatibility with every product version, frontend, runtime, client, provider, browser, environment, or topology. Use the exact package manifests, current product documentation, and exercised profile for the combination you evaluate. - -## Start with one bounded path +Each product repository owns its technical source and released behavior. This +site presents the current admitted documentation paths. <CardGrid> <LinkCard - title="Build a CQRS feature with Arc" - description="Follow Arc's command, query, validation, authorization, and generated TypeScript proxy path." + title="Chronicle" + description="Chronicle uses a .NET/Orleans actor-based kernel behind gRPC/HTTP surfaces and supports multiple event stores, namespaces, and persistent event-store subscriptions with outbox/inbox sequences." + href="/chronicle/" + /> + <LinkCard + title="Arc" + description="Arc is an opinionated CQRS application framework for ASP.NET Core with commands, queries, validation, authorization, and TypeScript proxy generation." href="/arc/" /> <LinkCard - title="Render an Arc command with Components" - description="Install Components, mount the provider, and render a typed command form." + title="Components" + description="Components is a React component library aligned with Arc application patterns." href="/components/" /> <LinkCard - title="Inspect Chronicle from the terminal" - description="Install the CLI, connect to a Chronicle server, and follow its documented diagnostic workflows." + title="Cratis CLI" + description="The Cratis CLI provides terminal workflows for inspecting and diagnosing Chronicle." href="/cli/" /> +</CardGrid> + +## Chronicle technical paths + +<CardGrid> + <Card title="Self-hosted software" icon="seti:db"> + Chronicle and its bundled local Workbench are available as MIT-licensed + self-hosted software; authorized local use is separate from paid Cratis + support, hosted coordination, or managed operational responsibility. + </Card> <LinkCard - title="Browse the Chronicle architecture" - description="Understand the current kernel, protocol surfaces, stores, namespaces, and subscription model." + title="Architecture" + description="Locate the kernel, protocol surfaces, event stores, namespaces, and persistent subscriptions." href="/chronicle/architecture/" /> + <LinkCard + title="Workbench" + description="Chronicle Workbench provides a bundled local browser surface for authorized inspection of Chronicle runtime state and preview of supported projection behavior." + href="/chronicle/workbench/" + /> </CardGrid> -## What this site treats as authoritative +## Keep the boundaries visible -- Product repositories own source, examples, technical documentation, package metadata, and release evidence. -- cratis.io is the canonical rendered technical navigation surface. -- [cratis.no](https://cratis.no) owns company, fit, trust, and commercial information. -- Repository READMEs and package pages summarize one product or package and link back to canonical technical documentation. - -A repository, package, example, successful build, or polished page does not establish maturity, support, security, performance, compatibility, or production suitability. Those statements require their own current evidence and approval. +<CardGrid> + <Card title="Arc and event sourcing" icon="puzzle"> + Event sourcing is not required to use Arc. Evaluate the exact Arc package + profile documented on this site. + </Card> + <Card title="Browser and terminal inspection" icon="rocket"> + Workbench provides the bounded local browser path. The Cratis CLI provides + terminal inspection and diagnostic workflows. + </Card> + <Card title="Evidence before inference" icon="approve-check"> + A repository, package, example, or successful build does not establish + maturity, support, security, performance, compatibility, or production + suitability. + </Card> +</CardGrid> -## Help improve the documentation +## More paths -- [Ask a technical question or join the community](https://discord.gg/kt4AMpV8WV) -- [Report a documentation problem](https://github.com/Cratis/Documentation/issues) -- [Browse the Cratis repositories](https://github.com/Cratis) +- [Company, fit, and trust information](https://cratis.no/) +- [Cratis repositories](https://github.com/Cratis) +- [Community discussion](https://discord.gg/kt4AMpV8WV) +- [Documentation issues](https://github.com/Cratis/Documentation/issues) From 938925de484bd5d4d4ddfb7a54e710b87b40a46a Mon Sep 17 00:00:00 2001 From: woksin <sindrewilting@gmail.com> Date: Tue, 25 Aug 2026 14:15:32 +0200 Subject: [PATCH 3/6] Pin public documentation workflow actions Bind checkout, Node setup, Pages artifact upload, and deployment to exact reviewed commits so the major deployment packet does not depend on movable action tags. --- .github/workflows/docs-site.yml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/docs-site.yml b/.github/workflows/docs-site.yml index 92e84f11..3327f8ce 100644 --- a/.github/workflows/docs-site.yml +++ b/.github/workflows/docs-site.yml @@ -21,7 +21,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout Documentation - uses: actions/checkout@v4 + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 with: path: Documentation persist-credentials: false @@ -53,7 +53,7 @@ jobs: } >> "$GITHUB_OUTPUT" - name: Checkout Chronicle - uses: actions/checkout@v4 + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 with: repository: Cratis/Chronicle ref: ${{ steps.revisions.outputs.chronicle }} @@ -61,7 +61,7 @@ jobs: persist-credentials: false - name: Checkout Arc - uses: actions/checkout@v4 + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 with: repository: Cratis/Arc ref: ${{ steps.revisions.outputs.arc }} @@ -69,7 +69,7 @@ jobs: persist-credentials: false - name: Checkout Components - uses: actions/checkout@v4 + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 with: repository: Cratis/Components ref: ${{ steps.revisions.outputs.components }} @@ -77,7 +77,7 @@ jobs: persist-credentials: false - name: Checkout CLI - uses: actions/checkout@v4 + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 with: repository: Cratis/cli ref: ${{ steps.revisions.outputs.cli }} @@ -85,7 +85,7 @@ jobs: persist-credentials: false - name: Setup Node - uses: actions/setup-node@v4 + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: node-version: 23 @@ -99,7 +99,7 @@ jobs: - name: Upload Pages artifact if: ${{ github.event_name != 'pull_request' }} - uses: actions/upload-pages-artifact@v3 + uses: actions/upload-pages-artifact@56afc609e74202658d3ffba0e8f6dda462b719fa # v3 with: path: Documentation/web/dist @@ -116,4 +116,4 @@ jobs: url: ${{ steps.deployment.outputs.page_url }} steps: - id: deployment - uses: actions/deploy-pages@v4 + uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4 From 2f233a3065351791f5e45c56b583dd1fc555c506 Mon Sep 17 00:00:00 2001 From: woksin <sindrewilting@gmail.com> Date: Tue, 25 Aug 2026 17:13:39 +0200 Subject: [PATCH 4/6] Rebind the reviewed ecosystem documentation --- web/public-surface.json | 28 ++++++++++++++++------------ web/scripts/sync-public-content.mjs | 13 ++++++++++--- web/src/components/SimpleCard.astro | 9 +++++++++ web/src/styles/cratis.css | 12 ++++++++++++ 4 files changed, 47 insertions(+), 15 deletions(-) diff --git a/web/public-surface.json b/web/public-surface.json index 49802ff8..ee03c834 100644 --- a/web/public-surface.json +++ b/web/public-surface.json @@ -42,9 +42,9 @@ "contentPath": "chronicle/index.mdx", "source": { "repository": "Chronicle", - "revision": "bbc01a8e18bda6f2d81d370ae91a8b324800773e", + "revision": "de98cf9bde7210d38bb87af35fc1727d1d061ea4", "path": "Documentation/index.mdx", - "sha256": "ce5e8a6ce7d17ba02023439fc90d3a2250fa71f75b3a584f312388edbcea57e4" + "sha256": "15b8eef95042b33e40cd69b7802bb75c35b288b0fe7f34dcd76a25e29f78f4cd" }, "claims": ["CLM-013", "CLM-014", "CLM-028", "CLM-032"], "search": false, @@ -56,9 +56,9 @@ "contentPath": "chronicle/architecture.mdx", "source": { "repository": "Chronicle", - "revision": "bbc01a8e18bda6f2d81d370ae91a8b324800773e", + "revision": "de98cf9bde7210d38bb87af35fc1727d1d061ea4", "path": "Documentation/architecture.mdx", - "sha256": "0f79925a1e1130edfff89680a627660cb64a8749454562dacc7e73333cdabb30" + "sha256": "6c02e3280288af61927c5f8b9133676067e6fef45ef11867fbc64b23ca581962" }, "claims": ["CLM-013", "CLM-014", "CLM-028"], "search": false, @@ -70,9 +70,9 @@ "contentPath": "chronicle/workbench/index.mdx", "source": { "repository": "Chronicle", - "revision": "bbc01a8e18bda6f2d81d370ae91a8b324800773e", + "revision": "de98cf9bde7210d38bb87af35fc1727d1d061ea4", "path": "Documentation/workbench/index.mdx", - "sha256": "52ada440bafa53727a031e3b4faa92c7b7a4b13750ffc24c919a2788390eaa23" + "sha256": "545a3ff4516033f6bd549f5bc2ce4a744790af04e9405232adcdb096a5c309d6" }, "claims": ["CLM-013", "CLM-014", "CLM-032"], "search": false, @@ -84,9 +84,9 @@ "contentPath": "arc/index.mdx", "source": { "repository": "Arc", - "revision": "3b54436c95004ab989a8788dc960b87f4ff40f5c", + "revision": "953dff585a9d9db9f51233e9b22c22e4a6b0f7e9", "path": "Documentation/index.mdx", - "sha256": "c4bde7b68e35c2506453f19867f415f1abf4e3a234678a3daf33ba0b98410d18" + "sha256": "4e81a9bad393128c4db148e6257993b2ec68fbfead41f7632fe051af590e456b" }, "claims": ["CLM-011", "CLM-012"], "search": false, @@ -98,9 +98,9 @@ "contentPath": "components/index.mdx", "source": { "repository": "Components", - "revision": "a1e4da5b36b3ffad67ab8b2e3a67098242dbbee2", + "revision": "d6e75bc8af3bf7ecd12d93098b1bbff7638635d1", "path": "Documentation/index.mdx", - "sha256": "1c966f3140ec1e854eef23d46257e75517a1b8224a5488346ab72db474702202" + "sha256": "181823a19ccf26e2ab03570a8b4429329c70811c3a1719756f02eb5e5e96bb53" }, "claims": ["CLM-011", "CLM-012"], "search": false, @@ -112,7 +112,7 @@ "contentPath": "cli/index.mdx", "source": { "repository": "cli", - "revision": "711013ff87ec05dc8574ee133f56c1df230e253d", + "revision": "5e8b987e08af1fc6d26faa15b25df95f2b3cee95", "path": "Documentation/index.mdx", "sha256": "cfe3c0c14547130f06ebd37d378bdaa4259a3dfd6f8955583f8187ac0e80e7cf" }, @@ -129,7 +129,11 @@ }, { "path": "web/src/components/SimpleCard.astro", - "sha256": "49d34c3d9b2dfbf12fd99fb4ecd6797ce7e34379e3434ca50be286e7d19ceda5" + "sha256": "057370dc8bc7bd91262d9ad5b28e79cb7f963093afed136717f4bdf958c75f87" + }, + { + "path": "web/src/components/YouWillLearn.astro", + "sha256": "cab0237be9dc77ec09bb7629ab16f66efa2a8e82bd6d09138f5f6ddb4456494e" } ], "generatedArtifacts": { diff --git a/web/scripts/sync-public-content.mjs b/web/scripts/sync-public-content.mjs index a41244bf..696a162b 100644 --- a/web/scripts/sync-public-content.mjs +++ b/web/scripts/sync-public-content.mjs @@ -25,7 +25,8 @@ const allowedClaims = new Set([ const expectedRenderDependencies = [ ['web/src/components/TopicHero.astro', 'a67bf5552c44b7eb53d86a1b367340d7ca9027222e4226f16e6f58be7ee262ac'], - ['web/src/components/SimpleCard.astro', '49d34c3d9b2dfbf12fd99fb4ecd6797ce7e34379e3434ca50be286e7d19ceda5'], + ['web/src/components/SimpleCard.astro', '057370dc8bc7bd91262d9ad5b28e79cb7f963093afed136717f4bdf958c75f87'], + ['web/src/components/YouWillLearn.astro', 'cab0237be9dc77ec09bb7629ab16f66efa2a8e82bd6d09138f5f6ddb4456494e'], ]; const expectedRoutePolicy = [ @@ -61,7 +62,7 @@ const repositoryRoots = { }; function firstExisting(...candidates) { - return candidates.find((candidate) => existsSync(candidate)) ?? candidates[candidates.length - 1]; + return candidates.find((candidate) => existsSync(candidate)) ?? candidates.at(-1); } function assertObject(value, name) { @@ -148,7 +149,13 @@ function topicsFor(routes) { return topics.filter((topic) => available.has(topic.id)); } -const manifest = assertObject(JSON.parse(await fs.readFile(manifestPath, 'utf8')), 'manifest'); +let manifestValue; +try { + manifestValue = JSON.parse(await fs.readFile(manifestPath, 'utf8')); +} catch (error) { + throw new Error(`public-surface.json is not valid JSON: ${error instanceof Error ? error.message : String(error)}`, { cause: error }); +} +const manifest = assertObject(manifestValue, 'manifest'); if (manifest.schemaVersion !== 1) throw new Error('manifest.schemaVersion must be 1'); if (!Array.isArray(manifest.routes) || manifest.routes.length === 0) { throw new Error('manifest.routes must be a non-empty array'); diff --git a/web/src/components/SimpleCard.astro b/web/src/components/SimpleCard.astro index 80a2e17e..91ba84cf 100644 --- a/web/src/components/SimpleCard.astro +++ b/web/src/components/SimpleCard.astro @@ -74,4 +74,13 @@ const { title, icon, link } = Astro.props; line-height: 1.5; color: var(--sl-color-gray-2); } + @media (prefers-reduced-motion: reduce) { + .cratis-feature-card { + transition: none; + } + .cratis-feature-card:hover, + .cratis-feature-card:focus-visible { + transform: none; + } + } </style> diff --git a/web/src/styles/cratis.css b/web/src/styles/cratis.css index a2e7ba61..ee1d9759 100644 --- a/web/src/styles/cratis.css +++ b/web/src/styles/cratis.css @@ -297,6 +297,18 @@ color: var(--sl-color-accent); } +@media (prefers-reduced-motion: reduce) { + .card, + .sl-link-card { + transition: none; + } + + .card:hover, + .sl-link-card:hover { + transform: none; + } +} + /* --- Headings: confident rhythm without viewport/font-width tricks --- */ .sl-markdown-content h2 { letter-spacing: 0; From 6ad260c46da32f133ab1b1a9faa4d7488cce6b39 Mon Sep 17 00:00:00 2001 From: woksin <sindrewilting@gmail.com> Date: Tue, 25 Aug 2026 23:41:59 +0200 Subject: [PATCH 5/6] Rebind public surface to reconciled product heads --- web/public-surface.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/web/public-surface.json b/web/public-surface.json index 5fb17710..3e0e769e 100644 --- a/web/public-surface.json +++ b/web/public-surface.json @@ -42,7 +42,7 @@ "contentPath": "chronicle/index.mdx", "source": { "repository": "Chronicle", - "revision": "de98cf9bde7210d38bb87af35fc1727d1d061ea4", + "revision": "0a13238cadccbab9f567885014eaf9c801211e17", "path": "Documentation/index.mdx", "sha256": "15b8eef95042b33e40cd69b7802bb75c35b288b0fe7f34dcd76a25e29f78f4cd" }, @@ -56,7 +56,7 @@ "contentPath": "chronicle/architecture.mdx", "source": { "repository": "Chronicle", - "revision": "de98cf9bde7210d38bb87af35fc1727d1d061ea4", + "revision": "0a13238cadccbab9f567885014eaf9c801211e17", "path": "Documentation/architecture.mdx", "sha256": "6c02e3280288af61927c5f8b9133676067e6fef45ef11867fbc64b23ca581962" }, @@ -70,7 +70,7 @@ "contentPath": "chronicle/workbench/index.mdx", "source": { "repository": "Chronicle", - "revision": "de98cf9bde7210d38bb87af35fc1727d1d061ea4", + "revision": "0a13238cadccbab9f567885014eaf9c801211e17", "path": "Documentation/workbench/index.mdx", "sha256": "545a3ff4516033f6bd549f5bc2ce4a744790af04e9405232adcdb096a5c309d6" }, @@ -84,7 +84,7 @@ "contentPath": "arc/index.mdx", "source": { "repository": "Arc", - "revision": "953dff585a9d9db9f51233e9b22c22e4a6b0f7e9", + "revision": "b2ec087cc8d79f1a949e99699ebae3364af10536", "path": "Documentation/index.mdx", "sha256": "4e81a9bad393128c4db148e6257993b2ec68fbfead41f7632fe051af590e456b" }, @@ -98,7 +98,7 @@ "contentPath": "components/index.mdx", "source": { "repository": "Components", - "revision": "d2ffdc0fd9518ade92eceafa9c387697c92e72e0", + "revision": "0303cb7a225ce2ee6d3e485ccec4e979a6e157ea", "path": "Documentation/index.mdx", "sha256": "181823a19ccf26e2ab03570a8b4429329c70811c3a1719756f02eb5e5e96bb53" }, @@ -112,7 +112,7 @@ "contentPath": "cli/index.mdx", "source": { "repository": "cli", - "revision": "5e8b987e08af1fc6d26faa15b25df95f2b3cee95", + "revision": "21c488a3f038edb572a612896a178d6a181f8f51", "path": "Documentation/index.mdx", "sha256": "cfe3c0c14547130f06ebd37d378bdaa4259a3dfd6f8955583f8187ac0e80e7cf" }, From 9ec001e157e87fad802a1c506c1b68402decaae9 Mon Sep 17 00:00:00 2001 From: woksin <sindrewilting@gmail.com> Date: Wed, 26 Aug 2026 10:36:06 +0200 Subject: [PATCH 6/6] chore: remove AI work artifacts from version control (moved to local-only .ai-work/) --- archive/DOCS-AUDIT-2026-07.md | 172 ----------------------- archive/DOCS-AUDIT-HANDOVER.md | 247 --------------------------------- 2 files changed, 419 deletions(-) delete mode 100644 archive/DOCS-AUDIT-2026-07.md delete mode 100644 archive/DOCS-AUDIT-HANDOVER.md diff --git a/archive/DOCS-AUDIT-2026-07.md b/archive/DOCS-AUDIT-2026-07.md deleted file mode 100644 index 22156226..00000000 --- a/archive/DOCS-AUDIT-2026-07.md +++ /dev/null @@ -1,172 +0,0 @@ -> [!NOTE] -> **Historical record — written 2026-07-16. Not a live plan.** -> -> This audit is preserved here as a **record of what the documentation looked like on -> 2026-07-16**, not as a backlog anyone is currently working. It was never tracked in git -> until this commit and sat untracked in a working copy for a month. -> -> **Its findings are largely unaddressed.** A follow-up sweep re-checked the audit's -> concrete claims and found roughly **12 of 49 checks** now passing — and of those, close to -> **0% are attributable to the audit itself**; they were fixed by unrelated work that -> happened to touch the same pages. A spot-check on 2026-08-19 re-verified five of the P0 -> findings against the relevant repositories' `main` branches and found **all five still -> open**: Arc's `using OneOf;` in `Documentation/backend/commands/model-bound/index.md`, the -> two "will be documented here" placeholders in `Documentation/backend/proxy-generation.md`, -> the `beforeExecute` (vs `onBeforeExecute`) prop in `form-lifecycle.md`, the "TypeScript 6" -> line in `web/src/content/docs/compatibility.mdx`, and the stale "Latest release digest" -> card in `web/src/content/docs/whats-new.mdx`. -> -> **Treat every specific claim below as dated.** Line numbers, page counts, percentages, and -> file paths were accurate on 2026-07-16 and have drifted since. Re-verify anything before -> acting on it. The audit's *method* and *framing* have aged better than its *citations*. -> -> Kept because the analysis is substantive and re-deriving it would cost more than reading -> it — not because it is current. - -# Cratis Documentation Audit — 2026-07-16 - -A full content, structure, and site audit of the Cratis documentation (the `Cratis/Documentation` Astro Starlight site aggregating Chronicle, Arc, Components, CLI, Fundamentals, Architecture, Screenplay, Prompter, AuthProxy, Contributing) and the live **cratis.io**. - -**Method.** Fresh `npm run build` + every QA gate; first-hand link/frontmatter/diagram/heading censuses across all ~873 rendered pages; live-site + external-link probes; design/AI-export review; and five parallel deep-dive audits (one per product area) whose every code claim was verified against product `Source/`. Environment note: product siblings were on feature branches, so local content ≈ but is not identical to production `main`. - ---- - -## Executive summary - -**Verdict: a strong, professionally-presented documentation set with an excellent flagship layer — held back by a small number of concrete correctness bugs, one pervasive metadata gap, and structural/IA debt in the machine-converted reference tree.** This is a "raise a good site to a great one" list, not a rescue. - -The site is genuinely good where the team invested by hand: the front door, the per-product Overview/"Understanding" pages, the tutorials, and the scenarios read like a real teacher's tour (pain→relief, why-before-how, diagrams, synced C#↔TypeScript). The presentation layer (884-line brand CSS, rich custom components) is well above a default Starlight skin. Build passes; internal-link hygiene is excellent (1 genuinely broken internal link across 339 Chronicle pages). - -The weaknesses cluster into five themes: - -1. **Copy-paste code bugs** — a few widely-repeated examples don't compile / use removed or invented APIs. Highest severity because readers paste them verbatim. -2. **Systemic metadata gap** — 86% of pages have no `description` frontmatter (SEO, social cards, and the `llms.txt` AI export all read it). -3. **A two-tier quality split** — the hand-authored narrative layer is excellent; the bulk-converted DocFX reference tree is passive-voiced, diagram-less, description-less, and carries structural collisions. -4. **Structure & IA debt** — file-vs-folder URL collisions, orphaned sections, CI artifacts shipped as user docs, and an overloaded top-level nav topic that buries whole products. -5. **The full-stack differentiator is under-shown** — `<FullStackTabs>` (synced C#↔TS), the thing that most distinguishes Cratis, appears on a handful of pages and is absent from the reference where it matters most. - -### Scorecard - -| Area | Pages | Health | One-line | -|---|---|---|---| -| Front door / homepage | — | ★★★★★ | Strong hero, clear value prop, copy-pasteable quickstart, good CTAs. | -| Design / presentation | — | ★★★★★ | Hand-crafted brand theme + components; a real strength to preserve. | -| Chronicle | 339 | ★★★★ | Excellent where invested; CI artifacts leak, some contradictions, 89% no description. | -| Arc | 266 | ★★★½ | Superb narrative tier; reference tier has 4 URL collisions + 3 wrong code examples. | -| Components | 91 | ★★★½ | Great recipes; ~75 auto-ref pages weaker; removed `primereact/column` import ×9. | -| CLI | 31 | ★★★½ | Strong content, but `scenarios/` + reference landing orphaned from nav. | -| Prompter | 15 | ★★★★★ | Mature, tour-voiced, ready to surface. | -| AuthProxy | 6 | ★★★★★ | Hand-authored, diagrammed, honest security caveats. | -| Screenplay | 19 | ★★★★ | Substantive; "run it live" promise outruns shipped tooling — frame as Preview. | -| Fundamentals | 45 | ★★★ | Modernized landings only; deep `.md` are reference-dumps, ~1 diagram, no descriptions. | -| Architecture | 29 | ★★★ | Correct terse Roslyn-rule reference; thin landing, duplicate index tables. | -| Contributing | 18 | ★★★ | Solid but older; `building.md` omits frontend; release-digests orphaned. | -| IA / navigation | — | ★★½ | Overview topic overloaded (~23 entries); products buried; two "Scenarios". | -| QA tooling | — | ★★★ | Good gates, but 2 of 6 silently no-op locally; `check` needs API+Storybook builds. | - ---- - -## Priority 0 — Correctness & things users hit (fix first) - -### P0.1 — Broken / invented copy-paste code -- **[Components · High] Removed `primereact/column` import taught in 9 places.** `Column` is imported from `primereact/column`, but `Components/Source/DataTables/Column.tsx` is the Cratis-owned typed replacement (its own doc-comment says it replaces the *removed* `primereact/column`) and is exported from the `@cratis/components/DataTables` barrel. Readers get a broken import. Locations: `components/displaying-data.md:17`, `components/list-screen-with-actions.md:19`, `components/tutorial/list-it.mdx:18`, `components/tutorial/list-and-detail.mdx:33`, `components/DataTables/data-table-for-query.md:24`, `components/DataTables/data-table-for-observable-query.md:24`, `components/DataPage/index.md:26` & `:57`, `components/DataPage/menu-items.md:13`. **Fix:** `import { Column } from '@cratis/components/DataTables'`. -- **[Arc · High] Invented `CommandResult` API.** `arc/frontend/react/command-form/validation.md` (~392–439) uses `result.hasErrors('email')` / `result.getErrorsFor('email')` and an `errors` map. Verified against `Arc/Source/JavaScript/.../ICommandResult.ts`: only `isSuccess/isAuthorized/isValid/hasExceptions/validationResults/response` exist; `hasErrors`/`getErrorsFor` have zero project hits. **Fix:** use `validationResults` (field errors come from the form context's `getFieldError`). -- **[Arc · High] Wrong form-lifecycle prop.** `arc/frontend/react/command-form/form-lifecycle.md` (~158–171) documents `beforeExecute` returning `Promise<boolean>` to cancel submission. The real prop is `onBeforeExecute` (a value transform, `CommandForm.tsx:313`), not a boolean gate — and `command-form/index.md:69` already lists it correctly. Same page (`:12`) uses the wrong import `@cratis/arc/commands` (core) for a React component → should be `@cratis/arc.react/commands`. -- **[Components · Medium] `column-configuration.md` documents PrimeReact's `Column` surface** (`filterMatchMode`, `filterElement`, `exportable`, `frozen`, `headerStyle`) but shipped `ColumnProps<TData>` is a different curated surface (`dataType`, `showFilterMatchModes`, typed `body`). Rewrite against the real props. -- **[Components · Medium] `Dropdown/index.md:181-192`** uses PrimeReact `<Dialog visible onHide>` (unimported) contradicting the Cratis `Dialog` (`onConfirm/onCancel/onClose`). Use the Cratis dialog. -- **[Arc · Low] `using OneOf;` should be `using Cratis.Monads;`** in `arc/backend/commands/model-bound/index.md:38,65,185` — the `Result<TResult,TError>` guards won't compile as written. - -### P0.2 — Factual contradictions & wrong statements -- **[Chronicle · High] Event Revision contradiction.** `chronicle/concepts/event-sequence.md:48-50` says revisions are "not fully implemented yet, there is no API surface"; `chronicle/events/revision.md` documents a working Workbench procedure for the same feature. Reconcile to one truth. -- **[Chronicle · High] Three conflicting "three projection approaches".** `projections/architecture.mdx:9` (PDL/Model-Bound/Declarative) vs `projections/index.md:28` (Model-bound/Declarative/Reducer) vs `projections/choosing-a-read-model-style.mdx` (Model-bound/Declarative/Reducer). Pick one canonical framing; surface PDL (currently orphaned from the landing). -- **[Chronicle · High] `.AutoMap()` taught against the project's own rule + analyzer.** ~10 declarative pages (`projections/declarative/{event-context,from-event-sequence,functions,joins,not-rewindable,passive,remove-with-join,set-properties,index}.mdx`, `model-bound/index.mdx`) call `.AutoMap()` as routine boilerplate, while `projections/declarative/simple-projection.mdx:83` correctly says never to. Remove the routine calls. -- **[Chronicle · Medium] Wrong glossary term.** `chronicle/concepts/glossary.md:85` defines **Subject** as "an observable stream… behind reactive queries" — but the compliance docs and the site glossary define Subject as the PII/GDPR identity. Correct it. -- **[Chronicle · Medium] MongoDB-only claim.** `chronicle/concepts/event-store.md:11-13` presents MongoDB as the only backend, contradicting `index.mdx` + `hosting/configuration/storage.md` (Mongo/Postgres/SQL Server/SQLite). -- **[Site · Medium] `compatibility.mdx:26` says "TypeScript 6"** — Arc ships `typescript 7.0.2` (verified `Arc/package.json`; Fundamentals/Components on 6.0.3). Node ≥23 and Yarn 4.5.3 on that page are correct — leave them. -- **[Site · Medium] `whats-new.mdx` stale.** The "Latest release digest" card points to `2026-05-25-to-2026-06-01`, the 2nd-oldest of 6 digests (newest is `2026-06-22-to-2026-06-29`). -- **[Site · Medium] Studio described in present tense** (`why-cratis.mdx:58`, `cratis-stack.mdx:19`) though it's badged "Soon". Use future framing to match `studio.mdx`'s own honesty. - -### P0.3 — Structural collisions, orphans, and shipped placeholders -- **[Arc · High] Four file-vs-folder URL collisions.** A `foo.md` sibling to a `foo/` folder makes the sync demote the folder's `index.md` to `overview.md`, leaving the flat file as the landing: `backend/mongodb.md` (tells readers to "visit /arc/backend/mongodb/" — its own URL; real content orphaned at `/mongodb/overview/`), `backend/proxy-generation/configuration.md` (self-referential "moved" stub), `backend/chronicle/commands.md` ("moved" banner atop 150 lines duplicating `backend/chronicle/commands/`), and `frontend/core/queries.md` (167 lines) vs `frontend/core/queries/overview.md` (22 lines) — two pages titled "Queries", the stale controller-based one is what everything links to. Fix: one shape per section (real `index.md`, delete the sibling). -- **[Chronicle · High] CI artifacts shipped as user docs.** `chronicle/statistics/` embeds a coverage dashboard via raw `<iframe src="coverage.html">` (an unrouted file → 404 in the built site) plus a body-H1. `chronicle/benchmarks/` is raw `index.html` + `data.js` + an 887-line JSON with **no markdown page at all**. Move reporting out of the user docs; drop/replace these entries. -- **[CLI · Critical] `cli/scenarios/` orphaned from the sidebar.** `cli/Documentation/toc.yml` never references `scenarios/toc.yml`, so the four best CLI how-tos are unreachable via nav (only inline links). Also **`cli/reference/index.md` is orphaned** (`reference/toc.yml` omits it). Add both to their tocs. -- **[Arc · High] Live placeholder text in a shipped page.** `arc/backend/proxy-generation.md:79,83` — "Configuration details… will be documented here." / "Usage examples… will be provided in this section." Write the content or remove the headings. -- **[Chronicle · Critical] Live "in progress" banner.** `chronicle/projections/immediate-projections.md` opens "This documentation is in progress and will be updated soon" over one-sentence sections, yet is linked as a real topic. (Body is ~half-written; finish it, add a sync-vs-eventual diagram + "when not to use", drop the banner.) -- **[Chronicle · Medium] The one genuinely broken internal link:** `chronicle/clients/kotlin/get-started/index.md:12` → `/chronicle/clients/kotlin/./` (stray `.`). - ---- - -## Priority 1 — Systemic hygiene (do in bulk) - -- **[All · High] `description` frontmatter missing on 747 of 873 pages (86%).** Per area: Chronicle 301/339, Arc 229/266, Components 77/91, Fundamentals 44/45, CLI 29/31, Architecture 29/29, Contributing 17/18, Screenplay 13/19, Scenarios 8/12. Prompter/AuthProxy/Tools are fully covered — proof it's achievable. The converter copies `description` only when source frontmatter has it (it never synthesizes), so **every fix belongs in the product source repos.** Biggest single lever for SEO, search, social cards, and the `llms.txt` export. -- **[All · High] The `<FullStackTabs>` differentiator is under-used.** Arc shows it on 4 pages (none in the command/query/form reference); it's the clearest expression of full-stack type safety. Add C#↔generated-TS views to `arc/backend/commands/model-bound/index.md`, `queries/model-bound/index.md`, and the frontend command/query reference. -- **[All · Medium] Low diagram density.** Only 38/339 Chronicle, 21/266 Arc, **2/91 Components, 1/45 Fundamentals**, 0/29 Architecture (acceptable for reference). Concept pages lacking a diagram include Chronicle `concepts/{event-type-migrations,modeling-events,tagging,correlation-identity-causation,designing-read-models,geospatial}.mdx`, `projections/choosing-a-read-model-style.mdx` (a decision page begging for a flowchart), `reducers/index.mdx`, `compliance/index.md`, `subscriptions/index.md`; and the Fundamentals derived-types/serialization/metrics concept pages. The client-SDK trees (Elixir/Kotlin/TS, ~60 pages) are essentially diagram-free. -- **[All · Low] Heading case.** Title-Case headings violate the sentence-case rule pervasively: ~197 in Chronicle `projections/`, ~106 in `code-analysis/`, 369 across Components+CLI, ~17 in Arc. Bulk-downcase in source. -- **[All · Low] American English slips:** Chronicle `projections/pdl/nested.mdx:289`, `code-analysis/CHR0017.mdx:73` ("behaviour"); Components `migration.md:451` & `Styling/cratis-tokens.md:27` ("catalogue"), `Filter/index.md:225` ("initialises"), `DataPage/details-panel.md:42` ("signalled"), `Toolbar/drag-and-drop.md:48` ("Serialised"); Arc ~5 files ("behaviour"/"serialised"); Contributing `logging.md:13` ("labour"); Scenarios (7 files: behaviour/initialised/optimised/serialisation). -- **[Arc/Chronicle · Low] Untagged or non-standard code fences:** Chronicle (elixir connection-strings, ts getting-started, contributing/clients, pdl/joins); Arc (`flow`, `env`, `gitignore`); CLI example-output blocks untagged. Tag with a real language or `text`. -- **[All · Low] "The Arc" → "Arc"** in 17 Arc files; **npm vs yarn** mixed in JS install examples (project standardizes on Yarn 4) — pick one or use package-manager tabs. - ---- - -## Priority 2 — Structure, IA & coverage - -### Information architecture (astro.config.mjs) -- **[High] The "Cratis Stack" overview topic is an overloaded catch-all** (~23 top-level entries spanning all four Diátaxis types) that buries whole products: AuthProxy (6 pages), Studio, and Screenplay sit as collapsed items/loose leaves next to community/meta pages. Promote real products to their own icon-rail topics or a "Products" group; move meta/community/testing into clearer buckets. -- **[Medium] Two competing "Scenarios"** — site `/scenarios/` vs product `/chronicle/scenarios/`; `api-reference.md` links the latter while nav points at the former. Differentiate or unify. - -### Redundancy / overlap (sharpen boundaries, cross-link instead of re-explaining) -- **[Medium-High] Site "map" pages overlap heavily** — the Arc→Mongo/EF/Chronicle mermaid, the product card grid, and the standalone-vs-together narrative are each repeated across `index.mdx`, `why-cratis.mdx`, `cratis-stack.mdx`, and `adopting-cratis.mdx`. -- **[Medium] `showcase.mdx` ≈ `samples.mdx`** (same three samples); showcase's "architectures" promise is thin. Fold in or make it genuine reference architectures. -- **[Medium] Arc: Arc.Core-vs-ASP.NET-Core explained 4×** (`backend/overview.md`, `core/index.md`, `core/overview.md`, `asp-net-core/index.md`; the first two duplicate within one folder). **Twin aggregate pages** (`aggregate-root.md` 347L, `aggregate-roots.md` 233L). `backend/overview.md` duplicates `backend/index.md`. -- **[Medium] Chronicle: event-evolution explained 3×** (`understanding-event-evolution.md`, `concepts/event-type-migrations.mdx`, `migrations/index.md`). Three near-parallel hosting walkthroughs (`get-started/{console,worker,aspnetcore}.mdx`, ~880 lines each) — extract shared `[!INCLUDE]` steps. -- **[High] Two parallel, unreconciled CommandForm doc sets** — Components (`components/CommandForm/*`, fields inside `<CommandDialog>`) and Arc (`arc/frontend/react/command-form/*`, `<CommandForm command={...}>`). Both are real; prop tables and field names diverge (Components `DropdownField/MultiSelectField/SliderField` vs Arc `SelectField/RangeField`), and neither links the other, so it reads as contradictory. Add an orientation aside + cross-link on both `index` pages; make one canonical for field props. - -### Undocumented shipped surface -- **[Components · High]** No docs at all for the `Display` module (Avatar, Badge, Chip, ProgressBar, Skeleton, Tag), the `Notifications` module (Toaster/toast/toastCommandResult), or 3 CommandForm fields (PasswordField, RatingField, ToggleSwitchField — docs cover 12 of 15). -- **[Chronicle · Medium] Java client: 0 pages** (Kotlin 20, Elixir 24, TypeScript 20, .NET via shared docs) — yet client pages claim "synchronized examples for C#, Kotlin, Java, Elixir, and TypeScript". The C#↔other-language tab coverage is ~half-synchronized (512 C# tabs vs ~252–266 each), and many pages wrap a lone C# block in a one-option `<Tabs>`. Add the missing tabs, or drop the wrapper with an honest coverage note. -- **[Fundamentals · Medium] Broken toc entry** — `Fundamentals/Documentation/typescript/toc.yml:6` lists a "Coordinate" page (`coordinate.md`) that doesn't exist; it's silently dropped from the sidebar every sync. Write it or remove the entry. -- **[Contributing · High] `release-digests/` (6 pages) orphaned + internal.** Not in `.github/toc.yml`, machine-generated titles, no descriptions, and bullets carry internal implementation detail the PR-writing rule says must not be user-facing. Curate into `/whats-new` and exclude the raw files, or give them a landing + toc + real titles. - ---- - -## Priority 3 — Tour-voice, depth & maturity framing - -- **[Fundamentals · High] Deep pages fail the tour-voice bar** (reference-dump/marketing openings): `typescript/index.md`, `serialization/index.md`, `metrics/index.md`, `metrics/roslyn.md`, `serialization/derived_types.md`, `field_decorator.md`, `derived_types_integration.md`. And `typescript/index.md` is far weaker than its C# sibling — bring to parity. -- **[Arc · Medium] Controller-based framing presented as the default** on `frontend/core/queries.md` ("controller actions"), `frontend/react/commands/index.md:5`, `frontend/react/proxy-generation.md` (full `: Controller` example) — contradicts Arc's model-bound house default. Reframe to `[Command]`/`[ReadModel]`; mention controllers only as the escape hatch. -- **[Arc · Medium] MVVM is the thinnest frontend area** despite being the app-profile default: `react.mvvm/mvvm-context.md` (24L explains little), `tsyringe.md` (typos "bee able", "Rect MVVM"), `identity.md` (33L). Meanwhile hooks/CommandForm are over-documented (several 450–630L pages). -- **[Chronicle · Medium] Reference-dump concept pages** — `reducers/index.mdx` (242L, no diagram, weaker than `projections/index`), `projections/model-bound/index.mdx` (623L). Oversized model-bound pages (`convention-based.mdx` 1171L, `children.mdx` 1024L, `basic-mapping.mdx` 968L) mix how-to + reference + comparison ~5× the 200-line guide — split into focused how-to + reference table. -- **[Components · High] Speculative "(If implemented)" filler** documenting behavior that may not exist: `PivotViewer/interactions.md:102-110`, `TimeMachine/navigation.md:36,57-64`, `TimeMachine/views.md:163-173`, `SchemaEditor/editing.md:154-175` (a fabricated undo/redo example), `ObjectNavigationalBar/index.md:244-252`. Verify against source; document what exists, delete the rest. And "when NOT to use" guidance is absent from nearly every component reference page. -- **[Screenplay · Medium] "Run it live" promise outruns shipped tooling** — index/why/overview lead on Stage running a `.play` live, but getting-started stops at the editor and Studio is "Soon". Add a Preview/maturity badge. **[Studio]** `studio.mdx` uses emoji card icons + a raw `<div class="sl-card-grid">` instead of `<CardGrid>` + `seti:` icons. -- **[Architecture · Medium] Thin topic** — `architecture/index.md` is a 25-word landing; the 26 `CRARCH*` rule pages are far thinner than Chronicle's `CHR*` analyzer pages (no code example, no "why this rule exists"). Duplicate 26-row index tables in `CodeAnalysis/index.md` and `CodeAnalysis/Rules/index.md`. CRARCH0011 documents a 400-line threshold while `code-quality.md` states 200 — reconcile. -- **[Contributing · Medium] `building.md` omits the entire frontend build/test** — lists Node 23 as a prereq but documents only `dotnet build`/`test`; no `yarn`/`tsc`/lint, no Debug-vs-Release (proxy regen) note. - ---- - -## Tooling & process - -- **[Medium] 2 of 6 `npm run check` gates silently no-op locally.** `lint:prose` (Vale) and `check:external` (lychee) print "not installed — skipping" and pass, so a contributor gets a false green on prose + external links. Assert the tools in CI, or make the scripts warn louder. -- **[Medium] `npm run check` can't pass on a plain local build** — the 8 "broken" links it reports are all `/api/*` and `/storybook*` paths produced by separate `build:api` / `build:storybooks` steps (all 200 on production). Either run those in the `check` chain or teach `check-links` to skip them. -- **[Low] `chronicle-client-docs:check` fails on a false-positive** — it flags `chronicle/code-analysis/CHR0037.mdx` for a raw C# fence, but the `CHR*` pages document C#-only Roslyn analyzers where a single-language fence is legitimate. Exempt `code-analysis/**` from the shared-doc audit, or wrap the fence. -- **[Low] `__pycache__` dirs** can leak into the generated content tree from the snippet-validation script — exclude from sync. -- **[Low] External-link freshness** — 9 links use the legacy `docs.microsoft.com` domain with pinned old `?view=aspnetcore-5.0/6.0` versions (all still 200 via redirect). Modernize to `learn.microsoft.com` + current versions. - ---- - -## Strengths to preserve - -- **Front door** — strong rotating hero, real value prop, copy-pasteable "get started in 3 steps", full-stack C#↔React example, platform cards, "choose your starting point" table, coming-from bridges. -- **Design/presentation** — 884-line hand-crafted brand CSS (ambient hero, tiered sidebar, brand-tinted code chips, CSS-var Mermaid theming, frosted header) and rich custom components (RotatingHero, StackJourney, StorybookEmbed, FullStackTabs). Well above default Starlight. -- **Flagship narrative pages** — `event-modeling.mdx`, `why-cratis.mdx`, Chronicle `index.mdx`/`read-models`/`projections`, Arc `understanding-*`, Components recipes, `cli/getting-started` are model pages: pain→relief, diagrams, honest "when it's the wrong fit". -- **AI export** — `/llms.txt` well-formed, `/llms-full.txt` comprehensive (~550k words). (Minor: the root `llms.txt` could enumerate key pages as a curated index.) -- **Link hygiene** — internal links essentially clean; external links healthy. -- **No TODO litter** — exactly one "in progress" banner across the whole site. - ---- - -## Suggested sequencing - -1. **Week 1 — P0.** Fix the copy-paste code bugs, the factual contradictions, the file-vs-folder collisions, the orphaned CLI sections, and pull the CI artifacts out of Chronicle. Small diffs, high reader impact. -2. **Weeks 2–3 — P1 in bulk.** A scripted pass to add `description` to every source page (even a one-liner), downcase Title-Case headings, and fix spelling/fences. Add `<FullStackTabs>` to the top command/query/read-model reference pages. -3. **Ongoing — P2/P3.** IA re-grouping (promote products out of the overview topic), de-duplicate the overlapping "map"/CommandForm/event-evolution content, document the undocumented Components modules, and lift the reference-dump pages toward the tour-voice bar section by section. - -*Every product's detailed, file-cited findings were captured during the audit; this report consolidates and prioritizes them.* diff --git a/archive/DOCS-AUDIT-HANDOVER.md b/archive/DOCS-AUDIT-HANDOVER.md deleted file mode 100644 index bde52732..00000000 --- a/archive/DOCS-AUDIT-HANDOVER.md +++ /dev/null @@ -1,247 +0,0 @@ -> [!NOTE] -> **Historical record — written 2026-07-16. Not a live plan. Do not paste this as a prompt.** -> -> This handover was written to be pasted into a fresh session as a working brief. **It is no -> longer safe to use that way** and is preserved only as a record of the plan that existed on -> 2026-07-16. It was never tracked in git until this commit. -> -> **The backlog it carries is largely unexecuted.** A follow-up sweep found roughly **12 of -> 49** of the companion audit's checks now passing, with close to **0% attributable to this -> plan** — the passing ones were fixed by unrelated work. A spot-check on 2026-08-19 -> confirmed five of the P0 items are still open on the relevant repositories' `main` -> branches. -> -> **The mechanics described in section 2 are the most perishable part.** Paths, the sync -> script's behaviour, the routing table, and the verify loop were accurate on 2026-07-16 and -> may have changed. Verify against the current repository before relying on any of it. -> -> The companion audit is at `archive/DOCS-AUDIT-2026-07.md`. - -# Handover — Cratis documentation improvement (from the 2026-07-16 audit) - -> **How to use this file.** Paste it as your first message in a fresh session opened at -> `/Volumes/sourcecode/repos/cratis`. It carries everything you need: the mission, the -> non-obvious mechanics of this docs system, which repo each fix lives in, the verify loop, -> and a prioritized backlog. The exhaustive, file-cited findings live in -> **`Documentation/DOCS-AUDIT-2026-07.md`** — read it before starting. Do the P0 items first. - ---- - -## 1. Mission - -Raise the Cratis documentation from "good" to "excellent" by executing the audit backlog. -The site is already strong (great front door, strong flagship pages, well-crafted brand -theme, clean internal links). The work is: fix a handful of correctness bugs, close one -systemic metadata gap, repair structural collisions, and lift the machine-converted -reference tree toward the tour-voice bar. **Do not restyle the site or rewrite the good -pages** — this is a punch-list, not a redesign. - -Work in **small, reviewable PRs, grouped by theme** (e.g. "fix broken code examples", -"add descriptions to Chronicle", "un-orphan CLI scenarios"). Verify every change. - ---- - -## 2. How this docs system works (read this — it is not obvious) - -- The site is **Astro Starlight** in `Documentation/web/`. It **aggregates** docs from each - product repo. Run everything from `Documentation/web`. -- **Source of truth = each product repo's `Documentation/` folder.** A build step - (`web/scripts/sync-content.mjs`) converts that DocFX-style Markdown into Starlight content - under `web/src/content/docs/<product>/`. -- **`web/src/content/docs/<product>/` is GENERATED and git-ignored. NEVER edit it.** - Edit the product source, then re-sync. (The audit's `src/content/docs/...` citations are - where a page *renders*; fix it at its *source* path — see the routing table below.) -- **Exception — site-level pages are authored directly in `web/` and tracked in git:** - `web/src/content/docs/*.mdx|*.md` (index, why-cratis, cratis-stack, adopting-cratis, - compatibility, whats-new, studio, faq, learning-paths, glossary.md, …) plus the - `scenarios/`, `authproxy/`, and `tools/` subtrees, and the navigation in - `web/astro.config.mjs`. These you edit in the Documentation repo itself. -- **Product source resolves sibling-first, submodule-fallback.** The sync prefers a sibling - clone at `/Volumes/sourcecode/repos/cratis/<Repo>/Documentation` (so the branch you have - checked out wins); if absent it falls back to the git submodule inside `Documentation/`. -- **The converter copies `description` frontmatter only if the source already has it — it - never synthesizes one.** So every "missing description" fix must be made in source. - -### Routing table — where each area's source lives - -| Area (renders at) | Edit the source in | Notes | -|---|---|---| -| `chronicle/**` | `Chronicle/Documentation/` | Client SDK pages (`clients/kotlin|elixir|typescript`) come from the **Chronicle.Kotlin / .Elixir / .TypeScript** repos via `web/scripts/chronicle-client-docs-config.mjs`. | -| `arc/**` | `Arc/Documentation/` | The "Arc" repo is the ApplicationModel repo cloned as `Arc`. | -| `components/**` | `Components/Documentation/` | Component source for verifying APIs: `Components/Source/<Component>/`. | -| `cli/**` | `cli/Documentation/` | | -| `fundamentals/**` | `Fundamentals/Documentation/` | | -| `architecture/**` | `Architecture/Documentation/` | | -| `screenplay/**` | `Screenplay/Documentation/screenplay/` | | -| `prompter/**` | `Prompter/Documentation/` | | -| `contributing/**` | the **Cratis/.github** repo (submodule `GitHubLanding`) | `release-digests/` are machine-generated. | -| site pages, `scenarios/**`, `authproxy/**`, `tools/**`, nav | **`Documentation/web/` itself** (tracked) | Edit directly here. | - ---- - -## 3. The verify loop (do this for every change) - -```bash -cd Documentation/web -npm install # first time only -npm run dev # or: npm run sync (regenerates content from source) -# ...make the edit in the SOURCE location, then: -npm run sync # or restart dev; re-converts from source -npm run check # build + lint:docs + check:links + lint:prose + lint:markdown + check:external + chronicle-client-docs -``` - -**Gate caveats you must know (discovered in the audit):** -- `npm run check` **cannot pass on a plain build** in isolation — its internal-link check - reports `/api/*` and `/storybook*` links as broken unless you also run `npm run build:api` - and `npm run build:storybooks` first. Those links are live (200) in production. Either run - those builds before `check:links`, or verify the 8 known link "failures" are only those - paths. -- **`lint:prose` (Vale) and `check:external` (lychee) silently no-op if the tools aren't - installed** (they print "not installed — skipping" and pass). Install Vale + lychee to - actually exercise prose and external-link gates locally, or rely on CI. -- The sync prints `1 broken toc entries dropped` — that's the Fundamentals TS "Coordinate" - entry (P2 item). It should read `0` once fixed. -- **Verify code examples against real source before "fixing" them.** The audit already - distinguished real bugs from correct-but-suspicious code; when in doubt, grep the product - `Source/` (e.g. `Arc/Source/JavaScript`, `Components/Source`) to confirm an API exists. -- **Branch caveat:** the audit ran with product siblings on feature branches, not `main`. - Confirm which branch each product repo is on and target the right one; the production site - builds from `main`. - -**Definition of done for a change:** source edited (not generated), `npm run sync` clean, -`npm run check` green (0 errors, 0 broken links, `0 broken toc`), page sits in the right nav -bucket, and any code example is verified against source. Follow the repo's American-English, -sentence-case-headings, and descriptive-link-text rules. - ---- - -## 4. The quality bar (read these first) - -In `Documentation/.ai/rules/`: -- `writing-cratis-docs.md` — the tour voice (pain→relief, why-before-how, honest limits), - Diátaxis page types, and the Starlight authoring components (`<Steps>`, `<Tabs>`, - `<FullStackTabs>`, diagrams). -- `documentation-structure-and-formatting.md` — frontmatter, no-body-H1, headings/ToC, - asides, code fences, links, trailing newline. -- `writing-correct-examples.md` — verify every framework API against real source. - -Relevant skills (invoke via the skill tooling): `edit-cratis-docs`, `add-cratis-docs-page`, -`qa-cratis-docs` (headless light/dark visual QA), `write-documentation`. - ---- - -## 5. The backlog (prioritized) - -Full detail with every `file:line` and a concrete fix is in -**`Documentation/DOCS-AUDIT-2026-07.md`**. Condensed here so this handover stands alone. - -### P0 — Correctness & things users hit (do first; mostly small diffs) - -1. **Broken/invented copy-paste code** - - **Components:** the removed `import { Column } from 'primereact/column'` appears in **9 - places** — replace with `import { Column } from '@cratis/components/DataTables'`. - (`components/displaying-data.md:17`, `list-screen-with-actions.md:19`, - `tutorial/list-it.mdx:18`, `tutorial/list-and-detail.mdx:33`, - `DataTables/data-table-for-query.md:24`, `data-table-for-observable-query.md:24`, - `DataPage/index.md:26,57`, `DataPage/menu-items.md:13` — all under `Components/Documentation/`.) - - **Arc:** `arc/frontend/react/command-form/validation.md` invents `CommandResult.hasErrors()` - / `getErrorsFor()` (use `validationResults` / form-context `getFieldError`); - `form-lifecycle.md` documents a non-existent `beforeExecute` (the real prop is - `onBeforeExecute`, a value transform) and imports from `@cratis/arc/commands` instead of - `@cratis/arc.react/commands`. Source: `Arc/Documentation/`. - - **Components:** `DataTables/column-configuration.md` documents PrimeReact's Column props, - not the shipped `ColumnProps<TData>`; `Dropdown/index.md:181-192` uses PrimeReact's - `<Dialog onHide>` instead of the Cratis dialog. Arc `commands/model-bound/index.md` uses - `using OneOf;` where it should be `using Cratis.Monads;`. - -2. **Unreachable / broken-in-build content** - - **CLI:** add `Scenarios` (`href: scenarios/toc.yml`) and the `reference` Overview to - `cli/Documentation/toc.yml` / `reference/toc.yml` — both sections are orphaned from nav. - - **Arc:** 4 file-vs-folder URL collisions (`backend/mongodb.md`, - `backend/proxy-generation/configuration.md`, `backend/chronicle/commands.md`, - `frontend/core/queries.md`) — collapse each to one `index.md`, delete the sibling `.md`. - - **Chronicle:** move the CI artifacts out of the user docs — `chronicle/statistics/` - (coverage `<iframe>` to an unrouted file → 404) and `chronicle/benchmarks/` (raw - HTML/JS/887-line JSON, no page). - -3. **Factual contradictions / wrong statements** - - Chronicle Event Revision "not implemented" (`concepts/event-sequence.md:48`) vs the - working procedure in `events/revision.md` — reconcile. - - Chronicle: three conflicting "three projection approaches"; `.AutoMap()` taught against - the project's own rule in ~10 declarative pages; wrong "Subject" glossary definition - (`concepts/glossary.md:85`); MongoDB-as-only-backend (`concepts/event-store.md:11`). - - Site (edit in `web/`): `compatibility.mdx:26` says "TypeScript 6" — Arc ships **7.0.2**; - `whats-new.mdx` "Latest" points to the 2nd-oldest of 6 digests; `why-cratis.mdx:58` / - `cratis-stack.mdx:19` describe Studio ("Soon") in present tense. - -4. **Shipped placeholders** — Arc `backend/proxy-generation.md:79,83` ("will be documented - here"); Chronicle `projections/immediate-projections.md` "in progress" banner (page is - ~half-written — finish it and drop the banner). - -### P1 — Systemic hygiene (bulk passes) - -- **Add `description` frontmatter to the 747/873 pages that lack it** (Chronicle 301, Arc 229, - Components 77, Fundamentals 44, CLI 29, Architecture 29, Contributing 17, Screenplay 13, - Scenarios 8). One sentence each, in **source**. Biggest single lever (SEO, social, llms.txt). - Consider a script that adds a `description:` derived from the page's intro where missing. -- **Add `<FullStackTabs>` (synced C#↔generated-TS)** to the model-bound command/query/read-model - reference pages — the differentiator is barely shown in the reference tree. -- **Add a Mermaid diagram per non-trivial concept page** (Components 2/91 and Fundamentals - 1/45 are the worst; also Chronicle concept pages listed in the report). -- **Sentence-case headings** (Title-Case is pervasive: ~197 in Chronicle projections/, 369 - across Components+CLI), **American-English** fixes, **tag untagged code fences**. - -### P2 — Structure, IA & coverage - -- **Re-group the nav** (`web/astro.config.mjs`): the "Cratis Stack" overview topic is an - overloaded ~23-entry catch-all that buries AuthProxy/Studio/Screenplay. Promote real - products; resolve the two competing "Scenarios" entry points. -- **De-duplicate** the overlapping "map" pages (index/why-cratis/cratis-stack/adopting), - `showcase`≈`samples`, Arc's Core-vs-ASP.NET (×4) and twin aggregate pages, Chronicle's - event-evolution (×3). **Reconcile the two CommandForm doc sets** (Components vs Arc) with a - cross-link + one canonical field-props page. -- **Document the undocumented shipped surface** — Components `Display` (Avatar/Badge/Chip/ - ProgressBar/Skeleton/Tag) and `Notifications` (Toaster/toast/toastCommandResult) modules and - 3 CommandForm fields; decide Chronicle's Java-client story (0 pages, yet claimed). -- **Fix the dropped toc entry** — `Fundamentals/Documentation/typescript/toc.yml:6` - ("Coordinate" → missing `coordinate.md`). **Contributing `release-digests/`**: curate into - `/whats-new` and exclude the raw files, or give them a landing + toc + real titles. - -### P3 — Tour-voice, depth & maturity framing - -- Rewrite Fundamentals reference-dump openings; bring `fundamentals/typescript/index.md` to - parity with the C# sibling. -- Arc: reframe controller-based framing to the model-bound default; flesh out the thin MVVM area. -- Components: remove speculative "(If implemented)" filler; add "when NOT to use". -- Split the oversized Chronicle model-bound pages (968–1171 lines); add diagrams to reducers. -- Badge Screenplay as **Preview**; fix `studio.mdx` to use `<CardGrid>` + `seti:` icons. -- Architecture: real landing paragraph; give CRARCH rules examples + rationale; de-dupe the - two identical index tables; reconcile the 400- vs 200-line threshold. - -### Tooling/process - -- Make CI assert Vale + lychee are installed (or the scripts warn loudly); wire - `build:api`/`build:storybooks` into the `check` chain (or teach `check-links` to skip those - paths). Exempt `chronicle/code-analysis/**` from the `chronicle-client-docs` shared-doc - audit (its C#-only analyzer docs are a false positive). - ---- - -## 6. Recommended first PR (fast, high-impact) - -Fix the **broken/invented code examples** (P0.1) — Components `primereact/column` ×9 and the -three Arc frontend bugs. Small diffs, verifiable against `Source/`, and they stop readers from -pasting non-working code. Verify each replacement compiles/type-checks against the real -exports, `npm run sync && npm run check`, then open one PR titled e.g. -"Fix broken/invented code examples in Components and Arc docs". - -Then tackle the **CLI orphaned nav** (one `toc.yml` edit) and the **two site-page facts** -(`compatibility.mdx` TypeScript version, `whats-new.mdx` latest digest) — both trivial and -user-visible. - ---- - -## 7. Deliverables from the audit (for reference) - -- `Documentation/DOCS-AUDIT-2026-07.md` — full file-cited report (this backlog's source). -- Shareable dashboard: https://claude.ai/code/artifact/94c59811-039b-4555-9add-266868ac4af6