diff --git a/.github/workflows/web-demos.yml b/.github/workflows/web-demos.yml index 6979b250..08187fc0 100644 --- a/.github/workflows/web-demos.yml +++ b/.github/workflows/web-demos.yml @@ -1,110 +1,100 @@ +name: Coln Lab + on: - pull_request: push: - branches: [main] - -name: Web demos - -permissions: - contents: read - pull-requests: write + branches: [live-demos] concurrency: - group: web-demos-${{ github.event_name }}-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: true + group: pages + cancel-in-progress: false jobs: - # build-and-deploy: - # runs-on: ubuntu-latest - # env: - # CARGO_TARGET_DIR: ${{ github.workspace }}/target - # NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} - # NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }} - - # steps: - # - name: Checkout ⬇️ - # uses: actions/checkout@v6 - - # - name: Install Nix ❄️ - # uses: cachix/install-nix-action@v31 - - # - name: Set up Cachix ♻️ - # if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository - # uses: cachix/cachix-action@v17 - # with: - # name: coln - # authToken: ${{ secrets.CACHIX_AUTH_TOKEN }} - - # - name: Restore build caches - # uses: actions/cache@v4 - # with: - # path: | - # .pnpm-store - # ~/.npm - # ~/.cargo/registry - # ~/.cargo/git - # ~/.ghc-wasm/.cabal - # target - # dist-newstyle - # key: web-demos-${{ runner.os }}-${{ hashFiles('flake.lock', 'Cargo.lock', 'cabal.project', 'packages/**/*.cabal', 'packages/coln-js-runtime/package-lock.json', 'examples/sync-demo/pnpm-lock.yaml') }} - # restore-keys: | - # web-demos-${{ runner.os }}- - - # - name: Build sync demo - # run: nix run --accept-flake-config .#sync-demo -- "$GITHUB_WORKSPACE" - # env: - # VITE_BASE: /sync/ - - # - name: Build web compiler - # run: | - # nix develop --accept-flake-config --command wasm32-wasi-cabal update 'hackage.haskell.org,2026-07-15T17:07:49Z' - # nix develop --accept-flake-config --command just examples/build-web-compiler - - # - name: Assemble deploy directory - # run: | - # cp examples/index.html examples/style.css _build/web/ - # cp -r examples/sync-demo/dist _build/web/sync - - # - name: Deploy PR preview to Netlify - # id: deploy-preview - # if: github.event_name == 'pull_request' && env.NETLIFY_AUTH_TOKEN != '' && env.NETLIFY_SITE_ID != '' - # run: | - # npx --yes netlify-cli@latest deploy \ - # --auth "$NETLIFY_AUTH_TOKEN" \ - # --site "$NETLIFY_SITE_ID" \ - # --dir _build/web \ - # --no-build \ - # --alias "pr-${{ github.event.pull_request.number }}" \ - # --message "PR #${{ github.event.pull_request.number }} web demos" \ - # --json > netlify-deploy.json - - # cat netlify-deploy.json - # deploy_url=$(node -e "const d = require('./netlify-deploy.json'); console.log(d.deploy_ssl_url || d.ssl_url || d.deploy_url || d.url)") - # echo "deploy-url=$deploy_url" >> "$GITHUB_OUTPUT" - - # - name: Comment with deploy preview URL - # if: github.event_name == 'pull_request' && steps.deploy-preview.outputs.deploy-url != '' - # uses: actions/github-script@v8 - # with: - # script: | - # const marker = '' - # const body = `${marker}\nWeb demo preview: ${{ steps.deploy-preview.outputs.deploy-url }}` - # const { owner, repo } = context.repo - # const issue_number = context.issue.number - # const comments = await github.rest.issues.listComments({ owner, repo, issue_number, per_page: 100 }) - # const existing = comments.data.find(comment => comment.body?.includes(marker)) - # if (existing) { - # await github.rest.issues.updateComment({ owner, repo, comment_id: existing.id, body }) - # } else { - # await github.rest.issues.createComment({ owner, repo, issue_number, body }) - # } - # - name: Deploy main to Netlify production - # if: github.event_name == 'push' && env.NETLIFY_AUTH_TOKEN != '' && env.NETLIFY_SITE_ID != '' - # run: | - # npx --yes netlify-cli@latest deploy \ - # --auth "$NETLIFY_AUTH_TOKEN" \ - # --site "$NETLIFY_SITE_ID" \ - # --dir _build/web \ - # --no-build \ - # --prod \ - # --message "web demos ${{ github.sha }}" + build: + runs-on: ubuntu-latest + permissions: + contents: read + env: + CARGO_TARGET_DIR: ${{ github.workspace }}/target + CACHIX_AUTH_TOKEN: ${{ secrets.CACHIX_AUTH_TOKEN }} + PNPM_STORE_DIR: ${{ github.workspace }}/.pnpm-store + VITE_SUBDUCTION_ENDPOINT: wss://coln.sync.inkandswitch.com + + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Configure Pages + id: pages + uses: actions/configure-pages@v5 + + - name: Install Nix + uses: cachix/install-nix-action@v31 + + - name: Set up Cachix + if: ${{ env.CACHIX_AUTH_TOKEN != '' }} + uses: cachix/cachix-action@v17 + with: + name: coln + authToken: ${{ env.CACHIX_AUTH_TOKEN }} + + - name: Restore build caches + uses: actions/cache@v4 + with: + path: | + .pnpm-store + ~/.npm + ~/.cargo/registry + ~/.cargo/git + ~/.ghc-wasm/.cabal + ~/.cache/ms-playwright + target + dist-newstyle + key: lab-${{ runner.os }}-${{ hashFiles('flake.lock', 'Cargo.lock', 'cabal.project', 'packages/**/*.cabal', 'packages/coln-js-runtime/package-lock.json', 'packages/coln-repo/pnpm-lock.yaml', 'examples/lab/pnpm-lock.yaml') }} + restore-keys: | + lab-${{ runner.os }}- + + - name: Build linked packages + run: | + nix develop --accept-flake-config .#web --command npm ci --prefix packages/coln-js-runtime + nix develop --accept-flake-config .#web --command npm run --prefix packages/coln-js-runtime build + nix develop --accept-flake-config .#web --command pnpm --dir packages/coln-repo install --frozen-lockfile --store-dir "$PNPM_STORE_DIR" + nix develop --accept-flake-config .#web --command pnpm --dir packages/coln-repo build + + - name: Install Lab dependencies + run: nix develop --accept-flake-config .#web --command pnpm --dir examples/lab install --frozen-lockfile --store-dir "$PNPM_STORE_DIR" + + - name: Install Chromium + run: nix develop --accept-flake-config .#web --command pnpm --dir examples/lab exec playwright install --with-deps chromium + + - name: Build compiler WASM + run: | + nix develop --accept-flake-config .#web --command wasm32-wasi-cabal update 'hackage.haskell.org,2026-07-15T17:07:49Z' + nix develop --accept-flake-config .#web --command just examples/stage-web-lab + + - name: Test Lab + run: nix develop --accept-flake-config .#web --command pnpm --dir examples/lab test + + - name: Build Lab + run: nix develop --accept-flake-config .#web --command pnpm --dir examples/lab build + env: + VITE_BASE: ${{ steps.pages.outputs.base_path }}/ + + - name: Upload Pages artifact + uses: actions/upload-pages-artifact@v5 + with: + path: _build/web/lab-app + + deploy: + needs: build + runs-on: ubuntu-latest + permissions: + pages: write + id-token: write + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v5 diff --git a/.gitignore b/.gitignore index 38a4c7b6..3ecb61c6 100644 --- a/.gitignore +++ b/.gitignore @@ -337,6 +337,8 @@ TSWLatexianTemp* .shake _build/ +.env +.env.local /cabal.project.local # nix result diff --git a/examples/cli-demo/.gitignore b/examples/cli-demo/.gitignore new file mode 100644 index 00000000..8166d595 --- /dev/null +++ b/examples/cli-demo/.gitignore @@ -0,0 +1,4 @@ +dist/ +node_modules/ +test-results/ +playwright-report/ diff --git a/examples/cli-demo/README.md b/examples/cli-demo/README.md new file mode 100644 index 00000000..85626dc8 --- /dev/null +++ b/examples/cli-demo/README.md @@ -0,0 +1,139 @@ +# Coln CLI Demo + +`coln-repo` is an experimental, JSON-only CLI for agents to read and update an +existing synchronized Coln document. It exposes the document's compiled IR +without interpreting its domain and evaluates trusted, synchronous JavaScript +for reads and transactions. + +Nothing in this protocol is stable yet. + +## Build + +Build local packages first: + +```bash +npm ci --prefix packages/coln-js-runtime +npm run --prefix packages/coln-js-runtime build +pnpm --dir packages/coln-repo install +pnpm --dir packages/coln-repo build +pnpm --dir examples/cli-demo install +pnpm --dir examples/cli-demo build +``` + +The built binary is `examples/cli-demo/dist/cli.js`, exposed as `coln-repo` by +the package manifest. Put it on your `PATH` with: + +```bash +pnpm --dir examples/cli-demo link --global +``` + +The examples below assume `coln-repo` is on `PATH`; otherwise substitute +`node examples/cli-demo/dist/cli.js`. + +## Agent Skill + +The model-facing workflow lives in [`skills/coln-repo/SKILL.md`](./skills/coln-repo/SKILL.md), +a portable skill (frontmatter plus markdown) that OpenCode, Claude Code, and +similar harnesses can load. Two ways to reach it: + +```bash +coln-repo guide # print the skill body as JSON +coln-repo install-skill # copy to ~/.agents/skills/coln-repo +coln-repo install-skill --dir ~/.claude/skills +``` + +`install-skill` reports `installed`, `updated`, or `unchanged`; rerun it after +upgrading the CLI to refresh the installed copy. No harness configuration or +permission rules are shipped; see Security below. + +## Commands + +Every command takes the document as a named flag. `--endpoint` overrides +`SUBDUCTION_ENDPOINT`; otherwise the public Subduction relay is used. + +```bash +coln-repo ir --document [--endpoint ] +``` + +`ir` returns the compiled JSON IR unchanged inside the response's `ir` field. +Agents should use it as structural evidence, not domain documentation. + +`query` reads one JavaScript expression from stdin. A read-only `store` is in +scope with `jsonIR()`, `scanTable(path)`, `rowById(path, rowRef)`, and `heads()`. + +```bash +coln-repo query --document coln:... <<'JS' +store.scanTable("Records.Documents") +JS +``` + +`exec` reads a JavaScript function body from stdin. The current transaction API +is in scope as `txn`; today it exposes `txn.add(path, values)`. Return a +JSON-serializable value to include it in the response. + +```bash +coln-repo exec --document coln:... <<'JS' +const folder = txn.add("Records.Folders", [{ tag: "string", value: "Inbox" }]) +const document = txn.add("Records.Documents", [ + folder, + { tag: "string", value: "Notes" }, +]) +return { folder, document } +JS +``` + +Values passed to `txn.add` are tagged: + +```js +{ tag: "string", value: "hello" } +{ tag: "int", value: 42 } +{ tag: "row_id", value: rowReference } +``` + +`txn.add` returns a complete tagged `row_id` value. Pass that value directly to +later `txn.add` calls; do not wrap it in another `{ tag: "row_id", ... }`. + +Programs must complete synchronously. Their return values must be JSON +serializable. Program `console` output is redirected to stderr so stdout remains +one JSON response. + +## Responses + +Every response contains `protocolVersion: 0`, `ok`, and the command where known. +Successful writes include heads before and after the local commit plus sync +status. Errors identify their phase and whether a mutation was applied locally. + +| Exit | Code | Phase | Mutation state | +|---|---|---|---| +| 2 | `INVALID_ARGUMENTS`, `PROGRAM_REQUIRED` | `arguments` | `not_applied` | +| 3 | `DOCUMENT_OPEN_FAILED` | `open` | `not_applied` | +| 4 | `IR_FAILED`, `QUERY_FAILED` | `read`, `evaluate` | `not_applied` | +| 4 | `GUIDE_FAILED`, `SKILL_INSTALL_FAILED` | `read`, `install` | `not_applied` | +| 5 | `TRANSACTION_FAILED` | `transaction` | `not_applied` | +| 5 | `RESULT_INVALID` | `result` | `not_applied` | +| 5 | `RESULT_FAILED` | `result` | `applied_locally` | +| 6 | `SYNC_FAILED` | `flush` | `applied_locally` | +| 1 | `INTERNAL_ERROR` | `internal` | `unknown` | + +`RESULT_INVALID` means the program's return value was not JSON; the transaction +was rolled back so the caller never loses the row references it asked for. + +If an `exec` response reports `mutationState: "applied_locally"`, do not retry it +automatically: the local commit succeeded, but relay synchronization or result +reporting failed afterwards. The CLI does not judge retryability of +`not_applied` failures; connection timeouts may be transient, schema rejections +are not. + +## Security + +`query` and `exec` evaluate trusted JavaScript with this Node process's +capabilities. Agent harnesses should request approval for them by default. +Allowing either command without approval is equivalent to allowing arbitrary +local Node execution. `ir`, `guide`, and `install-skill` do not evaluate user +JavaScript and may be allowlisted separately. + +## Verify + +```bash +pnpm --dir examples/cli-demo test +``` diff --git a/examples/cli-demo/package.json b/examples/cli-demo/package.json new file mode 100644 index 00000000..e5a5b31d --- /dev/null +++ b/examples/cli-demo/package.json @@ -0,0 +1,33 @@ +{ + "name": "coln-cli-demo", + "private": true, + "type": "module", + "license": "(Apache-2.0 OR MIT)", + "bin": { + "coln-repo": "./dist/cli.js" + }, + "files": [ + "dist", + "skills" + ], + "scripts": { + "build": "tsc -p tsconfig.build.json", + "cli": "tsx src/cli.ts", + "typecheck": "tsc --noEmit", + "test": "pnpm run typecheck && pnpm run test:e2e", + "test:e2e": "pnpm run build && playwright test" + }, + "dependencies": { + "@automerge/automerge-repo": "github:onsetsoftware/automerge-repo#test/custom-url-scemes&path:packages/automerge-repo", + "@coln-project/repo": "link:../../packages/coln-repo" + }, + "devDependencies": { + "@automerge/automerge-subduction": "0.16.1", + "@playwright/test": "^1.62.1", + "@types/node": "^25.9.5", + "@types/ws": "^8.18.1", + "tsx": "^4.23.13", + "typescript": "^6.0.3", + "ws": "^8.21.3" + } +} diff --git a/examples/cli-demo/playwright.config.ts b/examples/cli-demo/playwright.config.ts new file mode 100644 index 00000000..31262adf --- /dev/null +++ b/examples/cli-demo/playwright.config.ts @@ -0,0 +1,21 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import { defineConfig } from "@playwright/test" + +export default defineConfig({ + testDir: "./tests", + testMatch: "**/*.spec.ts", + timeout: 30_000, + expect: { timeout: 10_000 }, + fullyParallel: false, + retries: 0, + webServer: { + command: "node tests/server.mjs", + port: 3031, + reuseExistingServer: false, + stdout: "pipe", + stderr: "pipe", + }, +}) diff --git a/examples/cli-demo/pnpm-lock.yaml b/examples/cli-demo/pnpm-lock.yaml new file mode 100644 index 00000000..196f61bc --- /dev/null +++ b/examples/cli-demo/pnpm-lock.yaml @@ -0,0 +1,603 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +overrides: + '@automerge/automerge': 3.3.2 + '@automerge/automerge-repo': github:onsetsoftware/automerge-repo#test/custom-url-scemes&path:packages/automerge-repo + '@automerge/automerge-subduction': 0.16.1 + +importers: + + .: + dependencies: + '@automerge/automerge-repo': + specifier: github:onsetsoftware/automerge-repo#test/custom-url-scemes&path:packages/automerge-repo + version: https://codeload.github.com/onsetsoftware/automerge-repo/tar.gz/b39d94c5624a3497150e08ba18546f29222289d8#path:packages/automerge-repo + '@coln-project/repo': + specifier: link:../../packages/coln-repo + version: link:../../packages/coln-repo + devDependencies: + '@automerge/automerge-subduction': + specifier: 0.16.1 + version: 0.16.1 + '@playwright/test': + specifier: ^1.62.1 + version: 1.62.1 + '@types/node': + specifier: ^25.9.5 + version: 25.9.5 + '@types/ws': + specifier: ^8.18.1 + version: 8.18.1 + tsx: + specifier: ^4.23.13 + version: 4.23.13 + typescript: + specifier: ^6.0.3 + version: 6.0.3 + ws: + specifier: ^8.21.3 + version: 8.21.3 + +packages: + + '@automerge/automerge-repo@https://codeload.github.com/onsetsoftware/automerge-repo/tar.gz/b39d94c5624a3497150e08ba18546f29222289d8#path:packages/automerge-repo': + resolution: {gitHosted: true, integrity: sha512-6OS6f5Jo1ZuZLy2G43c74SHh1/a0fIO8QB7OsX4jJZSu7fMYjCoDWTZ2rX/MMzOU2Ywjg9CLCjy9YV2w5gvmxg==, path: packages/automerge-repo, tarball: https://codeload.github.com/onsetsoftware/automerge-repo/tar.gz/b39d94c5624a3497150e08ba18546f29222289d8} + version: 2.6.0-subduction.47 + engines: {node: '>=22.13'} + + '@automerge/automerge-subduction@0.16.1': + resolution: {integrity: sha512-alH7U4eYn0O7sT6hNLv7CJhNODJi+QD6qfRQTNfZCxL3Q5JZdJ8lFpdvXUkBf+I3bu0GLvVlTaYPWGrBihjpIw==} + + '@automerge/automerge@3.3.2': + resolution: {integrity: sha512-9vCdCL7pdQwUra66SBxPVHr+/t9epXKni9KDeak2rNBFMzABVh2u6gSpcwxi3jkR5qr047jVqZv9DlrOfVxLFw==} + + '@cbor-extract/cbor-extract-darwin-arm64@2.2.2': + resolution: {integrity: sha512-ZKZ/F8US7JR92J4DMct6cLW/Y66o2K576+zjlEN/MevH70bFIsB10wkZEQPLzl2oNh2SMGy55xpJ9JoBRl5DOA==} + cpu: [arm64] + os: [darwin] + + '@cbor-extract/cbor-extract-darwin-x64@2.2.2': + resolution: {integrity: sha512-32b1mgc+P61Js+KW9VZv/c+xRw5EfmOcPx990JbCBSkYJFY0l25VinvyyWfl+3KjibQmAcYwmyzKF9J4DyKP/Q==} + cpu: [x64] + os: [darwin] + + '@cbor-extract/cbor-extract-linux-arm64@2.2.2': + resolution: {integrity: sha512-wfqgzqCAy/Vn8i6WVIh7qZd0DdBFaWBjPdB6ma+Wihcjv0gHqD/mw3ouVv7kbbUNrab6dKEx/w3xQZEdeXIlzg==} + cpu: [arm64] + os: [linux] + + '@cbor-extract/cbor-extract-linux-arm@2.2.2': + resolution: {integrity: sha512-tNg0za41TpQfkhWjptD+0gSD2fggMiDCSacuIeELyb2xZhr7PrhPe5h66Jc67B/5dmpIhI2QOUtv4SBsricyYQ==} + cpu: [arm] + os: [linux] + + '@cbor-extract/cbor-extract-linux-x64@2.2.2': + resolution: {integrity: sha512-rpiLnVEsqtPJ+mXTdx1rfz4RtUGYIUg2rUAZgd1KjiC1SehYUSkJN7Yh+aVfSjvCGtVP0/bfkQkXpPXKbmSUaA==} + cpu: [x64] + os: [linux] + + '@cbor-extract/cbor-extract-win32-x64@2.2.2': + resolution: {integrity: sha512-dI+9P7cfWxkTQ+oE+7Aa6onEn92PHgfWXZivjNheCRmTBDBf2fx6RyTi0cmgpYLnD1KLZK9ZYrMxaPZ4oiXhGA==} + cpu: [x64] + os: [win32] + + '@esbuild/aix-ppc64@0.28.2': + resolution: {integrity: sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.28.2': + resolution: {integrity: sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.28.2': + resolution: {integrity: sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.28.2': + resolution: {integrity: sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.28.2': + resolution: {integrity: sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.28.2': + resolution: {integrity: sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.28.2': + resolution: {integrity: sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.28.2': + resolution: {integrity: sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.28.2': + resolution: {integrity: sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.28.2': + resolution: {integrity: sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.28.2': + resolution: {integrity: sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.28.2': + resolution: {integrity: sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.28.2': + resolution: {integrity: sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.28.2': + resolution: {integrity: sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.28.2': + resolution: {integrity: sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.28.2': + resolution: {integrity: sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.28.2': + resolution: {integrity: sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.28.2': + resolution: {integrity: sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.28.2': + resolution: {integrity: sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.28.2': + resolution: {integrity: sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.28.2': + resolution: {integrity: sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.28.2': + resolution: {integrity: sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.28.2': + resolution: {integrity: sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.28.2': + resolution: {integrity: sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.28.2': + resolution: {integrity: sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.28.2': + resolution: {integrity: sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@noble/hashes@1.8.0': + resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==} + engines: {node: ^14.21.3 || >=16} + + '@playwright/test@1.62.1': + resolution: {integrity: sha512-DTcUc8qii+cpHvtOwggMtBRMjKZHXYWdw8syRYu2vtzuq4Wxphqq4NfCs5Zt44L6mA8rfDfj+PHnxFc/FeK6mQ==} + engines: {node: '>=20'} + hasBin: true + + '@types/node@25.9.5': + resolution: {integrity: sha512-OScDchr2fwuUmWdf4kZ9h7PcJiYDVInhJizG/biAq3cAvqwYktuy/TYGGdZNMtNTFUP7rnb0NU4TUdm82kt4Rg==} + + '@types/ws@8.18.1': + resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} + + base-x@5.0.1: + resolution: {integrity: sha512-M7uio8Zt++eg3jPj+rHMfCC+IuygQHHCOU+IYsVtik6FWjuYpVt/+MRKcgsAMHh8mMFAwnB+Bs+mTrFiXjMzKg==} + + bs58@6.0.0: + resolution: {integrity: sha512-PD0wEnEYg6ijszw/u8s+iI3H17cTymlrwkKhDhPZq+Sokl3AU4htyBFTjAeNAlCCmg0f53g6ih3jATyCKftTfw==} + + bs58check@4.0.0: + resolution: {integrity: sha512-FsGDOnFg9aVI9erdriULkd/JjEWONV/lQE5aYziB5PoBsXRind56lh8doIZIc9X4HoxT5x4bLjMWN1/NB8Zp5g==} + + cbor-extract@2.2.2: + resolution: {integrity: sha512-hlSxxI9XO2yQfe9g6msd3g4xCfDqK5T5P0fRMLuaLHhxn4ViPrm+a+MUfhrvH2W962RGxcBwEGzLQyjbDG1gng==} + hasBin: true + + cbor-x@1.6.6: + resolution: {integrity: sha512-8QiD9PGOxyQHo7s2pzwTBH6lTjqekxPdl9Aq6fXvZgCuCJHOht1puDEA/fTr6mciB76c+M+Gi0qT2i1a4pm4Wg==} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + esbuild@0.28.2: + resolution: {integrity: sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==} + engines: {node: '>=18'} + hasBin: true + + eventemitter3@5.0.4: + resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==} + + fast-sha256@1.3.0: + resolution: {integrity: sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==} + + fsevents@2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + isomorphic-ws@5.0.0: + resolution: {integrity: sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw==} + peerDependencies: + ws: '*' + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + node-gyp-build-optional-packages@5.1.1: + resolution: {integrity: sha512-+P72GAjVAbTxjjwUmwjVrqrdZROD4nf8KgpBoDxqXXTiYZZt/ud60dE5yvCSr9lRO8e8yv6kgJIC0K0PfZFVQw==} + hasBin: true + + playwright-core@1.62.1: + resolution: {integrity: sha512-wPYSwEBJY9GHraISXqyqtx0na0LpO3XEX7jNDhntbex7tzUS7kLnZsOlFruFJB4Hi/rhDMjXGqHewDZ68nYZVw==} + engines: {node: '>=20'} + hasBin: true + + playwright@1.62.1: + resolution: {integrity: sha512-0M+L3LAD8/nm554LOla9Ayx0j0tmFZ0FBcoQ7F1VuVHpM/XpiC8RcDzBQB8W5+hA8L22THxELzeF+2WcUzvcLg==} + engines: {node: '>=20'} + hasBin: true + + tsx@4.23.13: + resolution: {integrity: sha512-BL5MGkRln6aDYhb0xbQlEAGw743BaZYWdbWtdJOBriYJboKgUUYCadFp2/FpBBZquBC/ezNBn7wMMPx7FDZUDw==} + engines: {node: '>=18.0.0'} + hasBin: true + + typescript@6.0.3: + resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@7.24.6: + resolution: {integrity: sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==} + + uuid@14.0.2: + resolution: {integrity: sha512-xZe/16rV4aa+HGSOCiY2YeLT1OybRLrrkL/Rqaq7p7GMVXjFh+6wN4oMYgjFmnSnhY8t6Xpdl2l9qmnHYuMHwQ==} + hasBin: true + + ws@8.21.3: + resolution: {integrity: sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + xstate@5.32.6: + resolution: {integrity: sha512-WfA8WNrh6r9osuGwVm+aIPM4jmMW2LKHV1Yv9thYpOtL4HVF/kobh95K/O8v2jDCpDmP2EoY+6uvuqHXCZ6ZLw==} + +snapshots: + + '@automerge/automerge-repo@https://codeload.github.com/onsetsoftware/automerge-repo/tar.gz/b39d94c5624a3497150e08ba18546f29222289d8#path:packages/automerge-repo': + dependencies: + '@automerge/automerge': 3.3.2 + '@automerge/automerge-subduction': 0.16.1 + bs58check: 4.0.0 + cbor-x: 1.6.6 + debug: 4.4.3 + eventemitter3: 5.0.4 + fast-sha256: 1.3.0 + isomorphic-ws: 5.0.0(ws@8.21.3) + uuid: 14.0.2 + ws: 8.21.3 + xstate: 5.32.6 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + '@automerge/automerge-subduction@0.16.1': {} + + '@automerge/automerge@3.3.2': {} + + '@cbor-extract/cbor-extract-darwin-arm64@2.2.2': + optional: true + + '@cbor-extract/cbor-extract-darwin-x64@2.2.2': + optional: true + + '@cbor-extract/cbor-extract-linux-arm64@2.2.2': + optional: true + + '@cbor-extract/cbor-extract-linux-arm@2.2.2': + optional: true + + '@cbor-extract/cbor-extract-linux-x64@2.2.2': + optional: true + + '@cbor-extract/cbor-extract-win32-x64@2.2.2': + optional: true + + '@esbuild/aix-ppc64@0.28.2': + optional: true + + '@esbuild/android-arm64@0.28.2': + optional: true + + '@esbuild/android-arm@0.28.2': + optional: true + + '@esbuild/android-x64@0.28.2': + optional: true + + '@esbuild/darwin-arm64@0.28.2': + optional: true + + '@esbuild/darwin-x64@0.28.2': + optional: true + + '@esbuild/freebsd-arm64@0.28.2': + optional: true + + '@esbuild/freebsd-x64@0.28.2': + optional: true + + '@esbuild/linux-arm64@0.28.2': + optional: true + + '@esbuild/linux-arm@0.28.2': + optional: true + + '@esbuild/linux-ia32@0.28.2': + optional: true + + '@esbuild/linux-loong64@0.28.2': + optional: true + + '@esbuild/linux-mips64el@0.28.2': + optional: true + + '@esbuild/linux-ppc64@0.28.2': + optional: true + + '@esbuild/linux-riscv64@0.28.2': + optional: true + + '@esbuild/linux-s390x@0.28.2': + optional: true + + '@esbuild/linux-x64@0.28.2': + optional: true + + '@esbuild/netbsd-arm64@0.28.2': + optional: true + + '@esbuild/netbsd-x64@0.28.2': + optional: true + + '@esbuild/openbsd-arm64@0.28.2': + optional: true + + '@esbuild/openbsd-x64@0.28.2': + optional: true + + '@esbuild/openharmony-arm64@0.28.2': + optional: true + + '@esbuild/sunos-x64@0.28.2': + optional: true + + '@esbuild/win32-arm64@0.28.2': + optional: true + + '@esbuild/win32-ia32@0.28.2': + optional: true + + '@esbuild/win32-x64@0.28.2': + optional: true + + '@noble/hashes@1.8.0': {} + + '@playwright/test@1.62.1': + dependencies: + playwright: 1.62.1 + + '@types/node@25.9.5': + dependencies: + undici-types: 7.24.6 + + '@types/ws@8.18.1': + dependencies: + '@types/node': 25.9.5 + + base-x@5.0.1: {} + + bs58@6.0.0: + dependencies: + base-x: 5.0.1 + + bs58check@4.0.0: + dependencies: + '@noble/hashes': 1.8.0 + bs58: 6.0.0 + + cbor-extract@2.2.2: + dependencies: + node-gyp-build-optional-packages: 5.1.1 + optionalDependencies: + '@cbor-extract/cbor-extract-darwin-arm64': 2.2.2 + '@cbor-extract/cbor-extract-darwin-x64': 2.2.2 + '@cbor-extract/cbor-extract-linux-arm': 2.2.2 + '@cbor-extract/cbor-extract-linux-arm64': 2.2.2 + '@cbor-extract/cbor-extract-linux-x64': 2.2.2 + '@cbor-extract/cbor-extract-win32-x64': 2.2.2 + optional: true + + cbor-x@1.6.6: + optionalDependencies: + cbor-extract: 2.2.2 + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + detect-libc@2.1.2: + optional: true + + esbuild@0.28.2: + optionalDependencies: + '@esbuild/aix-ppc64': 0.28.2 + '@esbuild/android-arm': 0.28.2 + '@esbuild/android-arm64': 0.28.2 + '@esbuild/android-x64': 0.28.2 + '@esbuild/darwin-arm64': 0.28.2 + '@esbuild/darwin-x64': 0.28.2 + '@esbuild/freebsd-arm64': 0.28.2 + '@esbuild/freebsd-x64': 0.28.2 + '@esbuild/linux-arm': 0.28.2 + '@esbuild/linux-arm64': 0.28.2 + '@esbuild/linux-ia32': 0.28.2 + '@esbuild/linux-loong64': 0.28.2 + '@esbuild/linux-mips64el': 0.28.2 + '@esbuild/linux-ppc64': 0.28.2 + '@esbuild/linux-riscv64': 0.28.2 + '@esbuild/linux-s390x': 0.28.2 + '@esbuild/linux-x64': 0.28.2 + '@esbuild/netbsd-arm64': 0.28.2 + '@esbuild/netbsd-x64': 0.28.2 + '@esbuild/openbsd-arm64': 0.28.2 + '@esbuild/openbsd-x64': 0.28.2 + '@esbuild/openharmony-arm64': 0.28.2 + '@esbuild/sunos-x64': 0.28.2 + '@esbuild/win32-arm64': 0.28.2 + '@esbuild/win32-ia32': 0.28.2 + '@esbuild/win32-x64': 0.28.2 + + eventemitter3@5.0.4: {} + + fast-sha256@1.3.0: {} + + fsevents@2.3.2: + optional: true + + fsevents@2.3.3: + optional: true + + isomorphic-ws@5.0.0(ws@8.21.3): + dependencies: + ws: 8.21.3 + + ms@2.1.3: {} + + node-gyp-build-optional-packages@5.1.1: + dependencies: + detect-libc: 2.1.2 + optional: true + + playwright-core@1.62.1: {} + + playwright@1.62.1: + dependencies: + playwright-core: 1.62.1 + optionalDependencies: + fsevents: 2.3.2 + + tsx@4.23.13: + dependencies: + esbuild: 0.28.2 + optionalDependencies: + fsevents: 2.3.3 + + typescript@6.0.3: {} + + undici-types@7.24.6: {} + + uuid@14.0.2: {} + + ws@8.21.3: {} + + xstate@5.32.6: {} diff --git a/examples/cli-demo/pnpm-workspace.yaml b/examples/cli-demo/pnpm-workspace.yaml new file mode 100644 index 00000000..e29d98df --- /dev/null +++ b/examples/cli-demo/pnpm-workspace.yaml @@ -0,0 +1,8 @@ +allowBuilds: + cbor-extract: false + "@automerge/automerge-repo@https://codeload.github.com/onsetsoftware/automerge-repo/tar.gz/b39d94c5624a3497150e08ba18546f29222289d8#path:packages/automerge-repo": true + esbuild: true +overrides: + "@automerge/automerge": 3.3.2 + "@automerge/automerge-repo": github:onsetsoftware/automerge-repo#test/custom-url-scemes&path:packages/automerge-repo + "@automerge/automerge-subduction": 0.16.1 diff --git a/examples/cli-demo/skills/coln-repo/SKILL.md b/examples/cli-demo/skills/coln-repo/SKILL.md new file mode 100644 index 00000000..3a39de24 --- /dev/null +++ b/examples/cli-demo/skills/coln-repo/SKILL.md @@ -0,0 +1,128 @@ +--- +name: coln-repo +description: Read and update a synchronized Coln store from a coln: URL with the coln-repo CLI. Use whenever a request names a coln: document, a Coln store, realm, or theory, or asks to add, list, count, or connect records or table entries in one. +--- + +# Working With a Coln Store + +A Coln document is addressed by a `coln:` URL and holds a store: a set of +tables whose paths come from a compiled theory, such as `Records.Documents`. The +`coln-repo` CLI reads and updates one such document. Every response is a single +JSON object on stdout, including errors. Run `coln-repo help` for usage and +`coln-repo guide` to print this document. + +## Procedure + +### 1. Resolve the sync endpoint + +Pass `--endpoint ` when the user names a relay. Otherwise the CLI uses +`SUBDUCTION_ENDPOINT`, then the public relay `wss://coln.sync.inkandswitch.com`. +A document held by a local relay is only reachable there; do not invent one. If +the response is `DOCUMENT_OPEN_FAILED`, report which endpoint you tried and ask. + +### 2. Read the IR + +```bash +coln-repo ir --document [--endpoint ] +``` + +The `ir` field is the compiled JSON IR, unchanged. Each entity has a `path` +(render `[["Records"],["Documents"]]` as `Records.Documents`), ordered `columns` with a +`type` of either `{ tag: "builtin", type: "builtinInt" | "builtinString" }` or +`{ tag: "rowId", path }`, and an optional `primaryKey`. Rules describe +constraints the store enforces at commit, such as foreign keys. + +Treat the IR as structural evidence, not domain documentation. A table name or +abbreviation does not establish what its rows represent. + +### 3. Infer a mapping and confirm it + +Never assume that names or abbreviations carry a particular meaning. Before +writing, state the mapping you inferred and ask the user to confirm it, +including argument order when column names do not explain it: + +> The IR defines `Records.Folders` with a `name` string and +> `Records.Documents` with `folder` referencing `Records.Folders` followed by a +> `title` string. I take "add a document to Inbox" to mean adding or finding the +> Inbox folder, then adding a document as `[folderRowId, title]`. Is that right? + +If several entities plausibly match, ask; do not pick the closest name. Copy +entity paths exactly from the IR. + +### 4. Query before writing when the request refers to existing rows + +```bash +coln-repo query --document <<'JS' +store.scanTable("Records.Folders") +JS +``` + +`query` reads one synchronous JavaScript expression from stdin. `store` exposes +`scanTable(path)`, `rowById(path, rowRef)`, `heads()`, and `jsonIR()`. Rows are +`{ rowId, values }`; a `rowId` is already a tagged `row_id` value and can be +passed straight back as a column value in `txn.add`. `rowById` takes the inner +reference instead: `store.rowById(path, row.rowId.value)`. Project or slice +large tables in the expression rather than returning everything. + +### 5. Write in one transaction + +```bash +coln-repo exec --document <<'JS' +const folder = txn.add("Records.Folders", [{ tag: "string", value: "Inbox" }]) +const document = txn.add("Records.Documents", [ + folder, + { tag: "string", value: "Notes" }, +]) +return { folder, document } +JS +``` + +`exec` reads a synchronous JavaScript function body from stdin. `txn` is the +current transaction API; today it exposes `txn.add(path, values)`, which returns +a complete tagged `row_id` value usable directly in later `add` calls within +the same script. Do not wrap that result in another `row_id` value. Return a +JSON-serializable value: it is included in the response with row references +resolved to their committed form. + +Column values are tagged: + +```js +{ tag: "string", value: "hello" } +{ tag: "int", value: 42 } +{ tag: "row_id", value: rowReference } +``` + +### 6. Read the response before doing anything else + +Success: `ok: true`, `document.headsBefore` and `headsAfter`, `sync.status`, and +`result`. + +Failure: `ok: false` and `error` with `code`, `phase`, `message`, and +`mutationState`: + +- `not_applied`: nothing was committed. Fix the cause before trying again; a + schema or rule rejection will not pass on retry. +- `applied_locally`: the transaction committed but synchronization or result + reporting failed afterwards. Never repeat the transaction automatically; tell + the user what committed and what failed. +- `unknown`: internal failure; report it. + +### 7. Verify + +Confirm a write with a `query`, for example `store.scanTable(path).length`, and +quote the returned row references to the user. + +## Limits + +- Add-only: `txn.add` is the whole transaction API today. Say so rather than + simulating updates or deletes. +- Existing documents only: the CLI cannot create a document. +- Synchronous only: a program returning a promise is rejected. +- Trusted code: programs run with the Node process's full capabilities. + +## Permissions + +`ir`, `help`, and `guide` evaluate no user code and are safe to allow without +approval. `query` and `exec` evaluate arbitrary JavaScript; request approval for +them unless the user has explicitly accepted that allowing them is equivalent to +allowing arbitrary local Node execution. diff --git a/examples/cli-demo/src/cli.ts b/examples/cli-demo/src/cli.ts new file mode 100644 index 00000000..0305b839 --- /dev/null +++ b/examples/cli-demo/src/cli.ts @@ -0,0 +1,43 @@ +#!/usr/bin/env node +// SPDX-FileCopyrightText: 2026 Coln contributors +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import { main } from "./cli/main.js" + +ignoreBrokenPipe(process.stdout) +ignoreBrokenPipe(process.stderr) + +let exitCode: number +try { + exitCode = await main(process.argv.slice(2)) +} catch (error) { + console.log(JSON.stringify({ + protocolVersion: 0, + ok: false, + error: { + code: "INTERNAL_ERROR", + phase: "internal", + message: error instanceof Error ? error.message : String(error), + mutationState: "unknown", + }, + }, null, 2)) + exitCode = 1 +} + +await Promise.all([drain(process.stdout), drain(process.stderr)]) +process.exit(exitCode) + +function drain(stream: NodeJS.WritableStream): Promise { + return new Promise(resolve => { + stream.once("error", () => resolve()) + stream.write("", () => resolve()) + }) +} + +// A consumer such as `head` may close a pipe while main is still running. +function ignoreBrokenPipe(stream: NodeJS.WritableStream): void { + stream.on("error", error => { + if ((error as NodeJS.ErrnoException).code !== "EPIPE") throw error + }) +} diff --git a/examples/cli-demo/src/cli/arguments.ts b/examples/cli-demo/src/cli/arguments.ts new file mode 100644 index 00000000..15ea7acd --- /dev/null +++ b/examples/cli-demo/src/cli/arguments.ts @@ -0,0 +1,93 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +export type DocumentCommand = "ir" | "query" | "exec" +export type Command = DocumentCommand | "guide" | "install-skill" + +export type ParsedArguments = + | { kind: "help"; command?: Command } + | { kind: "guide" } + | { kind: "install-skill"; directory?: string } + | { + kind: "command" + command: DocumentCommand + documentUrl: string + verbose: boolean + endpoint?: string + } + +const documentCommands: DocumentCommand[] = ["ir", "query", "exec"] +const commands: Command[] = [...documentCommands, "guide", "install-skill"] + +export function parseArguments(arguments_: string[]): ParsedArguments { + const positional: string[] = [] + let verbose = false + let help = false + let documentUrl: string | undefined + let endpoint: string | undefined + let directory: string | undefined + + for (let index = 0; index < arguments_.length; index += 1) { + const argument = arguments_[index] + if (argument === "-v" || argument === "--verbose") verbose = true + else if (argument === "-h" || argument === "--help") help = true + else if (argument.startsWith("--document=")) { + documentUrl = argument.slice("--document=".length) + } else if (argument === "--document") { + documentUrl = optionValue(arguments_, ++index, "--document requires a Coln URL") + } else if (argument.startsWith("--endpoint=")) { + endpoint = argument.slice("--endpoint=".length) + if (endpoint === "") throw new Error("--endpoint requires a WebSocket URL") + } else if (argument === "--endpoint") { + endpoint = optionValue(arguments_, ++index, "--endpoint requires a WebSocket URL") + } else if (argument.startsWith("--dir=")) { + directory = argument.slice("--dir=".length) + if (directory === "") throw new Error("--dir requires a directory path") + } else if (argument === "--dir") { + directory = optionValue(arguments_, ++index, "--dir requires a directory path") + } else if (argument.startsWith("-")) throw new Error(`Unknown option: ${argument}`) + else positional.push(argument) + } + + const explicitHelp = help || positional[0] === "help" + if (explicitHelp || arguments_.length === 0) { + const command = positional[0] === "help" ? positional[1] : positional[0] + if (command !== undefined && !isCommand(command)) { + throw new Error(`Unknown command: ${command}`) + } + return { kind: "help", command } + } + + const [command, ...extra] = positional + if (command === undefined) throw new Error("Missing command") + if (!isCommand(command)) throw new Error(`Unknown command: ${command}`) + if (extra.length > 0) throw new Error(`Unexpected argument: ${extra[0]}`) + + if (command === "guide" || command === "install-skill") { + if (documentUrl !== undefined) throw new Error(`--document is not used by ${command}`) + if (endpoint !== undefined) throw new Error(`--endpoint is not used by ${command}`) + if (command === "guide") { + if (directory !== undefined) throw new Error("--dir is not used by guide") + return { kind: "guide" } + } + return { kind: "install-skill", directory } + } + + if (directory !== undefined) throw new Error(`--dir is not used by ${command}`) + if (documentUrl === undefined || documentUrl === "") { + throw new Error("--document requires a Coln URL") + } + + return { kind: "command", command, documentUrl, verbose, endpoint } +} + +function isCommand(value: string | undefined): value is Command { + return value !== undefined && (commands as string[]).includes(value) +} + +function optionValue(arguments_: string[], index: number, message: string): string { + const value = arguments_[index] + if (value === undefined || value.startsWith("-")) throw new Error(message) + return value +} diff --git a/examples/cli-demo/src/cli/commands/exec.ts b/examples/cli-demo/src/cli/commands/exec.ts new file mode 100644 index 00000000..8ea4bd1a --- /dev/null +++ b/examples/cli-demo/src/cli/commands/exec.ts @@ -0,0 +1,17 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import { evaluateExec, snapshotJsonResult } from "../evaluation.js" +import type { ColnHandle } from "@coln-project/repo" + +export function runExec(handle: ColnHandle, source: string): unknown { + let result: unknown = null + + handle.change(transaction => { + result = evaluateExec(transaction, source) + }) + + // Commit resolves pending row references in place; snapshot only afterwards. + return snapshotJsonResult(result) +} diff --git a/examples/cli-demo/src/cli/commands/ir.ts b/examples/cli-demo/src/cli/commands/ir.ts new file mode 100644 index 00000000..5b62109f --- /dev/null +++ b/examples/cli-demo/src/cli/commands/ir.ts @@ -0,0 +1,9 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import type { ColnHandle } from "@coln-project/repo" + +export function runIr(handle: ColnHandle): unknown { + return JSON.parse(handle.doc().jsonIR()) +} diff --git a/examples/cli-demo/src/cli/commands/query.ts b/examples/cli-demo/src/cli/commands/query.ts new file mode 100644 index 00000000..73481aa3 --- /dev/null +++ b/examples/cli-demo/src/cli/commands/query.ts @@ -0,0 +1,10 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import { evaluateQuery } from "../evaluation.js" +import type { ColnHandle } from "@coln-project/repo" + +export function runQuery(handle: ColnHandle, source: string): unknown { + return evaluateQuery(handle.doc(), source) +} diff --git a/examples/cli-demo/src/cli/evaluation.ts b/examples/cli-demo/src/cli/evaluation.ts new file mode 100644 index 00000000..9e7b013e --- /dev/null +++ b/examples/cli-demo/src/cli/evaluation.ts @@ -0,0 +1,84 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import { Console } from "node:console" +import type { ColnDocument, ColnTransaction } from "@coln-project/repo" + +/** A program produced a value the JSON protocol cannot carry. */ +export class InvalidResultError extends TypeError { + constructor() { + super("JavaScript result must be JSON-serializable") + this.name = "InvalidResultError" + } +} + +export function evaluateQuery(store: ColnDocument, source: string): unknown { + const evaluate = new Function("store", "console", `"use strict"; return (${source}\n)`) as ( + store: ColnDocument, + console: Console, + ) => unknown + const result = evaluate(store, evaluationConsole) + assertSynchronous(result) + const json = jsonResult(result) + return snapshotJsonResult(json) +} + +export function evaluateExec( + transaction: ColnTransaction, + source: string, +): unknown { + const evaluate = new Function("txn", "console", `"use strict";\n${source}`) as ( + transaction: ColnTransaction, + console: Console, + ) => unknown + const result = evaluate(transaction, evaluationConsole) + assertSynchronous(result) + return jsonResult(result) +} + +// Keep program logging away from the JSON protocol on stdout: every console +// method, including table/dir/group, writes to stderr. +const evaluationConsole = new Console({ stdout: process.stderr, stderr: process.stderr }) + +function assertSynchronous(value: unknown): void { + if ( + (typeof value === "object" || typeof value === "function") + && value !== null + && "then" in value + && typeof value.then === "function" + ) { + throw new TypeError("JavaScript must complete synchronously") + } +} + +function jsonResult(value: unknown): unknown { + if (value === undefined) return null + assertJsonValue(value, new WeakSet()) + return value +} + +export function snapshotJsonResult(value: unknown): unknown { + return JSON.parse(JSON.stringify(value)) +} + +function assertJsonValue(value: unknown, ancestors: WeakSet): void { + if (value === null || typeof value === "string" || typeof value === "boolean") return + if (typeof value === "number" && Number.isFinite(value)) return + if (typeof value !== "object") throw new InvalidResultError() + if (ancestors.has(value)) throw new InvalidResultError() + + const prototype = Object.getPrototypeOf(value) + if (prototype !== Object.prototype && prototype !== Array.prototype && prototype !== null) { + throw new InvalidResultError() + } + if (Object.getOwnPropertySymbols(value).length > 0) throw new InvalidResultError() + + ancestors.add(value) + for (const descriptor of Object.values(Object.getOwnPropertyDescriptors(value))) { + if (!descriptor.enumerable) continue + if (!("value" in descriptor)) throw new InvalidResultError() + assertJsonValue(descriptor.value, ancestors) + } + ancestors.delete(value) +} diff --git a/examples/cli-demo/src/cli/help.ts b/examples/cli-demo/src/cli/help.ts new file mode 100644 index 00000000..30a9a321 --- /dev/null +++ b/examples/cli-demo/src/cli/help.ts @@ -0,0 +1,106 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import type { Command } from "./arguments.js" + +const overview = `coln-repo reads and updates synchronized Coln documents. + +A Coln theory describes the entities and rules in a document. The compiler lowers +that theory to JSON IR. Read the IR, confirm inferred domain meanings with the +user, then query or execute a transaction. + +Usage: + coln-repo ir --document [--endpoint ] [-v] + coln-repo query --document [--endpoint ] [-v] + coln-repo exec --document [--endpoint ] [-v] + coln-repo guide + coln-repo install-skill [--dir ] + coln-repo help [command] + +Commands: + ir Return the full compiled JSON IR + query Read a JavaScript expression from stdin and evaluate it + exec Read a JavaScript body from stdin and run one change transaction + guide Print the agent workflow for this tool (the coln-repo skill) + install-skill Install or update the coln-repo skill for agent harnesses + +Options: + --endpoint Sync server WebSocket URL, overriding SUBDUCTION_ENDPOINT + --dir Skills root for install-skill (default ~/.agents/skills) + -v, --verbose Print connection and sync progress to stderr + -h, --help Print command help + +Environment: + SUBDUCTION_ENDPOINT Sync server WebSocket URL, used when --endpoint is absent + +The endpoint defaults to wss://coln.sync.inkandswitch.com. A document held +by a local relay is only reachable by pointing at that relay, for example +--endpoint ws://127.0.0.1:3030. + +Agents: run "coln-repo guide" first. It explains how to read the IR, confirm +inferred meanings with the user, and interpret responses. + +Every result, including an error, is JSON on stdout. JavaScript is trusted code +and runs with this Node process's capabilities.` + +const commandHelp: Record = { + guide: `Usage: coln-repo guide + +Print the coln-repo skill body: the step-by-step workflow an agent should follow +to read a document's IR, confirm its interpretation, query, write, and read the +response. The same text is what install-skill installs.`, + "install-skill": `Usage: coln-repo install-skill [--dir ] + +Copy the coln-repo skill directory to /coln-repo, creating it or +replacing it when the installed copy differs. The default root is +~/.agents/skills, which OpenCode reads. Other harnesses use their own root, for +example --dir ~/.claude/skills for Claude Code. + +The response reports status "installed", "updated", or "unchanged".`, + ir: `Usage: coln-repo ir --document [--endpoint ] [-v] + +Return the document's compiled JSON IR without interpreting its schema.`, + query: `Usage: coln-repo query --document [--endpoint ] [-v] + +Evaluate a synchronous JavaScript expression with a read-only store in scope. +Read the expression from stdin and return its value in the JSON response. + +Store methods: + store.jsonIR() Return the IR JSON string + store.scanTable(path) Return all rows in an IR table + store.rowById(path, rowRef) Return one row, or undefined (rowRef = row.rowId.value) + store.heads() Return current commit hashes + +Example: + printf 'store.scanTable("Records.Documents")' | coln-repo query --document coln:...`, + exec: `Usage: coln-repo exec --document [--endpoint ] [-v] + +Run a synchronous JavaScript script in one atomic Coln Repo change. The script +is read from stdin and receives the current transaction API as txn. Return a +JSON-serializable value to include it in the response. + +Available operations: + txn.add(path, values) Add a row and return its tagged row reference + +Values are tagged: { tag: "int", value } | { tag: "string", value } | +{ tag: "row_id", value: rowRef }. +txn.add returns a complete tagged row_id value; pass it directly to later adds. + +Example: + coln-repo exec --document coln:... <<'JS' + const folder = txn.add("Records.Folders", [{ tag: "string", value: "Inbox" }]) + const document = txn.add("Records.Documents", [ + folder, + { tag: "string", value: "Notes" }, + ]) + return { folder, document } +JS + +The change is committed only if the script returns synchronously without error. +The CLI waits for the connected server to flush it before reporting success.`, +} + +export function helpText(command?: Command): string { + return command === undefined ? overview : commandHelp[command] +} diff --git a/examples/cli-demo/src/cli/main.ts b/examples/cli-demo/src/cli/main.ts new file mode 100644 index 00000000..96f22e6d --- /dev/null +++ b/examples/cli-demo/src/cli/main.ts @@ -0,0 +1,238 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import { join } from "node:path" +import { parseArguments, type Command } from "./arguments.js" +import { runExec } from "./commands/exec.js" +import { runIr } from "./commands/ir.js" +import { runQuery } from "./commands/query.js" +import { InvalidResultError } from "./evaluation.js" +import { helpText } from "./help.js" +import { defaultEndpoint, flush, openDocument } from "./repository.js" +import { defaultSkillInstallRoot, installSkill, readGuide, skillName } from "./skill.js" + +const protocolVersion = 0 + +export async function main(arguments_: string[]): Promise { + let argumentsResult + try { + argumentsResult = parseArguments(arguments_) + } catch (error) { + writeJson(failure(undefined, "INVALID_ARGUMENTS", "arguments", error, "not_applied")) + return 2 + } + + if (argumentsResult.kind === "help") { + writeJson({ + protocolVersion, + ok: true, + command: "help", + help: helpText(argumentsResult.command), + }) + return 0 + } + + if (argumentsResult.kind === "guide") { + try { + writeJson({ ...success("guide"), guide: readGuide() }) + return 0 + } catch (error) { + writeJson(failure("guide", "GUIDE_FAILED", "read", error, "not_applied")) + return 4 + } + } + + if (argumentsResult.kind === "install-skill") { + const root = argumentsResult.directory ?? defaultSkillInstallRoot + try { + const installed = installSkill(root) + writeJson({ ...success("install-skill"), skill: { name: skillName, ...installed } }) + return 0 + } catch (error) { + writeJson({ + ...failure("install-skill", "SKILL_INSTALL_FAILED", "install", error, "not_applied"), + skill: { name: skillName, target: join(root, skillName) }, + }) + return 4 + } + } + + const { command, documentUrl, verbose } = argumentsResult + const endpoint = argumentsResult.endpoint ?? process.env.SUBDUCTION_ENDPOINT ?? defaultEndpoint + let source: string | undefined + if (command === "query" || command === "exec") { + if (process.stdin.isTTY) { + writeJson(failure(command, "PROGRAM_REQUIRED", "arguments", new Error( + `${command} reads JavaScript from stdin; pipe or redirect a program into it`, + ), "not_applied")) + return 2 + } + source = await readStdin() + if (source.trim() === "") { + writeJson(failure(command, "PROGRAM_REQUIRED", "arguments", new Error( + `${command} requires JavaScript on stdin`, + ), "not_applied")) + return 2 + } + } + + let opened + try { + opened = await openDocument(documentUrl, endpoint, verbose) + } catch (error) { + writeJson({ + ...failure(command, "DOCUMENT_OPEN_FAILED", "open", error, "not_applied"), + document: { url: documentUrl, endpoint }, + }) + return 3 + } + + const { repo, handle } = opened + try { + if (command === "ir") { + let heads: string[] | undefined + try { + heads = handle.doc().heads() + writeJson({ + ...success(command), + document: documentState(documentUrl, endpoint, heads), + ir: runIr(handle), + }) + return 0 + } catch (error) { + writeJson({ + ...failure(command, "IR_FAILED", "read", error, "not_applied"), + document: heads === undefined + ? { url: documentUrl, endpoint } + : documentState(documentUrl, endpoint, heads), + }) + return 4 + } + } + + if (command === "query") { + let heads: string[] | undefined + try { + heads = handle.doc().heads() + const result = runQuery(handle, source!) + writeJson({ + ...success(command), + document: documentState(documentUrl, endpoint, heads), + result, + }) + return 0 + } catch (error) { + writeJson({ + ...failure(command, "QUERY_FAILED", "evaluate", error, "not_applied"), + document: heads === undefined + ? { url: documentUrl, endpoint } + : documentState(documentUrl, endpoint, heads), + }) + return 4 + } + } + + const headsBefore = handle.doc().heads() + let result: unknown + try { + result = runExec(handle, source!) + } catch (error) { + const headsAfter = handle.doc().heads() + // Three failure shapes: the transaction rejected the program (rolled back), + // the program's return value is not JSON (rolled back before commit), or + // snapshotting the value after commit failed (committed locally). + const applied = !sameHeads(headsBefore, headsAfter) + const invalidResult = error instanceof InvalidResultError + writeJson({ + ...failure( + command, + applied ? "RESULT_FAILED" : invalidResult ? "RESULT_INVALID" : "TRANSACTION_FAILED", + applied || invalidResult ? "result" : "transaction", + error, + applied ? "applied_locally" : "not_applied", + ), + document: transactionState(documentUrl, endpoint, headsBefore, headsAfter), + }) + return 5 + } + + const headsAfter = handle.doc().heads() + try { + await flush(repo, endpoint, verbose) + } catch (error) { + writeJson({ + ...failure(command, "SYNC_FAILED", "flush", error, "applied_locally"), + document: transactionState(documentUrl, endpoint, headsBefore, headsAfter), + result, + }) + return 6 + } + + writeJson({ + ...success(command), + document: transactionState(documentUrl, endpoint, headsBefore, headsAfter), + sync: { status: "flushed" }, + result, + }) + return 0 + } finally { + await repo.shutdown().catch(error => console.error(errorMessage(error))) + } +} + +function success(command: Command): object { + return { protocolVersion, ok: true, command } +} + +function failure( + command: Command | undefined, + code: string, + phase: string, + error: unknown, + mutationState: "not_applied" | "applied_locally", +): object { + return { + protocolVersion, + ok: false, + ...(command === undefined ? {} : { command }), + error: { + code, + phase, + message: errorMessage(error), + mutationState, + }, + } +} + +function documentState(url: string, endpoint: string, heads: string[]): object { + return { url, endpoint, heads } +} + +function transactionState( + url: string, + endpoint: string, + headsBefore: string[], + headsAfter: string[], +): object { + return { url, endpoint, headsBefore, headsAfter } +} + +function errorMessage(error: unknown): string { + return error instanceof Error ? error.message : String(error) +} + +function sameHeads(left: string[], right: string[]): boolean { + return left.length === right.length && left.every(head => right.includes(head)) +} + +function writeJson(value: unknown): void { + console.log(JSON.stringify(value, null, 2)) +} + +async function readStdin(): Promise { + process.stdin.setEncoding("utf8") + let source = "" + for await (const chunk of process.stdin) source += chunk + return source +} diff --git a/examples/cli-demo/src/cli/repository.ts b/examples/cli-demo/src/cli/repository.ts new file mode 100644 index 00000000..710c2b29 --- /dev/null +++ b/examples/cli-demo/src/cli/repository.ts @@ -0,0 +1,112 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import { + initSubduction, + isValidDocumentUrl, + Repo, + setLoggerFactory, + setSubductionLogLevel, +} from "@automerge/automerge-repo" +import { find, type ColnHandle } from "@coln-project/repo" + +export const defaultEndpoint = "wss://coln.sync.inkandswitch.com" +export const operationTimeoutMs = 10_000 + +export interface OpenDocument { + repo: Repo + handle: ColnHandle +} + +export async function openDocument( + documentUrl: string, + endpoint: string, + verbose: boolean, +): Promise { + if (!isValidDocumentUrl(documentUrl, "coln")) throw new Error(`Invalid Coln URL: ${documentUrl}`) + validateEndpoint(endpoint) + if (verbose) console.error(`Connecting to ${endpoint}`) + + await initSubduction() + configureLogging(verbose) + const repo = new Repo({ subductionWebsocketEndpoints: [endpoint] }) + // Repo construction resets this to warn. Subduction tracing writes to stdout, + // so keep it at error; CLI-owned verbose progress uses stderr. + setSubductionLogLevel("error") + try { + await withTimeout(waitForConnection(repo), `Timed out connecting to ${endpoint}`) + if (verbose) console.error(`Connected to ${endpoint}`) + const handle = await withTimeout( + find(repo, documentUrl), + `Timed out loading ${documentUrl}`, + ) + return { repo, handle } + } catch (error) { + await repo.shutdown() + throw error + } +} + +function configureLogging(verbose: boolean): void { + setLoggerFactory(namespace => { + const write = (message: string, ...args: unknown[]) => { + console.error(`[${namespace}]`, message, ...args) + } + const quiet = () => {} + return { + debug: verbose ? write : quiet, + info: verbose ? write : quiet, + warn: verbose ? write : quiet, + error: write, + } + }) +} + +export async function flush(repo: Repo, endpoint: string, verbose: boolean): Promise { + if (!repo.isSubductionConnected()) throw new Error(`Disconnected from ${endpoint} before sync`) + await withTimeout(repo.flush(), `Timed out syncing to ${endpoint}`) + if (verbose) console.error(`Synced to ${endpoint}`) +} + +function waitForConnection(repo: Repo): Promise { + if (repo.isSubductionConnected()) return Promise.resolve() + return new Promise(resolve => { + const connected = ({ connected: isConnected }: { connected: boolean }) => { + if (!isConnected) return + repo.off("subduction-connection", connected) + resolve() + } + repo.on("subduction-connection", connected) + if (repo.isSubductionConnected()) { + repo.off("subduction-connection", connected) + resolve() + } + }) +} + +async function withTimeout(promise: Promise, message: string): Promise { + let timeout: ReturnType | undefined + try { + return await Promise.race([ + promise, + new Promise((_resolve, reject) => { + timeout = setTimeout(() => reject(new Error(message)), operationTimeoutMs) + }), + ]) + } finally { + if (timeout !== undefined) clearTimeout(timeout) + } +} + +function validateEndpoint(endpoint: string): void { + let url: URL + try { + url = new URL(endpoint) + } catch { + throw new Error(`Invalid Subduction endpoint: ${endpoint}`) + } + if (url.protocol !== "ws:" && url.protocol !== "wss:") { + throw new Error(`Subduction endpoint must use ws: or wss: ${endpoint}`) + } +} diff --git a/examples/cli-demo/src/cli/skill.ts b/examples/cli-demo/src/cli/skill.ts new file mode 100644 index 00000000..442fd094 --- /dev/null +++ b/examples/cli-demo/src/cli/skill.ts @@ -0,0 +1,78 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync } from "node:fs" +import { homedir } from "node:os" +import { join, relative } from "node:path" +import { fileURLToPath } from "node:url" + +export const skillName = "coln-repo" + +/** + * The skill directory ships beside `src/` and `dist/`, so the same relative + * path resolves whether this module runs from source (tsx) or from the build. + */ +export const skillSourceDirectory = fileURLToPath( + new URL(`../../skills/${skillName}/`, import.meta.url), +) + +export const defaultSkillInstallRoot = join(homedir(), ".agents", "skills") + +/** Body of SKILL.md with its YAML frontmatter removed. */ +export function readGuide(): string { + const text = readFileSync(join(skillSourceDirectory, "SKILL.md"), "utf8") + const match = /^---\r?\n[\s\S]*?\r?\n---\r?\n/.exec(text) + return (match === null ? text : text.slice(match[0].length)).trim() +} + +export type InstallStatus = "installed" | "updated" | "unchanged" + +export interface InstallResult { + status: InstallStatus + source: string + target: string + files: string[] +} + +/** Copy the skill directory to `/`, replacing stale content. */ +export function installSkill(root: string): InstallResult { + const target = join(root, skillName) + const files = listFiles(skillSourceDirectory) + const existed = existsSync(target) + const status: InstallStatus = !existed + ? "installed" + : sameContent(skillSourceDirectory, target, files) ? "unchanged" : "updated" + + if (status !== "unchanged") { + // Replace, don't merge: files dropped from the skill must not linger. + rmSync(target, { recursive: true, force: true }) + mkdirSync(target, { recursive: true }) + cpSync(skillSourceDirectory, target, { recursive: true, force: true }) + } + + return { status, source: skillSourceDirectory, target, files } +} + +function listFiles(directory: string, prefix = directory): string[] { + const files: string[] = [] + for (const entry of readdirSync(directory, { withFileTypes: true })) { + const path = join(directory, entry.name) + if (entry.isDirectory()) files.push(...listFiles(path, prefix)) + else files.push(relative(prefix, path)) + } + return files.sort() +} + +function sameContent(source: string, target: string, files: string[]): boolean { + if (!existsSync(target) || !statSync(target).isDirectory()) return false + const targetFiles = listFiles(target) + if (targetFiles.length !== files.length || targetFiles.some((file, index) => file !== files[index])) { + return false + } + return files.every(file => { + const targetPath = join(target, file) + if (!existsSync(targetPath) || !statSync(targetPath).isFile()) return false + return readFileSync(join(source, file)).equals(readFileSync(targetPath)) + }) +} diff --git a/examples/sync-demo/server.mjs b/examples/cli-demo/tests/server.mjs similarity index 65% rename from examples/sync-demo/server.mjs rename to examples/cli-demo/tests/server.mjs index 6a8de6a2..55bbe053 100644 --- a/examples/sync-demo/server.mjs +++ b/examples/cli-demo/tests/server.mjs @@ -1,3 +1,7 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + import { WebSocketServer } from "ws" import { MemorySigner, @@ -5,8 +9,8 @@ import { Subduction, } from "@automerge/automerge-subduction" -class WsTransport { - #ws +class WebSocketTransport { + #socket #queue = [] #waiters = [] #errorWaiters = [] @@ -15,13 +19,13 @@ class WsTransport { #resolveClosed #disconnectCallback = null - constructor(ws) { - this.#ws = ws + constructor(socket) { + this.#socket = socket this.#closedPromise = new Promise(resolve => { this.#resolveClosed = resolve }) - ws.on("message", data => { + socket.on("message", data => { const bytes = toUint8Array(data) const waiter = this.#waiters.shift() if (waiter) { @@ -31,9 +35,8 @@ class WsTransport { this.#queue.push(bytes) } }) - - ws.on("close", () => this.#markClosed(new Error("WebSocket closed"))) - ws.on("error", err => this.#markClosed(err instanceof Error ? err : new Error(String(err)))) + socket.on("close", () => this.#markClosed(new Error("WebSocket closed"))) + socket.on("error", error => this.#markClosed(error)) } onDisconnect(callback) { @@ -43,7 +46,7 @@ class WsTransport { async sendBytes(bytes) { if (this.#closed) throw new Error("WebSocket closed") await new Promise((resolve, reject) => { - this.#ws.send(Buffer.from(bytes), err => (err ? reject(err) : resolve())) + this.#socket.send(Buffer.from(bytes), error => (error ? reject(error) : resolve())) }) } @@ -59,18 +62,18 @@ class WsTransport { async disconnect() { this.#markClosed(new Error("WebSocket closed"), false) - this.#ws.close() + this.#socket.close() } closed() { return this.#closedPromise } - #markClosed(err, notify = true) { + #markClosed(error, notify = true) { if (this.#closed) return this.#closed = true this.#resolveClosed() - for (const reject of this.#errorWaiters) reject(err) + for (const reject of this.#errorWaiters) reject(error) this.#waiters = [] this.#errorWaiters = [] if (notify) this.#disconnectCallback?.() @@ -86,23 +89,24 @@ function toUint8Array(data) { return new Uint8Array(Buffer.from(data)) } -const port = Number(process.env.PORT ?? 3030) -const serviceName = `localhost:${port}` +const port = 3031 +const serviceName = `127.0.0.1:${port}` const subduction = new Subduction({ signer: new MemorySigner(), storage: new MemoryStorage(), }) -const wss = new WebSocketServer({ port }) +const server = new WebSocketServer({ port }) -wss.on("connection", ws => { - subduction.acceptTransport(new WsTransport(ws), serviceName).catch(err => { - if (!String(err?.message ?? err).includes("closed")) console.error(err) +server.on("connection", socket => { + subduction.acceptTransport(new WebSocketTransport(socket), serviceName).catch(error => { + if (!String(error?.message ?? error).includes("closed")) console.error(error) }) }) -process.on("SIGINT", async () => { +async function shutdown() { await subduction.disconnectAll().catch(() => {}) - wss.close(() => process.exit(0)) -}) + server.close(() => process.exit(0)) +} -console.log(`Subduction relay listening on ws://localhost:${port}`) +process.on("SIGINT", shutdown) +process.on("SIGTERM", shutdown) diff --git a/examples/cli-demo/tests/sync.spec.ts b/examples/cli-demo/tests/sync.spec.ts new file mode 100644 index 00000000..94ad55f9 --- /dev/null +++ b/examples/cli-demo/tests/sync.spec.ts @@ -0,0 +1,289 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import { expect, test } from "@playwright/test" +import { spawn } from "node:child_process" +import { existsSync, mkdtempSync, rmSync } from "node:fs" +import { tmpdir } from "node:os" +import { join } from "node:path" +import { initSubduction, Repo } from "@automerge/automerge-repo" +import { colnDocType, type ColnSchema, type ColnTransaction } from "@coln-project/repo" + +const endpoint = "ws://127.0.0.1:3031" +const peopleTable = "Scheduling.People" +const meetingsTable = "Scheduling.Meetings" +const schema = { + entities: [ + { + path: [["Scheduling"], ["People"]], + value: { + entityVariant: { tag: "table" }, + columns: [{ + path: [["name"]], + type: { tag: "builtin", type: "builtinString" }, + }], + primaryKey: null, + }, + }, + { + path: [["Scheduling"], ["Meetings"]], + value: { + entityVariant: { tag: "table" }, + columns: [ + { + path: [["organizer"]], + type: { tag: "rowId", path: [["Scheduling"], ["People"]] }, + }, + { + path: [["attendee"]], + type: { tag: "rowId", path: [["Scheduling"], ["People"]] }, + }, + ], + primaryKey: null, + }, + }, + ], + rules: [], +} satisfies ColnSchema + +interface CliResult { + exitCode: number | null + stdout: string + stderr: string +} + +function runCli(arguments_: string[], input = ""): Promise { + return new Promise((resolve, reject) => { + const child = spawn(process.execPath, ["dist/cli.js", ...arguments_], { + cwd: process.cwd(), + env: process.env, + timeout: 15_000, + }) + let stdout = "" + let stderr = "" + child.stdout.setEncoding("utf8").on("data", chunk => { stdout += chunk }) + child.stderr.setEncoding("utf8").on("data", chunk => { stderr += chunk }) + child.on("error", reject) + child.on("close", exitCode => resolve({ exitCode, stdout, stderr })) + child.stdin.end(input) + }) +} + +function documentArguments(command: string, documentUrl: string, verbose = false): string[] { + return [ + command, + "--document", + documentUrl, + "--endpoint", + endpoint, + ...(verbose ? ["--verbose"] : []), + ] +} + +test("CLI provides help, guide, and skill installation", async () => { + const help = await runCli(["help", "exec"]) + expect(help.exitCode).toBe(0) + expect(JSON.parse(help.stdout)).toMatchObject({ + protocolVersion: 0, + ok: true, + command: "help", + help: expect.any(String), + }) + + const guide = await runCli(["guide"]) + expect(guide.exitCode).toBe(0) + expect(JSON.parse(guide.stdout)).toMatchObject({ + protocolVersion: 0, + ok: true, + command: "guide", + guide: expect.any(String), + }) + + const root = mkdtempSync(join(tmpdir(), "coln-skills-")) + try { + const installed = await runCli(["install-skill", "--dir", root]) + expect(installed.exitCode).toBe(0) + expect(JSON.parse(installed.stdout)).toMatchObject({ + protocolVersion: 0, + ok: true, + command: "install-skill", + skill: { + name: "coln-repo", + status: "installed", + target: join(root, "coln-repo"), + files: expect.arrayContaining(["SKILL.md"]), + }, + }) + expect(existsSync(join(root, "coln-repo", "SKILL.md"))).toBe(true) + } finally { + rmSync(root, { recursive: true, force: true }) + } +}) + +test("CLI reads a store's IR and rows", async () => { + await initSubduction() + const creator = new Repo({ subductionWebsocketEndpoints: [endpoint] }) + + try { + const handle = creator.create(schema, colnDocType) + handle.change((transaction: ColnTransaction) => { + transaction.add(peopleTable, [{ tag: "string", value: "Ada" }]) + }) + await creator.flush() + + const ir = await runCli(documentArguments("ir", handle.url)) + expect(ir.exitCode).toBe(0) + expect(ir.stderr).toBe("") + const irResponse = JSON.parse(ir.stdout) + expect(irResponse).toMatchObject({ + protocolVersion: 0, + ok: true, + command: "ir", + document: { url: handle.url, endpoint, heads: expect.any(Array) }, + ir: schema, + }) + expect(irResponse.document.heads.length).toBeGreaterThan(0) + + const query = await runCli( + documentArguments("query", handle.url), + `store.scanTable("${peopleTable}").map(row => row.values[0].value)`, + ) + expect(query.exitCode).toBe(0) + const queryResponse = JSON.parse(query.stdout) + expect(queryResponse).toMatchObject({ + protocolVersion: 0, + ok: true, + command: "query", + document: { url: handle.url, endpoint, heads: expect.any(Array) }, + result: ["Ada"], + }) + expect(queryResponse.document.heads.length).toBeGreaterThan(0) + } finally { + await creator.shutdown() + } +}) + +test("CLI rejects legacy Automerge store URLs", async () => { + await initSubduction() + const creator = new Repo() + + try { + const handle = creator.create(schema, colnDocType) + const legacyUrl = handle.url.replace(/^coln:/, "automerge:") + const result = await runCli(documentArguments("ir", legacyUrl)) + + expect(result.exitCode).toBe(3) + expect(JSON.parse(result.stdout)).toMatchObject({ + ok: false, + command: "ir", + error: { + code: "DOCUMENT_OPEN_FAILED", + message: `Invalid Coln URL: ${legacyUrl}`, + mutationState: "not_applied", + }, + }) + } finally { + await creator.shutdown() + } +}) + +test("CLI commits dependent rows in one transaction", async () => { + await initSubduction() + const creator = new Repo({ subductionWebsocketEndpoints: [endpoint] }) + const verifier = new Repo({ subductionWebsocketEndpoints: [endpoint] }) + + try { + const handle = creator.create(schema, colnDocType) + await creator.flush() + + const result = await runCli(documentArguments("exec", handle.url, true), ` + const organizer = txn.add("${peopleTable}", [{ tag: "string", value: "Ada" }]) + const attendee = txn.add("${peopleTable}", [{ tag: "string", value: "Lin" }]) + const meeting = txn.add("${meetingsTable}", [organizer, attendee]) + console.log("scheduled") + return { organizer, attendee, meeting } + `) + expect(result.exitCode).toBe(0) + expect(result.stderr).toContain("scheduled") + const response = JSON.parse(result.stdout) + expect(response).toMatchObject({ + protocolVersion: 0, + ok: true, + command: "exec", + document: { + url: handle.url, + endpoint, + headsBefore: expect.any(Array), + headsAfter: expect.any(Array), + }, + sync: { status: "flushed" }, + }) + expect(response.document.headsBefore.length).toBeGreaterThan(0) + expect(response.document.headsAfter.length).toBeGreaterThan(0) + expect(response.document.headsAfter).not.toEqual(response.document.headsBefore) + expect(response.result.organizer.value.existing.commit).toBe(response.document.headsAfter[0]) + expect(response.result.attendee.value.existing.commit).toBe(response.document.headsAfter[0]) + expect(response.result.meeting.value.existing.commit).toBe(response.document.headsAfter[0]) + + const verified = await verifier.find(handle.url, colnDocType) + await expect.poll(() => verified.doc().scanTable(meetingsTable).length).toBe(1) + expect(verified.doc().scanTable(peopleTable)).toHaveLength(2) + expect(verified.doc().scanTable(meetingsTable)[0].values).toEqual([ + response.result.organizer, + response.result.attendee, + ]) + } finally { + await Promise.all([creator.shutdown(), verifier.shutdown()]) + } +}) + +test("CLI reports a failed transaction without changing the store", async () => { + await initSubduction() + const creator = new Repo({ subductionWebsocketEndpoints: [endpoint] }) + + try { + const handle = creator.create(schema, colnDocType) + await creator.flush() + + const result = await runCli( + documentArguments("exec", handle.url), + 'txn.add("Scheduling.Missing", [])', + ) + expect(result.exitCode).not.toBe(0) + const response = JSON.parse(result.stdout) + expect(response).toMatchObject({ + protocolVersion: 0, + ok: false, + command: "exec", + document: { + url: handle.url, + endpoint, + headsBefore: expect.any(Array), + headsAfter: expect.any(Array), + }, + error: { + code: "TRANSACTION_FAILED", + message: expect.any(String), + mutationState: "not_applied", + }, + }) + expect(response.error.message.length).toBeGreaterThan(0) + expect(response.document.headsAfter).toEqual(response.document.headsBefore) + + const query = await runCli( + documentArguments("query", handle.url), + `store.scanTable("${peopleTable}").length`, + ) + expect(query.exitCode).toBe(0) + expect(JSON.parse(query.stdout)).toMatchObject({ + protocolVersion: 0, + ok: true, + command: "query", + document: { url: handle.url, endpoint, heads: response.document.headsBefore }, + result: 0, + }) + } finally { + await creator.shutdown() + } +}) diff --git a/examples/cli-demo/tsconfig.build.json b/examples/cli-demo/tsconfig.build.json new file mode 100644 index 00000000..796337f7 --- /dev/null +++ b/examples/cli-demo/tsconfig.build.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "noEmit": false, + "outDir": "dist", + "rootDir": "src" + }, + "include": ["src/**/*.ts"] +} diff --git a/examples/cli-demo/tsconfig.json b/examples/cli-demo/tsconfig.json new file mode 100644 index 00000000..2dbc108f --- /dev/null +++ b/examples/cli-demo/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "Bundler", + "strict": true, + "noEmit": true, + "types": ["node"] + }, + "include": ["src/**/*.ts", "tests/**/*.ts", "playwright.config.ts"] +} diff --git a/examples/compiler-demo/index.html b/examples/compiler-demo/index.html deleted file mode 100644 index 12fc5b72..00000000 --- a/examples/compiler-demo/index.html +++ /dev/null @@ -1,115 +0,0 @@ - - - - - Coln - - - -
-
-

Coln

- -
-

Source

- -

Diagnostics

-
-

IR

-
-

JSON

-

-    
- - - - diff --git a/examples/index.html b/examples/index.html deleted file mode 100644 index 9dee93e3..00000000 --- a/examples/index.html +++ /dev/null @@ -1,17 +0,0 @@ - - - - - Coln - - - -
-

Coln demos

- -
- - diff --git a/examples/justfile b/examples/justfile index d4274e0f..1724801c 100644 --- a/examples/justfile +++ b/examples/justfile @@ -1,16 +1,38 @@ mod coln-compiler-wasm '../packages/coln-compiler-wasm' build_dir := "../_build/web/compiler" +lab_app_dir := "lab" -build-web-compiler: coln-compiler-wasm::build +stage-web-lab: #!/usr/bin/env bash set -euxo pipefail - cp compiler-demo/index.html {{ build_dir }}/index.html - cp style.css {{ build_dir }}/style.css - examples_dir="{{ build_dir }}/examples" - mkdir -p "$examples_dir" - cp ../packages/coln-compiler/test/golden/*.coln "$examples_dir/" - ls -1 "$examples_dir"/*.coln | xargs -n1 basename | jq -nR '[inputs]' > "$examples_dir/index.json" - -serve-web-compiler port="8000": build-web-compiler - simple-http-server --nocache --index --open --port {{ port }} {{ build_dir }} + if [[ ! -f "{{ build_dir }}/dist/coln.wasm" || ! -f "{{ build_dir }}/dist/ghc_wasm_jsffi.js" || ! -f "{{ build_dir }}/dist/loadHaskellWasm.js" ]]; then + just coln-compiler-wasm::build + fi + mkdir -p "{{ lab_app_dir }}/public/dist" + cp "{{ build_dir }}/dist/coln.wasm" "{{ lab_app_dir }}/public/dist/coln.wasm" + cp "{{ build_dir }}/dist/ghc_wasm_jsffi.js" "{{ lab_app_dir }}/public/dist/ghc_wasm_jsffi.js" + cp "{{ build_dir }}/dist/loadHaskellWasm.js" "{{ lab_app_dir }}/public/dist/loadHaskellWasm.js" + +build-web-lab: stage-web-lab + pnpm --dir {{ lab_app_dir }} build + +dev-web-lab: stage-web-lab + pnpm --dir {{ lab_app_dir }} dev + +patchwork_dir := "patchwork" + +stage-web-patchwork: + #!/usr/bin/env bash + set -euxo pipefail + if [[ ! -f "{{ build_dir }}/dist/coln.wasm" || ! -f "{{ build_dir }}/dist/ghc_wasm_jsffi.js" ]]; then + just coln-compiler-wasm::build + fi + pnpm --dir {{ patchwork_dir }} run stage + +build-web-patchwork: stage-web-patchwork + pnpm --dir {{ patchwork_dir }} build + +check-web-patchwork: + pnpm --dir {{ patchwork_dir }} test + pnpm --dir {{ patchwork_dir }} check diff --git a/examples/lab/.env.example b/examples/lab/.env.example new file mode 100644 index 00000000..1c65153c --- /dev/null +++ b/examples/lab/.env.example @@ -0,0 +1,2 @@ +# Browser-visible configuration for Coln Lab. +VITE_SUBDUCTION_ENDPOINT=ws://127.0.0.1:3030 diff --git a/examples/lab/.gitignore b/examples/lab/.gitignore new file mode 100644 index 00000000..4611f692 --- /dev/null +++ b/examples/lab/.gitignore @@ -0,0 +1,4 @@ +node_modules +public/dist +test-results +playwright-report diff --git a/examples/lab/README.md b/examples/lab/README.md new file mode 100644 index 00000000..63c23c80 --- /dev/null +++ b/examples/lab/README.md @@ -0,0 +1,27 @@ +# Coln Lab + +Coln Lab combines editors for theories and stores with an interactive Graph Demo. + +From the repository root: + +```console +just examples/dev-web-lab +``` + +The command stages the browser compiler and starts or reuses Lab's local +Subduction relay. Build-time browser configuration can be placed in +`examples/lab/.env`; exported shell variables take precedence. See +`examples/lab/.env.example` for available settings. + +Store Editor and Graph Demo can execute JavaScript directly on the browser main +thread. Run only code you trust. + +Lab owns its complete browser implementation, relay scripts, and graph schema. +Regenerate the checked-in graph bindings after changing `src/tools/graph/graph.coln`: + +```console +pnpm --dir examples/lab graph:generate +``` + +Run unit and browser tests with `pnpm --dir examples/lab test` and +`pnpm --dir examples/lab test:e2e`. diff --git a/examples/lab/index.html b/examples/lab/index.html new file mode 100644 index 00000000..754742c9 --- /dev/null +++ b/examples/lab/index.html @@ -0,0 +1,16 @@ + + + + + + + + + + Coln Lab + + +
+ + + diff --git a/examples/lab/package.json b/examples/lab/package.json new file mode 100644 index 00000000..dc8a0c2a --- /dev/null +++ b/examples/lab/package.json @@ -0,0 +1,45 @@ +{ + "name": "coln-lab", + "private": true, + "license": "(Apache-2.0 OR MIT)", + "type": "module", + "scripts": { + "dev": "node scripts/with-server.mjs vite --host 127.0.0.1", + "server": "node scripts/server.mjs", + "graph:generate": "bash src/tools/graph/compile.sh", + "check": "svelte-check --tsconfig ./tsconfig.json && tsc --noEmit", + "build": "pnpm run check && vite build && node scripts/check-static-build.mjs", + "test": "vitest run", + "test:e2e": "playwright test", + "test:e2e:dev": "PLAYWRIGHT_DEV=1 playwright test" + }, + "dependencies": { + "@automerge/automerge-codemirror": "0.2.0", + "@automerge/automerge-repo": "github:onsetsoftware/automerge-repo#test/custom-url-scemes&path:packages/automerge-repo", + "@automerge/automerge-repo-storage-indexeddb": "2.6.0-subduction.47", + "@automerge/automerge-subduction": "0.16.1", + "@codemirror/lang-javascript": "^6.2.5", + "@codemirror/state": "^6.7.2", + "@codemirror/view": "^6.43.11", + "@coln-project/repo": "link:../../packages/coln-repo", + "@coln-project/runtime": "link:../../packages/coln-js-runtime", + "codemirror": "^6.0.2", + "json-formatter-js": "^2.5.23", + "paneforge": "^1.0.2", + "svelte": "^5.57.0", + "ws": "^8.21.0" + }, + "devDependencies": { + "@playwright/test": "^1.62.1", + "@sveltejs/vite-plugin-svelte": "^7.3.0", + "@tailwindcss/vite": "^4.3.3", + "@types/node": "^26.4.1", + "@vitest/browser-playwright": "^4.1.11", + "svelte-check": "^4.7.6", + "tailwindcss": "^4.3.3", + "typescript": "^6.0.3", + "vite": "^8.2.2", + "vite-plugin-wasm": "^3.6.0", + "vitest": "^4.1.11" + } +} diff --git a/examples/lab/playwright.config.ts b/examples/lab/playwright.config.ts new file mode 100644 index 00000000..e112c911 --- /dev/null +++ b/examples/lab/playwright.config.ts @@ -0,0 +1,37 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import { defineConfig, devices } from "@playwright/test" + +export default defineConfig({ + testDir: "./tests", + timeout: 360_000, + expect: { timeout: 60_000 }, + fullyParallel: false, + retries: 0, + use: { + baseURL: "http://127.0.0.1:5180", + trace: "retain-on-failure", + }, + webServer: [ + { + command: + "SUBDUCTION_SERVICE_NAME=127.0.0.1:3033 PORT=3033 node scripts/server.mjs", + port: 3033, + reuseExistingServer: false, + stdout: "pipe", + stderr: "pipe", + }, + { + command: process.env.PLAYWRIGHT_DEV + ? "VITE_SUBDUCTION_ENDPOINT=ws://127.0.0.1:3033 ./node_modules/.bin/vite --host 127.0.0.1 --port 5180 --force" + : "VITE_SUBDUCTION_ENDPOINT=ws://127.0.0.1:3033 ./node_modules/.bin/vite build && ./node_modules/.bin/vite preview --host 127.0.0.1 --port 5180", + url: "http://127.0.0.1:5180", + timeout: 300_000, + reuseExistingServer: false, + stdout: "pipe", + stderr: "pipe", + }, + ], + projects: [{ name: "chromium", use: { ...devices["Desktop Chrome"] } }], +}) diff --git a/examples/lab/pnpm-lock.yaml b/examples/lab/pnpm-lock.yaml new file mode 100644 index 00000000..7b7dca46 --- /dev/null +++ b/examples/lab/pnpm-lock.yaml @@ -0,0 +1,1909 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +overrides: + '@automerge/automerge': 3.3.2 + '@automerge/automerge-subduction': 0.16.1 + +importers: + + .: + dependencies: + '@automerge/automerge-codemirror': + specifier: 0.2.0 + version: 0.2.0 + '@automerge/automerge-repo': + specifier: github:onsetsoftware/automerge-repo#test/custom-url-scemes&path:packages/automerge-repo + version: https://codeload.github.com/onsetsoftware/automerge-repo/tar.gz/b39d94c5624a3497150e08ba18546f29222289d8#path:packages/automerge-repo + '@automerge/automerge-repo-storage-indexeddb': + specifier: 2.6.0-subduction.47 + version: 2.6.0-subduction.47 + '@automerge/automerge-subduction': + specifier: 0.16.1 + version: 0.16.1 + '@codemirror/lang-javascript': + specifier: ^6.2.5 + version: 6.2.5 + '@codemirror/state': + specifier: ^6.7.2 + version: 6.7.2 + '@codemirror/view': + specifier: ^6.43.11 + version: 6.43.11 + '@coln-project/repo': + specifier: link:../../packages/coln-repo + version: link:../../packages/coln-repo + '@coln-project/runtime': + specifier: link:../../packages/coln-js-runtime + version: link:../../packages/coln-js-runtime + codemirror: + specifier: ^6.0.2 + version: 6.0.2 + json-formatter-js: + specifier: ^2.5.23 + version: 2.5.23 + paneforge: + specifier: ^1.0.2 + version: 1.0.2(svelte@5.57.0) + svelte: + specifier: ^5.57.0 + version: 5.57.0 + ws: + specifier: ^8.21.0 + version: 8.21.3 + devDependencies: + '@playwright/test': + specifier: ^1.62.1 + version: 1.62.1 + '@sveltejs/vite-plugin-svelte': + specifier: ^7.3.0 + version: 7.3.0(svelte@5.57.0)(vite@8.2.2(@types/node@26.4.1)(jiti@2.7.0)) + '@tailwindcss/vite': + specifier: ^4.3.3 + version: 4.3.3(vite@8.2.2(@types/node@26.4.1)(jiti@2.7.0)) + '@types/node': + specifier: ^26.4.1 + version: 26.4.1 + '@vitest/browser-playwright': + specifier: ^4.1.11 + version: 4.1.11(playwright@1.62.1)(vite@8.2.2(@types/node@26.4.1)(jiti@2.7.0))(vitest@4.1.11) + svelte-check: + specifier: ^4.7.6 + version: 4.7.6(picomatch@4.0.7)(svelte@5.57.0)(typescript@6.0.3) + tailwindcss: + specifier: ^4.3.3 + version: 4.3.3 + typescript: + specifier: ^6.0.3 + version: 6.0.3 + vite: + specifier: ^8.2.2 + version: 8.2.2(@types/node@26.4.1)(jiti@2.7.0) + vite-plugin-wasm: + specifier: ^3.6.0 + version: 3.6.0(vite@8.2.2(@types/node@26.4.1)(jiti@2.7.0)) + vitest: + specifier: ^4.1.11 + version: 4.1.11(@types/node@26.4.1)(@vitest/browser-playwright@4.1.11)(vite@8.2.2(@types/node@26.4.1)(jiti@2.7.0)) + +packages: + + '@automerge/automerge-codemirror@0.2.0': + resolution: {integrity: sha512-GBNh4ESZHFNHZKLznf/2G2KdmBD+unEdk64+l621LsjIsJs6HBmQmO8RCY1v7vIq31kFyoVHNcBMGQOnLgy4GA==} + + '@automerge/automerge-repo-storage-indexeddb@2.6.0-subduction.47': + resolution: {integrity: sha512-fYn6nse7jQnVb9EploTsBuYUzW7xFin2HZLneSEQ7w4pRCilIU92ghZEjZSWRKEPunSN0MYEvfJJJdlLyYTdkQ==} + engines: {node: '>=22.13'} + + '@automerge/automerge-repo@2.6.0-subduction.47': + resolution: {integrity: sha512-NGBQUjGH67Kyrc8a6zoafvDKT1+pnFi2/yiMgULS7l+apWQT2QkgQZ3vQGms/ngdrmM6ye1fhcwHwBn/DGJ6tA==} + engines: {node: '>=22.13'} + + '@automerge/automerge-repo@https://codeload.github.com/onsetsoftware/automerge-repo/tar.gz/b39d94c5624a3497150e08ba18546f29222289d8#path:packages/automerge-repo': + resolution: {gitHosted: true, path: packages/automerge-repo, tarball: https://codeload.github.com/onsetsoftware/automerge-repo/tar.gz/b39d94c5624a3497150e08ba18546f29222289d8} + version: 2.6.0-subduction.47 + engines: {node: '>=22.13'} + + '@automerge/automerge-subduction@0.16.1': + resolution: {integrity: sha512-alH7U4eYn0O7sT6hNLv7CJhNODJi+QD6qfRQTNfZCxL3Q5JZdJ8lFpdvXUkBf+I3bu0GLvVlTaYPWGrBihjpIw==} + + '@automerge/automerge@3.3.2': + resolution: {integrity: sha512-9vCdCL7pdQwUra66SBxPVHr+/t9epXKni9KDeak2rNBFMzABVh2u6gSpcwxi3jkR5qr047jVqZv9DlrOfVxLFw==} + + '@blazediff/core@1.9.1': + resolution: {integrity: sha512-ehg3jIkYKulZh+8om/O25vkvSsXXwC+skXmyA87FFx6A/45eqOkZsBltMw/TVteb0mloiGT8oGRTcjRAz66zaA==} + + '@cbor-extract/cbor-extract-darwin-arm64@2.2.2': + resolution: {integrity: sha512-ZKZ/F8US7JR92J4DMct6cLW/Y66o2K576+zjlEN/MevH70bFIsB10wkZEQPLzl2oNh2SMGy55xpJ9JoBRl5DOA==} + cpu: [arm64] + os: [darwin] + + '@cbor-extract/cbor-extract-darwin-x64@2.2.2': + resolution: {integrity: sha512-32b1mgc+P61Js+KW9VZv/c+xRw5EfmOcPx990JbCBSkYJFY0l25VinvyyWfl+3KjibQmAcYwmyzKF9J4DyKP/Q==} + cpu: [x64] + os: [darwin] + + '@cbor-extract/cbor-extract-linux-arm64@2.2.2': + resolution: {integrity: sha512-wfqgzqCAy/Vn8i6WVIh7qZd0DdBFaWBjPdB6ma+Wihcjv0gHqD/mw3ouVv7kbbUNrab6dKEx/w3xQZEdeXIlzg==} + cpu: [arm64] + os: [linux] + + '@cbor-extract/cbor-extract-linux-arm@2.2.2': + resolution: {integrity: sha512-tNg0za41TpQfkhWjptD+0gSD2fggMiDCSacuIeELyb2xZhr7PrhPe5h66Jc67B/5dmpIhI2QOUtv4SBsricyYQ==} + cpu: [arm] + os: [linux] + + '@cbor-extract/cbor-extract-linux-x64@2.2.2': + resolution: {integrity: sha512-rpiLnVEsqtPJ+mXTdx1rfz4RtUGYIUg2rUAZgd1KjiC1SehYUSkJN7Yh+aVfSjvCGtVP0/bfkQkXpPXKbmSUaA==} + cpu: [x64] + os: [linux] + + '@cbor-extract/cbor-extract-win32-x64@2.2.2': + resolution: {integrity: sha512-dI+9P7cfWxkTQ+oE+7Aa6onEn92PHgfWXZivjNheCRmTBDBf2fx6RyTi0cmgpYLnD1KLZK9ZYrMxaPZ4oiXhGA==} + cpu: [x64] + os: [win32] + + '@codemirror/autocomplete@6.20.3': + resolution: {integrity: sha512-tlosUqb+3BbxCxZdu4tKeRghPFC+QM7q4X5YhKV2eCmPG+1r2F3f4AaSz5sCrFqUtX4Jh20VFTKecl16MgiV9g==} + + '@codemirror/commands@6.11.0': + resolution: {integrity: sha512-/K4Rl5BN0OtTiPWmJCdqODu38XnDMsDxKY5rgrPnCkutPTJf2wVbkoixLfealF5Kwse/s8P8M5jAiURiwSwnFA==} + + '@codemirror/lang-javascript@6.2.5': + resolution: {integrity: sha512-zD4e5mS+50htS7F+TYjBPsiIFGanfVqg4HyUz6WNFikgOPf2BgKlx+TQedI1w6n/IqRBVBbBWmGFdLB/7uxO4A==} + + '@codemirror/language@6.12.4': + resolution: {integrity: sha512-1q4PaT+o6PbgpkJt4Q8Fv5XJxTy4FUZ4MWETtyiDw3J0Pyr9E2vqcKL+k9wcvjNTIsauxvE7OfmWj3FRPHQ76A==} + + '@codemirror/lint@6.9.7': + resolution: {integrity: sha512-28/+iWLYxKxsvGYhSYL7zaCZqLz5+FFFDq9tVsvGv9kv8RY4fFAchJ5WX9M3YrrRlTIsECjsXPqeNgnSmNP2dg==} + + '@codemirror/search@6.7.2': + resolution: {integrity: sha512-gUYkYhT2+n/+VGZ+8EzE5WFkYZUZYm1VOKDudIsNqh42uRVQJ0a6Yss9sdKT3MeOYfuL1N6AZA57oza0Oyr0LA==} + + '@codemirror/state@6.7.2': + resolution: {integrity: sha512-U3RiPX62Wl/Gx4ftQ7UxLlloSfsFTQqKa+7vFBYteZGCzkp6oBqcsD7iSwniWRGobCAmDcwZSY+Six3+3ztdfg==} + + '@codemirror/view@6.43.11': + resolution: {integrity: sha512-2+esucbQX6wB2JYi1eDvdCPFTA31BN8oSy6xCmk3G6CloV11yOvEjYk+gH7kLrP0MuHG94E8WDhjs5oMiu3+Wg==} + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.6.0': + resolution: {integrity: sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@lezer/common@1.5.2': + resolution: {integrity: sha512-sxQE460fPZyU3sdc8lafxiPwJHBzZRy/udNFynGQky1SePYBdhkBl1kOagA9uT3pxR8K09bOrmTUqA9wb/PjSQ==} + + '@lezer/highlight@1.2.3': + resolution: {integrity: sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==} + + '@lezer/javascript@1.5.4': + resolution: {integrity: sha512-vvYx3MhWqeZtGPwDStM2dwgljd5smolYD2lR2UyFcHfxbBQebqx8yjmFmxtJ/E6nN6u1D9srOiVWm3Rb4tmcUA==} + + '@lezer/lr@1.4.10': + resolution: {integrity: sha512-rnCpTIBafOx4mRp43xOxDJbFipJm/c0cia/V5TiGlhmMa+wsSdoGmUN3w5Bqrks/09Q/D4tNAmWaT8p6NRi77A==} + + '@marijn/find-cluster-break@1.0.4': + resolution: {integrity: sha512-Wy0V7+SGUjnF9/TkiM1hKVDPj7jKXduPNboMVtHTA8dySMURWqfg/JZ9E2Sq8JgSJmkl7k7Qe9FLeMSrSraWmQ==} + + '@noble/hashes@1.8.0': + resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==} + engines: {node: ^14.21.3 || >=16} + + '@oxc-project/types@0.148.0': + resolution: {integrity: sha512-Nm4s/jB+4FpFsPhWGEC4h7rzksesmtnMXomo6rCMcg/b8zLQuOziRgkCS1fxDCXOlJB/6Q8oABOZ/OP6RIPj9A==} + + '@playwright/test@1.62.1': + resolution: {integrity: sha512-DTcUc8qii+cpHvtOwggMtBRMjKZHXYWdw8syRYu2vtzuq4Wxphqq4NfCs5Zt44L6mA8rfDfj+PHnxFc/FeK6mQ==} + engines: {node: '>=20'} + hasBin: true + + '@polka/url@1.0.0-next.29': + resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} + + '@rolldown/binding-android-arm-eabi@1.2.7': + resolution: {integrity: sha512-EypzgnYCwyVY4NDHKzGmNJT5b+XaQEBniHxsMdeIQLB/tcCzZnhqrzHpZFbX9iaxx+5RiB8caATBtfvZP7zVxQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] + + '@rolldown/binding-android-arm64@1.2.7': + resolution: {integrity: sha512-l17HE9EweWaqJZhuUuNBN/FzM62xw+DECVnJyvMsxn8vJFAGLy5QfLDoYAcronkAN8VxKZHezDpulHDPx95vFw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.2.7': + resolution: {integrity: sha512-8ED8ELFvHXc6OCETIn4gXObPiaR6bckM/ipXtbzlPVDRMBfEGjCKgO90F9YtfdpDatVx/ZQw7aZ1vUMf/+T3Mw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.2.7': + resolution: {integrity: sha512-/WPripjtiAIZ2tWY7ddijORT0Ujg87wxWW/qcoFVCKAWVDPhtY0xr7Dj0M3GyNGz60jGwTElhro/mkF9dT7dDQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.2.7': + resolution: {integrity: sha512-14DI4NcqpvbICxSnGLx3PmtDaWqRP/KGSGb6C+JLLVPeZRl6dKdHba3pGsqT3vpdTqhEYIPG0MMQ8c0xYqoJxA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.2.7': + resolution: {integrity: sha512-bxrWIRvHWQvbJwi+VIie/kDJmQxcNE6xxWwZdqF/ExVAigtHkv54WTLQPb+QsZdnFy18fg7JPfWGL0RH6vwIlQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.2.7': + resolution: {integrity: sha512-toOY2BChBZyuxU7OYX6Tn389di4IzAqPTycVcci0O7FSfBqzRB3RZn+K5Is6ANf4tmgRd/K1yZTsNTXbkXsnLg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-arm64-musl@1.2.7': + resolution: {integrity: sha512-lAIXTH/aiLRLxsTgQvfhjo4K1ydWIp00+V0voOr9beb/9ZmkUFrSIb03dXNFRgMNvkE6oGsF10ioQ6UsI+vS5Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rolldown/binding-linux-ppc64-gnu@1.2.7': + resolution: {integrity: sha512-kdnwS28Pkenp/mZMRwjXXXwxQ7pIsm+bF919LUK93BOyhcLsrVKdP2p9fxpiPNPAbNuch8ypQt0pm2P2LYCAGg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-s390x-gnu@1.2.7': + resolution: {integrity: sha512-516OdsyLdr5E65paF3yBF55t8mfm9+gmtCsK3xI7XKXIT7EfRlHhxL8K/NR6Hu8BWSgF5+1w74lTL0+nxcc8Qw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-gnu@1.2.7': + resolution: {integrity: sha512-r8/z8n7GFaYRln3xmP1Cxy0HH/HLM0uBUPkEuSVEfKGDA89M0FsZRZJRSwe/tJjRx+fpH/gjorfhB8tmEbSFLA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-musl@1.2.7': + resolution: {integrity: sha512-pAsE8iiDxUg1xBqdhrTfg45AVDVpirjz00sblEYClGNNcMnDb+e8beQgqIAw6LvauX/APvgxUnwrgun/YYGBhw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rolldown/binding-openharmony-arm64@1.2.7': + resolution: {integrity: sha512-lTcIYmmnQQA8Or/2DatS6oSqcdLHvendjS+zLu+FwgToynWMRSmQdpM65fTANJgIS4mjbMOo5KT2lnT9SAb96w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-win32-arm64-msvc@1.2.7': + resolution: {integrity: sha512-e3Gu3WxbNk/UqQhxqU7YIYO+9ZBvWNz3U+h/qRFosscMFzdRPbXYSaSWgSnklv2fz1TgzBTcti2z35c/7irsHw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.2.7': + resolution: {integrity: sha512-W/jg5qoRSqjsEv0+dZi4e687mcHqmVuU0P4fK6qS/xjetW2Gmc1W8j//z5nAeNcC8Ttm0hV46IjcYeuVwYhuiw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@rolldown/pluginutils@1.0.1': + resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} + + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + + '@sveltejs/acorn-typescript@1.0.13': + resolution: {integrity: sha512-wgKggnhZVL9Bfx1OaKKTrYY9BFRk6C8UAkQNUcIv1+llzYrIqy+RZm5HPKzn0NpEBvTVhTqB4kQyllZywsRBRQ==} + peerDependencies: + acorn: ^8.9.0 + + '@sveltejs/load-config@0.2.3': + resolution: {integrity: sha512-VT3qmUb8pRV2QrZjd8iAmtg8lf4W0TIjZbvXtz5MKei/q96teWZgGJyyidJzOjzZzvdq616eSRVeMYIQChUTAQ==} + engines: {node: '>= 18.0.0'} + + '@sveltejs/vite-plugin-svelte@7.3.0': + resolution: {integrity: sha512-QbRoJyD92e9R0ufeQIWRHrCC0ObcqSv/aBDdrQMoU+sypav3cDx5wytdQ6GLdXjEMO6xjrXGzfkUygng8JMv0A==} + engines: {node: ^20.19 || ^22.12 || >=24} + peerDependencies: + svelte: ^5.46.4 + vite: ^8.0.0-beta.7 || ^8.0.0 + + '@tailwindcss/node@4.3.3': + resolution: {integrity: sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg==} + + '@tailwindcss/oxide-android-arm64@4.3.3': + resolution: {integrity: sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [android] + + '@tailwindcss/oxide-darwin-arm64@4.3.3': + resolution: {integrity: sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [darwin] + + '@tailwindcss/oxide-darwin-x64@4.3.3': + resolution: {integrity: sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw==} + engines: {node: '>= 20'} + cpu: [x64] + os: [darwin] + + '@tailwindcss/oxide-freebsd-x64@4.3.3': + resolution: {integrity: sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw==} + engines: {node: '>= 20'} + cpu: [x64] + os: [freebsd] + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.3': + resolution: {integrity: sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ==} + engines: {node: '>= 20'} + cpu: [arm] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-gnu@4.3.3': + resolution: {integrity: sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@tailwindcss/oxide-linux-arm64-musl@4.3.3': + resolution: {integrity: sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@tailwindcss/oxide-linux-x64-gnu@4.3.3': + resolution: {integrity: sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@tailwindcss/oxide-linux-x64-musl@4.3.3': + resolution: {integrity: sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@tailwindcss/oxide-wasm32-wasi@4.3.3': + resolution: {integrity: sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + bundledDependencies: + - '@napi-rs/wasm-runtime' + - '@emnapi/core' + - '@emnapi/runtime' + - '@tybys/wasm-util' + - '@emnapi/wasi-threads' + - tslib + + '@tailwindcss/oxide-win32-arm64-msvc@4.3.3': + resolution: {integrity: sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [win32] + + '@tailwindcss/oxide-win32-x64-msvc@4.3.3': + resolution: {integrity: sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw==} + engines: {node: '>= 20'} + cpu: [x64] + os: [win32] + + '@tailwindcss/oxide@4.3.3': + resolution: {integrity: sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA==} + engines: {node: '>= 20'} + + '@tailwindcss/vite@4.3.3': + resolution: {integrity: sha512-yYU8cogLeSh/ms2jh8Fj7jaba/EWa7Ja6GoUqYZaraEuCI5YS6ms6ObZgjjedm+jm6XZjdNRWBpPP6Z86oOxcw==} + peerDependencies: + vite: ^5.2.0 || ^6 || ^7 || ^8 + + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + + '@types/node@26.4.1': + resolution: {integrity: sha512-k97ENvZWtvA6yqz5/FS6a7duDgOPEeOQOc2iKS/nY6mX6qJUKtLnWzQS+Xj6tXweyj6ZcTAK2Qecetnvi9nCLA==} + + '@vitest/browser-playwright@4.1.11': + resolution: {integrity: sha512-riLBxPqwnJ0lWs2DN2WeUfYeKLoAjbP2Xx8cLQdSddzMi20sksIa6K2mPz79DyMZKKVKH2ksOC2yJvtNcZg8cg==} + peerDependencies: + playwright: '*' + vitest: 4.1.11 + + '@vitest/browser@4.1.11': + resolution: {integrity: sha512-bwMovvAeuTFOK5kIFevw4VEf+1gVEICv4SYK4k3knJOxl6b1zEWud8mYKD73e1B0odAn174h1MofURy2TPWf3w==} + peerDependencies: + vitest: 4.1.11 + + '@vitest/expect@4.1.11': + resolution: {integrity: sha512-VX2x5vNJXET47KAFzwERI+KRMtTTCSWTfSMKsW7JsUsXV4psq++e3DvZpuTDOpHcxytiDs6p2nhVb2tVDiiUYw==} + + '@vitest/mocker@4.1.11': + resolution: {integrity: sha512-2XJVD55d1o5AZous5CCGKS74g/riOj9odEt2bQpCVZeblHyHdnMeFl4jl0XjU21stf4mbjUkew2eXQZt65g5CQ==} + peerDependencies: + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@4.1.11': + resolution: {integrity: sha512-yiZzPbGTS9Sr/JpFl8zHrcIkAofNbFV6k21vIgQN/cY/oxZeXhJv5sc/MBJ5jFKWmWs+oJHw0UXLZjmf931+Vw==} + + '@vitest/runner@4.1.11': + resolution: {integrity: sha512-LztvUgdwMNJMIkj3hQnnxiC2Xy1zNxq928W/xhjCLaNCzqTZOudjwbQf6v9IntZGPw132i2Lq2rgTRZHD3JHNw==} + + '@vitest/snapshot@4.1.11': + resolution: {integrity: sha512-pN7ikn1ON7h8ee4gIAp4AzyK+zBtJPzVbqOgu5LCEh4VaJVbPQcgYQYJIMGQPXVeJJq1fnfazis7a5pFNPahog==} + + '@vitest/spy@4.1.11': + resolution: {integrity: sha512-apNa/prQy2qCeywhnixOHPRCgGNhvg7T4Dapfl1GahLp/R+uhBm5cPyFoNVyqsNd2h1nJxL6BqqdIjiABL60YA==} + + '@vitest/utils@4.1.11': + resolution: {integrity: sha512-zTCVGpyFsGWBhllOyKlTw/vnr6D9qxsfSDyfbyZmTyjHw5N/VuvzHpHoQjm2ZJzn4RJgx5w4r7V0er69CmLgPQ==} + + acorn@8.18.0: + resolution: {integrity: sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==} + engines: {node: '>=0.4.0'} + hasBin: true + + aria-query@5.3.1: + resolution: {integrity: sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==} + engines: {node: '>= 0.4'} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + axobject-query@4.1.0: + resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} + engines: {node: '>= 0.4'} + + base-x@5.0.1: + resolution: {integrity: sha512-M7uio8Zt++eg3jPj+rHMfCC+IuygQHHCOU+IYsVtik6FWjuYpVt/+MRKcgsAMHh8mMFAwnB+Bs+mTrFiXjMzKg==} + + bs58@6.0.0: + resolution: {integrity: sha512-PD0wEnEYg6ijszw/u8s+iI3H17cTymlrwkKhDhPZq+Sokl3AU4htyBFTjAeNAlCCmg0f53g6ih3jATyCKftTfw==} + + bs58check@4.0.0: + resolution: {integrity: sha512-FsGDOnFg9aVI9erdriULkd/JjEWONV/lQE5aYziB5PoBsXRind56lh8doIZIc9X4HoxT5x4bLjMWN1/NB8Zp5g==} + + cbor-extract@2.2.2: + resolution: {integrity: sha512-hlSxxI9XO2yQfe9g6msd3g4xCfDqK5T5P0fRMLuaLHhxn4ViPrm+a+MUfhrvH2W962RGxcBwEGzLQyjbDG1gng==} + hasBin: true + + cbor-x@1.6.6: + resolution: {integrity: sha512-8QiD9PGOxyQHo7s2pzwTBH6lTjqekxPdl9Aq6fXvZgCuCJHOht1puDEA/fTr6mciB76c+M+Gi0qT2i1a4pm4Wg==} + + chai@6.2.2: + resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} + engines: {node: '>=18'} + + chokidar@4.0.3: + resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} + engines: {node: '>= 14.16.0'} + + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + + codemirror@6.0.2: + resolution: {integrity: sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + crelt@1.0.7: + resolution: {integrity: sha512-aK6BbWfhf4U/wCcLHKPJl/xa6VkVstRaPywWtMKGwuOLc/wZTyQYuoxgvZnNsBvv7Kg3YTBQYYBCggcviQczuA==} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + deepmerge@4.3.1: + resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} + engines: {node: '>=0.10.0'} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + devalue@5.9.2: + resolution: {integrity: sha512-po4PAY5c53tw5XMocSnf8A/5OHhbbUftpr93aEN6BBoAdntUmK7vu7wOATqvt7cXO7m1Cl4gMVn6p7n6n4mj0w==} + + enhanced-resolve@5.24.5: + resolution: {integrity: sha512-L1l8TNvomm6UVW5B253AGxQagSQr+vGwhMlrrfRS2qmhx46AMpMVJKQYLvWYbysTMY8VoicOvzHzoHMbyzB+4A==} + engines: {node: '>=10.13.0'} + + es-module-lexer@2.3.2: + resolution: {integrity: sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==} + + esm-env@1.2.2: + resolution: {integrity: sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==} + + esrap@2.3.6: + resolution: {integrity: sha512-yc0OC12UjPqLoc+fe+v5GNs4TOjAigUw3sTikfC+xeBPGUw7gDRz3DtYaqEhxyMVJojcSWJw7jT0QWR+CbuE/A==} + peerDependencies: + '@typescript-eslint/types': ^8.2.0 + peerDependenciesMeta: + '@typescript-eslint/types': + optional: true + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + eventemitter3@5.0.4: + resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==} + + expect-type@1.4.0: + resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==} + engines: {node: '>=12.0.0'} + + fast-sha256@1.3.0: + resolution: {integrity: sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fsevents@2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + inline-style-parser@0.2.7: + resolution: {integrity: sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==} + + is-reference@3.0.3: + resolution: {integrity: sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==} + + isomorphic-ws@5.0.0: + resolution: {integrity: sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw==} + peerDependencies: + ws: '*' + + jiti@2.7.0: + resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} + hasBin: true + + json-formatter-js@2.5.23: + resolution: {integrity: sha512-Cbm8wHXjo/C56aCePP1VuKvjxoMEmL7g7Ckss1oWFFlCsvOEEbye1kTeaNNaqba1Cl6YpIOYAnK65pUQ8mDIUQ==} + + lightningcss-android-arm64@1.32.0: + resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-android-arm64@1.33.0: + resolution: {integrity: sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.32.0: + resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-arm64@1.33.0: + resolution: {integrity: sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.32.0: + resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-darwin-x64@1.33.0: + resolution: {integrity: sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.32.0: + resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-freebsd-x64@1.33.0: + resolution: {integrity: sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.32.0: + resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm-gnueabihf@1.33.0: + resolution: {integrity: sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.32.0: + resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + lightningcss-linux-arm64-gnu@1.33.0: + resolution: {integrity: sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + lightningcss-linux-arm64-musl@1.32.0: + resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + lightningcss-linux-arm64-musl@1.33.0: + resolution: {integrity: sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + lightningcss-linux-x64-gnu@1.32.0: + resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + lightningcss-linux-x64-gnu@1.33.0: + resolution: {integrity: sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + lightningcss-linux-x64-musl@1.32.0: + resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + lightningcss-linux-x64-musl@1.33.0: + resolution: {integrity: sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + lightningcss-win32-arm64-msvc@1.32.0: + resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-arm64-msvc@1.33.0: + resolution: {integrity: sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.32.0: + resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss-win32-x64-msvc@1.33.0: + resolution: {integrity: sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.32.0: + resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} + engines: {node: '>= 12.0.0'} + + lightningcss@1.33.0: + resolution: {integrity: sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==} + engines: {node: '>= 12.0.0'} + + locate-character@3.0.0: + resolution: {integrity: sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + magic-string@1.2.3: + resolution: {integrity: sha512-Bpb0W2TbLKOZ7vJnOUnVRGq3WL2p+ISV29M6hYPL1AFCpyKZpdr5ytiXoTSSxRVhg8YW7f65+6gbG8WG6PCa/g==} + + mri@1.2.0: + resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} + engines: {node: '>=4'} + + mrmime@2.0.1: + resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} + engines: {node: '>=10'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.18: + resolution: {integrity: sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + node-gyp-build-optional-packages@5.1.1: + resolution: {integrity: sha512-+P72GAjVAbTxjjwUmwjVrqrdZROD4nf8KgpBoDxqXXTiYZZt/ud60dE5yvCSr9lRO8e8yv6kgJIC0K0PfZFVQw==} + hasBin: true + + obug@2.1.4: + resolution: {integrity: sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==} + engines: {node: '>=12.20.0'} + + paneforge@1.0.2: + resolution: {integrity: sha512-KzmIXQH1wCfwZ4RsMohD/IUtEjVhteR+c+ulb/CHYJHX8SuDXoJmChtsc/Xs5Wl8NHS4L5Q7cxL8MG40gSU1bA==} + peerDependencies: + svelte: ^5.29.0 + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.7: + resolution: {integrity: sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==} + engines: {node: '>=12'} + + playwright-core@1.62.1: + resolution: {integrity: sha512-wPYSwEBJY9GHraISXqyqtx0na0LpO3XEX7jNDhntbex7tzUS7kLnZsOlFruFJB4Hi/rhDMjXGqHewDZ68nYZVw==} + engines: {node: '>=20'} + hasBin: true + + playwright@1.62.1: + resolution: {integrity: sha512-0M+L3LAD8/nm554LOla9Ayx0j0tmFZ0FBcoQ7F1VuVHpM/XpiC8RcDzBQB8W5+hA8L22THxELzeF+2WcUzvcLg==} + engines: {node: '>=20'} + hasBin: true + + pngjs@7.0.0: + resolution: {integrity: sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==} + engines: {node: '>=14.19.0'} + + postcss@8.5.28: + resolution: {integrity: sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==} + engines: {node: ^10 || ^12 || >=14} + + readdirp@4.1.2: + resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} + engines: {node: '>= 14.18.0'} + + rolldown@1.2.7: + resolution: {integrity: sha512-g0EtLvBjTUB7jhyV0S/TCup3v/XSVl45vUIGbOGU4QPiyjTenCe4mKuFvW9fEgYmS2Fo42AUssRmNuMziXdrig==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + + runed@0.23.4: + resolution: {integrity: sha512-9q8oUiBYeXIDLWNK5DfCWlkL0EW3oGbk845VdKlPeia28l751VpfesaB/+7pI6rnbx1I6rqoZ2fZxptOJLxILA==} + peerDependencies: + svelte: ^5.7.0 + + runed@0.29.2: + resolution: {integrity: sha512-0cq6cA6sYGZwl/FvVqjx9YN+1xEBu9sDDyuWdDW1yWX7JF2wmvmVKfH+hVCZs+csW+P3ARH92MjI3H9QTagOQA==} + peerDependencies: + svelte: ^5.7.0 + + sade@1.8.1: + resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==} + engines: {node: '>=6'} + + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + sirv@3.0.2: + resolution: {integrity: sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==} + engines: {node: '>=18'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + std-env@4.2.0: + resolution: {integrity: sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==} + + style-mod@4.1.3: + resolution: {integrity: sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==} + + style-to-object@1.0.14: + resolution: {integrity: sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==} + + svelte-check@4.7.6: + resolution: {integrity: sha512-t2scM//ZuVbSY/T2w6FSBw1v9s2NEmh/g+sy1lqtosW5ylBV5AF4wFb1Ts9Kf3MbfPDUDJDZ9L436YT0SPTdvw==} + engines: {node: '>= 18.0.0'} + hasBin: true + peerDependencies: + svelte: ^4.0.0 || ^5.0.0-next.0 + typescript: ^5.0.0 || ^6.0.0 + + svelte-toolbelt@0.9.3: + resolution: {integrity: sha512-HCSWxCtVmv+c6g1ACb8LTwHVbDqLKJvHpo6J8TaqwUme2hj9ATJCpjCPNISR1OCq2Q4U1KT41if9ON0isINQZw==} + engines: {node: '>=18', pnpm: '>=8.7.0'} + peerDependencies: + svelte: ^5.30.2 + + svelte@5.57.0: + resolution: {integrity: sha512-NdbDn7fl4be1ViUG0oq/lvG6OZy3oENolV2ONjiqqsfVoeAfzaQAKUcEX3MrQod/Bebv1PgwET9rfXhgn9s4Kg==} + engines: {node: '>=18'} + + tailwindcss@4.3.3: + resolution: {integrity: sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==} + + tapable@2.3.3: + resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==} + engines: {node: '>=6'} + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@1.3.1: + resolution: {integrity: sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA==} + engines: {node: '>=18'} + + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + + tinyrainbow@3.1.1: + resolution: {integrity: sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==} + engines: {node: '>=14.0.0'} + + totalist@3.0.1: + resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} + engines: {node: '>=6'} + + typescript@6.0.3: + resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@8.3.0: + resolution: {integrity: sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==} + + uuid@14.0.2: + resolution: {integrity: sha512-xZe/16rV4aa+HGSOCiY2YeLT1OybRLrrkL/Rqaq7p7GMVXjFh+6wN4oMYgjFmnSnhY8t6Xpdl2l9qmnHYuMHwQ==} + hasBin: true + + vite-plugin-wasm@3.6.0: + resolution: {integrity: sha512-mL/QPziiIA4RAA6DkaZZzOstdwbW5jO4Vz7Zenj0wieKWBlNvIvX5L5ljum9lcUX0ShNfBgCNLKTjNkRVVqcsw==} + peerDependencies: + vite: ^2 || ^3 || ^4 || ^5 || ^6 || ^7 || ^8 + + vite@8.2.2: + resolution: {integrity: sha512-cFKLV/PRgAUlIRm5WjMjJ86jrftzpqcgH+Us+DS8mI3CDNiH30Whrz8uHL3+MOLPAgqbMBAqWdAHAphOAM+z/Q==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + '@vitejs/devtools': ^0.4.0 || ^0.5.0 + esbuild: ^0.27.0 || ^0.28.0 + jiti: '>=1.21.0' + less: ^4.0.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + '@vitejs/devtools': + optional: true + esbuild: + optional: true + jiti: + optional: true + less: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitefu@1.1.3: + resolution: {integrity: sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg==} + peerDependencies: + vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + vite: + optional: true + + vitest@4.1.11: + resolution: {integrity: sha512-fhACrNXUidIbGSBr5FlbuBkO7VWC1ZyLl0DO4CU2DrQoAPxX84Ysxs+HeGQpii5lZWV1Q4gBZTTu49mF+A6Edw==} + engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@opentelemetry/api': ^1.9.0 + '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 4.1.11 + '@vitest/browser-preview': 4.1.11 + '@vitest/browser-webdriverio': 4.1.11 + '@vitest/coverage-istanbul': 4.1.11 + '@vitest/coverage-v8': 4.1.11 + '@vitest/ui': 4.1.11 + happy-dom: '*' + jsdom: '*' + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@opentelemetry/api': + optional: true + '@types/node': + optional: true + '@vitest/browser-playwright': + optional: true + '@vitest/browser-preview': + optional: true + '@vitest/browser-webdriverio': + optional: true + '@vitest/coverage-istanbul': + optional: true + '@vitest/coverage-v8': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + w3c-keyname@2.2.8: + resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==} + + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + + ws@8.21.3: + resolution: {integrity: sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + xstate@5.32.6: + resolution: {integrity: sha512-WfA8WNrh6r9osuGwVm+aIPM4jmMW2LKHV1Yv9thYpOtL4HVF/kobh95K/O8v2jDCpDmP2EoY+6uvuqHXCZ6ZLw==} + + zimmerframe@1.1.5: + resolution: {integrity: sha512-msJxIvYDYcoNL+PJsu+7qmpDWsYmAxTY+2TNYXXF0hzBzBk0BMecOqDOG/EckUoKCuKwObfbugIl8QpqHDXeFA==} + +snapshots: + + '@automerge/automerge-codemirror@0.2.0': + dependencies: + '@automerge/automerge': 3.3.2 + '@codemirror/state': 6.7.2 + '@codemirror/view': 6.43.11 + codemirror: 6.0.2 + + '@automerge/automerge-repo-storage-indexeddb@2.6.0-subduction.47': + dependencies: + '@automerge/automerge-repo': 2.6.0-subduction.47 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + '@automerge/automerge-repo@2.6.0-subduction.47': + dependencies: + '@automerge/automerge': 3.3.2 + '@automerge/automerge-subduction': 0.16.1 + bs58check: 4.0.0 + cbor-x: 1.6.6 + debug: 4.4.3 + eventemitter3: 5.0.4 + fast-sha256: 1.3.0 + isomorphic-ws: 5.0.0(ws@8.21.3) + uuid: 14.0.2 + ws: 8.21.3 + xstate: 5.32.6 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + '@automerge/automerge-repo@https://codeload.github.com/onsetsoftware/automerge-repo/tar.gz/b39d94c5624a3497150e08ba18546f29222289d8#path:packages/automerge-repo': + dependencies: + '@automerge/automerge': 3.3.2 + '@automerge/automerge-subduction': 0.16.1 + bs58check: 4.0.0 + cbor-x: 1.6.6 + debug: 4.4.3 + eventemitter3: 5.0.4 + fast-sha256: 1.3.0 + isomorphic-ws: 5.0.0(ws@8.21.3) + uuid: 14.0.2 + ws: 8.21.3 + xstate: 5.32.6 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + '@automerge/automerge-subduction@0.16.1': {} + + '@automerge/automerge@3.3.2': {} + + '@blazediff/core@1.9.1': {} + + '@cbor-extract/cbor-extract-darwin-arm64@2.2.2': + optional: true + + '@cbor-extract/cbor-extract-darwin-x64@2.2.2': + optional: true + + '@cbor-extract/cbor-extract-linux-arm64@2.2.2': + optional: true + + '@cbor-extract/cbor-extract-linux-arm@2.2.2': + optional: true + + '@cbor-extract/cbor-extract-linux-x64@2.2.2': + optional: true + + '@cbor-extract/cbor-extract-win32-x64@2.2.2': + optional: true + + '@codemirror/autocomplete@6.20.3': + dependencies: + '@codemirror/language': 6.12.4 + '@codemirror/state': 6.7.2 + '@codemirror/view': 6.43.11 + '@lezer/common': 1.5.2 + + '@codemirror/commands@6.11.0': + dependencies: + '@codemirror/language': 6.12.4 + '@codemirror/state': 6.7.2 + '@codemirror/view': 6.43.11 + '@lezer/common': 1.5.2 + + '@codemirror/lang-javascript@6.2.5': + dependencies: + '@codemirror/autocomplete': 6.20.3 + '@codemirror/language': 6.12.4 + '@codemirror/lint': 6.9.7 + '@codemirror/state': 6.7.2 + '@codemirror/view': 6.43.11 + '@lezer/common': 1.5.2 + '@lezer/javascript': 1.5.4 + + '@codemirror/language@6.12.4': + dependencies: + '@codemirror/state': 6.7.2 + '@codemirror/view': 6.43.11 + '@lezer/common': 1.5.2 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.10 + style-mod: 4.1.3 + + '@codemirror/lint@6.9.7': + dependencies: + '@codemirror/state': 6.7.2 + '@codemirror/view': 6.43.11 + crelt: 1.0.7 + + '@codemirror/search@6.7.2': + dependencies: + '@codemirror/state': 6.7.2 + '@codemirror/view': 6.43.11 + crelt: 1.0.7 + + '@codemirror/state@6.7.2': + dependencies: + '@marijn/find-cluster-break': 1.0.4 + + '@codemirror/view@6.43.11': + dependencies: + '@codemirror/state': 6.7.2 + crelt: 1.0.7 + style-mod: 4.1.3 + w3c-keyname: 2.2.8 + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.6.0 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.6.0': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.6.0 + + '@lezer/common@1.5.2': {} + + '@lezer/highlight@1.2.3': + dependencies: + '@lezer/common': 1.5.2 + + '@lezer/javascript@1.5.4': + dependencies: + '@lezer/common': 1.5.2 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.10 + + '@lezer/lr@1.4.10': + dependencies: + '@lezer/common': 1.5.2 + + '@marijn/find-cluster-break@1.0.4': {} + + '@noble/hashes@1.8.0': {} + + '@oxc-project/types@0.148.0': {} + + '@playwright/test@1.62.1': + dependencies: + playwright: 1.62.1 + + '@polka/url@1.0.0-next.29': {} + + '@rolldown/binding-android-arm-eabi@1.2.7': + optional: true + + '@rolldown/binding-android-arm64@1.2.7': + optional: true + + '@rolldown/binding-darwin-arm64@1.2.7': + optional: true + + '@rolldown/binding-darwin-x64@1.2.7': + optional: true + + '@rolldown/binding-freebsd-x64@1.2.7': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.2.7': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.2.7': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.2.7': + optional: true + + '@rolldown/binding-linux-ppc64-gnu@1.2.7': + optional: true + + '@rolldown/binding-linux-s390x-gnu@1.2.7': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.2.7': + optional: true + + '@rolldown/binding-linux-x64-musl@1.2.7': + optional: true + + '@rolldown/binding-openharmony-arm64@1.2.7': + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.2.7': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.2.7': + optional: true + + '@rolldown/pluginutils@1.0.1': {} + + '@standard-schema/spec@1.1.0': {} + + '@sveltejs/acorn-typescript@1.0.13(acorn@8.18.0)': + dependencies: + acorn: 8.18.0 + + '@sveltejs/load-config@0.2.3': {} + + '@sveltejs/vite-plugin-svelte@7.3.0(svelte@5.57.0)(vite@8.2.2(@types/node@26.4.1)(jiti@2.7.0))': + dependencies: + deepmerge: 4.3.1 + magic-string: 1.2.3 + obug: 2.1.4 + svelte: 5.57.0 + vite: 8.2.2(@types/node@26.4.1)(jiti@2.7.0) + vitefu: 1.1.3(vite@8.2.2(@types/node@26.4.1)(jiti@2.7.0)) + + '@tailwindcss/node@4.3.3': + dependencies: + '@jridgewell/remapping': 2.3.5 + enhanced-resolve: 5.24.5 + jiti: 2.7.0 + lightningcss: 1.32.0 + magic-string: 0.30.21 + source-map-js: 1.2.1 + tailwindcss: 4.3.3 + + '@tailwindcss/oxide-android-arm64@4.3.3': + optional: true + + '@tailwindcss/oxide-darwin-arm64@4.3.3': + optional: true + + '@tailwindcss/oxide-darwin-x64@4.3.3': + optional: true + + '@tailwindcss/oxide-freebsd-x64@4.3.3': + optional: true + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.3': + optional: true + + '@tailwindcss/oxide-linux-arm64-gnu@4.3.3': + optional: true + + '@tailwindcss/oxide-linux-arm64-musl@4.3.3': + optional: true + + '@tailwindcss/oxide-linux-x64-gnu@4.3.3': + optional: true + + '@tailwindcss/oxide-linux-x64-musl@4.3.3': + optional: true + + '@tailwindcss/oxide-wasm32-wasi@4.3.3': + optional: true + + '@tailwindcss/oxide-win32-arm64-msvc@4.3.3': + optional: true + + '@tailwindcss/oxide-win32-x64-msvc@4.3.3': + optional: true + + '@tailwindcss/oxide@4.3.3': + optionalDependencies: + '@tailwindcss/oxide-android-arm64': 4.3.3 + '@tailwindcss/oxide-darwin-arm64': 4.3.3 + '@tailwindcss/oxide-darwin-x64': 4.3.3 + '@tailwindcss/oxide-freebsd-x64': 4.3.3 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.3.3 + '@tailwindcss/oxide-linux-arm64-gnu': 4.3.3 + '@tailwindcss/oxide-linux-arm64-musl': 4.3.3 + '@tailwindcss/oxide-linux-x64-gnu': 4.3.3 + '@tailwindcss/oxide-linux-x64-musl': 4.3.3 + '@tailwindcss/oxide-wasm32-wasi': 4.3.3 + '@tailwindcss/oxide-win32-arm64-msvc': 4.3.3 + '@tailwindcss/oxide-win32-x64-msvc': 4.3.3 + + '@tailwindcss/vite@4.3.3(vite@8.2.2(@types/node@26.4.1)(jiti@2.7.0))': + dependencies: + '@tailwindcss/node': 4.3.3 + '@tailwindcss/oxide': 4.3.3 + tailwindcss: 4.3.3 + vite: 8.2.2(@types/node@26.4.1)(jiti@2.7.0) + + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + + '@types/deep-eql@4.0.2': {} + + '@types/estree@1.0.9': {} + + '@types/node@26.4.1': + dependencies: + undici-types: 8.3.0 + + '@vitest/browser-playwright@4.1.11(playwright@1.62.1)(vite@8.2.2(@types/node@26.4.1)(jiti@2.7.0))(vitest@4.1.11)': + dependencies: + '@vitest/browser': 4.1.11(vite@8.2.2(@types/node@26.4.1)(jiti@2.7.0))(vitest@4.1.11) + '@vitest/mocker': 4.1.11(vite@8.2.2(@types/node@26.4.1)(jiti@2.7.0)) + playwright: 1.62.1 + tinyrainbow: 3.1.1 + vitest: 4.1.11(@types/node@26.4.1)(@vitest/browser-playwright@4.1.11)(vite@8.2.2(@types/node@26.4.1)(jiti@2.7.0)) + transitivePeerDependencies: + - bufferutil + - msw + - utf-8-validate + - vite + + '@vitest/browser@4.1.11(vite@8.2.2(@types/node@26.4.1)(jiti@2.7.0))(vitest@4.1.11)': + dependencies: + '@blazediff/core': 1.9.1 + '@vitest/mocker': 4.1.11(vite@8.2.2(@types/node@26.4.1)(jiti@2.7.0)) + '@vitest/utils': 4.1.11 + magic-string: 0.30.21 + pngjs: 7.0.0 + sirv: 3.0.2 + tinyrainbow: 3.1.1 + vitest: 4.1.11(@types/node@26.4.1)(@vitest/browser-playwright@4.1.11)(vite@8.2.2(@types/node@26.4.1)(jiti@2.7.0)) + ws: 8.21.3 + transitivePeerDependencies: + - bufferutil + - msw + - utf-8-validate + - vite + + '@vitest/expect@4.1.11': + dependencies: + '@standard-schema/spec': 1.1.0 + '@types/chai': 5.2.3 + '@vitest/spy': 4.1.11 + '@vitest/utils': 4.1.11 + chai: 6.2.2 + tinyrainbow: 3.1.1 + + '@vitest/mocker@4.1.11(vite@8.2.2(@types/node@26.4.1)(jiti@2.7.0))': + dependencies: + '@vitest/spy': 4.1.11 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 8.2.2(@types/node@26.4.1)(jiti@2.7.0) + + '@vitest/pretty-format@4.1.11': + dependencies: + tinyrainbow: 3.1.1 + + '@vitest/runner@4.1.11': + dependencies: + '@vitest/utils': 4.1.11 + pathe: 2.0.3 + + '@vitest/snapshot@4.1.11': + dependencies: + '@vitest/pretty-format': 4.1.11 + '@vitest/utils': 4.1.11 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@4.1.11': {} + + '@vitest/utils@4.1.11': + dependencies: + '@vitest/pretty-format': 4.1.11 + convert-source-map: 2.0.0 + tinyrainbow: 3.1.1 + + acorn@8.18.0: {} + + aria-query@5.3.1: {} + + assertion-error@2.0.1: {} + + axobject-query@4.1.0: {} + + base-x@5.0.1: {} + + bs58@6.0.0: + dependencies: + base-x: 5.0.1 + + bs58check@4.0.0: + dependencies: + '@noble/hashes': 1.8.0 + bs58: 6.0.0 + + cbor-extract@2.2.2: + dependencies: + node-gyp-build-optional-packages: 5.1.1 + optionalDependencies: + '@cbor-extract/cbor-extract-darwin-arm64': 2.2.2 + '@cbor-extract/cbor-extract-darwin-x64': 2.2.2 + '@cbor-extract/cbor-extract-linux-arm': 2.2.2 + '@cbor-extract/cbor-extract-linux-arm64': 2.2.2 + '@cbor-extract/cbor-extract-linux-x64': 2.2.2 + '@cbor-extract/cbor-extract-win32-x64': 2.2.2 + optional: true + + cbor-x@1.6.6: + optionalDependencies: + cbor-extract: 2.2.2 + + chai@6.2.2: {} + + chokidar@4.0.3: + dependencies: + readdirp: 4.1.2 + + clsx@2.1.1: {} + + codemirror@6.0.2: + dependencies: + '@codemirror/autocomplete': 6.20.3 + '@codemirror/commands': 6.11.0 + '@codemirror/language': 6.12.4 + '@codemirror/lint': 6.9.7 + '@codemirror/search': 6.7.2 + '@codemirror/state': 6.7.2 + '@codemirror/view': 6.43.11 + + convert-source-map@2.0.0: {} + + crelt@1.0.7: {} + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + deepmerge@4.3.1: {} + + detect-libc@2.1.2: {} + + devalue@5.9.2: {} + + enhanced-resolve@5.24.5: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.3.3 + + es-module-lexer@2.3.2: {} + + esm-env@1.2.2: {} + + esrap@2.3.6: + dependencies: + '@jridgewell/sourcemap-codec': 1.6.0 + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.9 + + eventemitter3@5.0.4: {} + + expect-type@1.4.0: {} + + fast-sha256@1.3.0: {} + + fdir@6.5.0(picomatch@4.0.7): + optionalDependencies: + picomatch: 4.0.7 + + fsevents@2.3.2: + optional: true + + fsevents@2.3.3: + optional: true + + graceful-fs@4.2.11: {} + + inline-style-parser@0.2.7: {} + + is-reference@3.0.3: + dependencies: + '@types/estree': 1.0.9 + + isomorphic-ws@5.0.0(ws@8.21.3): + dependencies: + ws: 8.21.3 + + jiti@2.7.0: {} + + json-formatter-js@2.5.23: {} + + lightningcss-android-arm64@1.32.0: + optional: true + + lightningcss-android-arm64@1.33.0: + optional: true + + lightningcss-darwin-arm64@1.32.0: + optional: true + + lightningcss-darwin-arm64@1.33.0: + optional: true + + lightningcss-darwin-x64@1.32.0: + optional: true + + lightningcss-darwin-x64@1.33.0: + optional: true + + lightningcss-freebsd-x64@1.32.0: + optional: true + + lightningcss-freebsd-x64@1.33.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.32.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.33.0: + optional: true + + lightningcss-linux-arm64-gnu@1.32.0: + optional: true + + lightningcss-linux-arm64-gnu@1.33.0: + optional: true + + lightningcss-linux-arm64-musl@1.32.0: + optional: true + + lightningcss-linux-arm64-musl@1.33.0: + optional: true + + lightningcss-linux-x64-gnu@1.32.0: + optional: true + + lightningcss-linux-x64-gnu@1.33.0: + optional: true + + lightningcss-linux-x64-musl@1.32.0: + optional: true + + lightningcss-linux-x64-musl@1.33.0: + optional: true + + lightningcss-win32-arm64-msvc@1.32.0: + optional: true + + lightningcss-win32-arm64-msvc@1.33.0: + optional: true + + lightningcss-win32-x64-msvc@1.32.0: + optional: true + + lightningcss-win32-x64-msvc@1.33.0: + optional: true + + lightningcss@1.32.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.32.0 + lightningcss-darwin-arm64: 1.32.0 + lightningcss-darwin-x64: 1.32.0 + lightningcss-freebsd-x64: 1.32.0 + lightningcss-linux-arm-gnueabihf: 1.32.0 + lightningcss-linux-arm64-gnu: 1.32.0 + lightningcss-linux-arm64-musl: 1.32.0 + lightningcss-linux-x64-gnu: 1.32.0 + lightningcss-linux-x64-musl: 1.32.0 + lightningcss-win32-arm64-msvc: 1.32.0 + lightningcss-win32-x64-msvc: 1.32.0 + + lightningcss@1.33.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.33.0 + lightningcss-darwin-arm64: 1.33.0 + lightningcss-darwin-x64: 1.33.0 + lightningcss-freebsd-x64: 1.33.0 + lightningcss-linux-arm-gnueabihf: 1.33.0 + lightningcss-linux-arm64-gnu: 1.33.0 + lightningcss-linux-arm64-musl: 1.33.0 + lightningcss-linux-x64-gnu: 1.33.0 + lightningcss-linux-x64-musl: 1.33.0 + lightningcss-win32-arm64-msvc: 1.33.0 + lightningcss-win32-x64-msvc: 1.33.0 + + locate-character@3.0.0: {} + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.6.0 + + magic-string@1.2.3: + dependencies: + '@jridgewell/sourcemap-codec': 1.6.0 + + mri@1.2.0: {} + + mrmime@2.0.1: {} + + ms@2.1.3: {} + + nanoid@3.3.18: {} + + node-gyp-build-optional-packages@5.1.1: + dependencies: + detect-libc: 2.1.2 + optional: true + + obug@2.1.4: {} + + paneforge@1.0.2(svelte@5.57.0): + dependencies: + runed: 0.23.4(svelte@5.57.0) + svelte: 5.57.0 + svelte-toolbelt: 0.9.3(svelte@5.57.0) + + pathe@2.0.3: {} + + picocolors@1.1.1: {} + + picomatch@4.0.7: {} + + playwright-core@1.62.1: {} + + playwright@1.62.1: + dependencies: + playwright-core: 1.62.1 + optionalDependencies: + fsevents: 2.3.2 + + pngjs@7.0.0: {} + + postcss@8.5.28: + dependencies: + nanoid: 3.3.18 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + readdirp@4.1.2: {} + + rolldown@1.2.7: + dependencies: + '@oxc-project/types': 0.148.0 + '@rolldown/pluginutils': 1.0.1 + optionalDependencies: + '@rolldown/binding-android-arm-eabi': 1.2.7 + '@rolldown/binding-android-arm64': 1.2.7 + '@rolldown/binding-darwin-arm64': 1.2.7 + '@rolldown/binding-darwin-x64': 1.2.7 + '@rolldown/binding-freebsd-x64': 1.2.7 + '@rolldown/binding-linux-arm-gnueabihf': 1.2.7 + '@rolldown/binding-linux-arm64-gnu': 1.2.7 + '@rolldown/binding-linux-arm64-musl': 1.2.7 + '@rolldown/binding-linux-ppc64-gnu': 1.2.7 + '@rolldown/binding-linux-s390x-gnu': 1.2.7 + '@rolldown/binding-linux-x64-gnu': 1.2.7 + '@rolldown/binding-linux-x64-musl': 1.2.7 + '@rolldown/binding-openharmony-arm64': 1.2.7 + '@rolldown/binding-win32-arm64-msvc': 1.2.7 + '@rolldown/binding-win32-x64-msvc': 1.2.7 + + runed@0.23.4(svelte@5.57.0): + dependencies: + esm-env: 1.2.2 + svelte: 5.57.0 + + runed@0.29.2(svelte@5.57.0): + dependencies: + esm-env: 1.2.2 + svelte: 5.57.0 + + sade@1.8.1: + dependencies: + mri: 1.2.0 + + siginfo@2.0.0: {} + + sirv@3.0.2: + dependencies: + '@polka/url': 1.0.0-next.29 + mrmime: 2.0.1 + totalist: 3.0.1 + + source-map-js@1.2.1: {} + + stackback@0.0.2: {} + + std-env@4.2.0: {} + + style-mod@4.1.3: {} + + style-to-object@1.0.14: + dependencies: + inline-style-parser: 0.2.7 + + svelte-check@4.7.6(picomatch@4.0.7)(svelte@5.57.0)(typescript@6.0.3): + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + '@sveltejs/load-config': 0.2.3 + chokidar: 4.0.3 + fdir: 6.5.0(picomatch@4.0.7) + picocolors: 1.1.1 + sade: 1.8.1 + svelte: 5.57.0 + typescript: 6.0.3 + transitivePeerDependencies: + - picomatch + + svelte-toolbelt@0.9.3(svelte@5.57.0): + dependencies: + clsx: 2.1.1 + runed: 0.29.2(svelte@5.57.0) + style-to-object: 1.0.14 + svelte: 5.57.0 + + svelte@5.57.0: + dependencies: + '@jridgewell/remapping': 2.3.5 + '@jridgewell/sourcemap-codec': 1.6.0 + '@sveltejs/acorn-typescript': 1.0.13(acorn@8.18.0) + '@types/estree': 1.0.9 + acorn: 8.18.0 + aria-query: 5.3.1 + axobject-query: 4.1.0 + clsx: 2.1.1 + devalue: 5.9.2 + esm-env: 1.2.2 + esrap: 2.3.6 + is-reference: 3.0.3 + locate-character: 3.0.0 + magic-string: 0.30.21 + zimmerframe: 1.1.5 + transitivePeerDependencies: + - '@typescript-eslint/types' + + tailwindcss@4.3.3: {} + + tapable@2.3.3: {} + + tinybench@2.9.0: {} + + tinyexec@1.3.1: {} + + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.7) + picomatch: 4.0.7 + + tinyrainbow@3.1.1: {} + + totalist@3.0.1: {} + + typescript@6.0.3: {} + + undici-types@8.3.0: {} + + uuid@14.0.2: {} + + vite-plugin-wasm@3.6.0(vite@8.2.2(@types/node@26.4.1)(jiti@2.7.0)): + dependencies: + vite: 8.2.2(@types/node@26.4.1)(jiti@2.7.0) + + vite@8.2.2(@types/node@26.4.1)(jiti@2.7.0): + dependencies: + lightningcss: 1.33.0 + picomatch: 4.0.7 + postcss: 8.5.28 + rolldown: 1.2.7 + tinyglobby: 0.2.17 + optionalDependencies: + '@types/node': 26.4.1 + fsevents: 2.3.3 + jiti: 2.7.0 + + vitefu@1.1.3(vite@8.2.2(@types/node@26.4.1)(jiti@2.7.0)): + optionalDependencies: + vite: 8.2.2(@types/node@26.4.1)(jiti@2.7.0) + + vitest@4.1.11(@types/node@26.4.1)(@vitest/browser-playwright@4.1.11)(vite@8.2.2(@types/node@26.4.1)(jiti@2.7.0)): + dependencies: + '@vitest/expect': 4.1.11 + '@vitest/mocker': 4.1.11(vite@8.2.2(@types/node@26.4.1)(jiti@2.7.0)) + '@vitest/pretty-format': 4.1.11 + '@vitest/runner': 4.1.11 + '@vitest/snapshot': 4.1.11 + '@vitest/spy': 4.1.11 + '@vitest/utils': 4.1.11 + es-module-lexer: 2.3.2 + expect-type: 1.4.0 + magic-string: 0.30.21 + obug: 2.1.4 + pathe: 2.0.3 + picomatch: 4.0.7 + std-env: 4.2.0 + tinybench: 2.9.0 + tinyexec: 1.3.1 + tinyglobby: 0.2.17 + tinyrainbow: 3.1.1 + vite: 8.2.2(@types/node@26.4.1)(jiti@2.7.0) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 26.4.1 + '@vitest/browser-playwright': 4.1.11(playwright@1.62.1)(vite@8.2.2(@types/node@26.4.1)(jiti@2.7.0))(vitest@4.1.11) + transitivePeerDependencies: + - msw + + w3c-keyname@2.2.8: {} + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + + ws@8.21.3: {} + + xstate@5.32.6: {} + + zimmerframe@1.1.5: {} diff --git a/examples/lab/pnpm-workspace.yaml b/examples/lab/pnpm-workspace.yaml new file mode 100644 index 00000000..8f558344 --- /dev/null +++ b/examples/lab/pnpm-workspace.yaml @@ -0,0 +1,7 @@ +allowBuilds: + cbor-extract: false + esbuild: true + "@automerge/automerge-repo@https://codeload.github.com/onsetsoftware/automerge-repo/tar.gz/b39d94c5624a3497150e08ba18546f29222289d8#path:packages/automerge-repo": true +overrides: + "@automerge/automerge": 3.3.2 + "@automerge/automerge-subduction": 0.16.1 diff --git a/examples/lab/scripts/check-static-build.mjs b/examples/lab/scripts/check-static-build.mjs new file mode 100644 index 00000000..a89f5b98 --- /dev/null +++ b/examples/lab/scripts/check-static-build.mjs @@ -0,0 +1,26 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import assert from "node:assert/strict" +import { readFile } from "node:fs/promises" + +const outputDirectory = new URL("../../../_build/web/lab-app/", import.meta.url) +const index = await readFile(new URL("index.html", outputDirectory), "utf8") +const configuredBase = process.env.VITE_BASE || "/" +const base = configuredBase.endsWith("/") ? configuredBase : `${configuredBase}/` + +assert.match(index, new RegExp(`(?:src|href)=["']${escapeRegex(base)}assets/`)) + +await Promise.all( + ["compiler", "editor", "sync"].map(async (route) => { + const routeIndex = await readFile( + new URL(`${route}/index.html`, outputDirectory), + "utf8", + ) + assert.equal(routeIndex, index, `${route}/index.html must match index.html`) + }), +) + +function escapeRegex(value) { + return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") +} diff --git a/examples/lab/scripts/server.mjs b/examples/lab/scripts/server.mjs new file mode 100644 index 00000000..77ed6890 --- /dev/null +++ b/examples/lab/scripts/server.mjs @@ -0,0 +1,161 @@ +#!/usr/bin/env node +// SPDX-FileCopyrightText: 2026 Coln contributors +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import { WebSocketServer } from "ws" +import { + MemorySigner, + MemoryStorage, + Subduction, +} from "@automerge/automerge-subduction" + +class WsTransport { + #ws + #queue = [] + #waiters = [] + #errorWaiters = [] + #closed = false + #closedPromise + #resolveClosed + #disconnectCallback = null + + constructor(ws) { + this.#ws = ws + this.#closedPromise = new Promise(resolve => { + this.#resolveClosed = resolve + }) + + ws.on("message", data => { + const bytes = toUint8Array(data) + const waiter = this.#waiters.shift() + if (waiter) { + this.#errorWaiters.shift() + waiter(bytes) + } else { + this.#queue.push(bytes) + } + }) + + ws.on("close", () => this.#markClosed(new Error("WebSocket closed"))) + ws.on("error", err => this.#markClosed(err instanceof Error ? err : new Error(String(err)))) + } + + onDisconnect(callback) { + this.#disconnectCallback = callback + } + + async sendBytes(bytes) { + if (this.#closed) throw new Error("WebSocket closed") + await new Promise((resolve, reject) => { + this.#ws.send(Buffer.from(bytes), err => (err ? reject(err) : resolve())) + }) + } + + recvBytes() { + const queued = this.#queue.shift() + if (queued) return Promise.resolve(queued) + if (this.#closed) return Promise.reject(new Error("WebSocket closed")) + return new Promise((resolve, reject) => { + this.#waiters.push(resolve) + this.#errorWaiters.push(reject) + }) + } + + async disconnect() { + this.#markClosed(new Error("WebSocket closed"), false) + this.#ws.close() + } + + closed() { + return this.#closedPromise + } + + #markClosed(err, notify = true) { + if (this.#closed) return + this.#closed = true + this.#resolveClosed() + for (const reject of this.#errorWaiters) reject(err) + this.#waiters = [] + this.#errorWaiters = [] + if (notify) this.#disconnectCallback?.() + } +} + +function toUint8Array(data) { + if (data instanceof Uint8Array) { + return new Uint8Array(data.buffer, data.byteOffset, data.byteLength) + } + if (data instanceof ArrayBuffer) return new Uint8Array(data) + if (Array.isArray(data)) return toUint8Array(Buffer.concat(data)) + return new Uint8Array(Buffer.from(data)) +} + +const port = Number(process.env.PORT ?? 3030) +const host = "127.0.0.1" +const serviceName = process.env.SUBDUCTION_SERVICE_NAME ?? `${host}:${port}` +const subduction = new Subduction({ + signer: new MemorySigner(), + storage: new MemoryStorage(), +}) +const wss = new WebSocketServer({ host, port }) +const leases = new Set() +const clients = new Set() +const managed = process.env.COLN_SYNC_MANAGED === "1" +let shutdownTimer +let shuttingDown = false + +wss.on("connection", (ws, request) => { + if (shuttingDown) { + ws.close() + return + } + if (request.url === "/.coln-sync/lease") { + clearTimeout(shutdownTimer) + leases.add(ws) + ws.send(JSON.stringify({ protocol: "coln-sync-relay", pid: process.pid })) + ws.once("close", () => { + leases.delete(ws) + scheduleShutdown() + }) + return + } + + const transport = new WsTransport(ws) + clients.add(ws) + ws.once("close", () => clients.delete(ws)) + subduction.acceptTransport(transport, serviceName).catch(err => { + if (!String(err?.message ?? err).includes("closed")) console.error(err) + }) +}) + +function scheduleShutdown() { + if (!managed || leases.size > 0 || shuttingDown) return + clearTimeout(shutdownTimer) + shutdownTimer = setTimeout(shutdown, 500) +} + +async function shutdown() { + if (shuttingDown) return + shuttingDown = true + clearTimeout(shutdownTimer) + const closed = new Promise(resolve => wss.close(resolve)) + for (const lease of leases) lease.close() + for (const client of clients) client.close() + await subduction.disconnectAll().catch(() => {}) + await closed + process.exit(0) +} + +process.on("SIGINT", shutdown) +process.on("SIGTERM", shutdown) + +wss.on("listening", () => { + console.log(`Subduction relay listening on ws://${host}:${port}`) + if (managed) shutdownTimer = setTimeout(shutdown, 10_000) +}) + +wss.on("error", error => { + if (error.code !== "EADDRINUSE") console.error(error) + process.exit(1) +}) diff --git a/examples/lab/scripts/with-server.mjs b/examples/lab/scripts/with-server.mjs new file mode 100644 index 00000000..d149aa4a --- /dev/null +++ b/examples/lab/scripts/with-server.mjs @@ -0,0 +1,109 @@ +#!/usr/bin/env node +// SPDX-FileCopyrightText: 2026 Coln contributors +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import { spawn } from "node:child_process" +import { fileURLToPath } from "node:url" +import WebSocket from "ws" + +const [, , command, ...args] = process.argv + +if (!command) { + console.error("Usage: coln-with-sync-server [args...]") + process.exit(1) +} + +const host = "127.0.0.1" +const port = Number(process.env.SUBDUCTION_PORT ?? 3030) +const endpoint = `ws://${host}:${port}` +const leaseUrl = `${endpoint}/.coln-sync/lease` +const serverPath = fileURLToPath(new URL("server.mjs", import.meta.url)) + +let lease = await acquireLease() +if (!lease) { + const server = spawn(process.execPath, [serverPath], { + detached: true, + env: { + ...process.env, + COLN_SYNC_MANAGED: "1", + PORT: String(port), + SUBDUCTION_SERVICE_NAME: `${host}:${port}`, + }, + stdio: "ignore", + }) + server.unref() + lease = await waitForLease() +} + +if (!lease) { + console.error(`Could not start the local Subduction relay at ${endpoint}`) + process.exit(1) +} + +const child = spawn(command, args, { + env: { + ...process.env, + SUBDUCTION_ENDPOINT: endpoint, + VITE_SUBDUCTION_ENDPOINT: endpoint, + }, + stdio: "inherit", +}) + +for (const signal of ["SIGINT", "SIGTERM"]) { + process.once(signal, () => child.kill(signal)) +} + +const exitCode = await new Promise(resolve => { + child.once("error", error => { + console.error(error.message) + resolve(1) + }) + child.once("exit", (code, signal) => { + resolve(code ?? (signal === "SIGINT" ? 130 : 143)) + }) +}) + +lease.close() +await Promise.race([ + new Promise(resolve => lease.once("close", resolve)), + new Promise(resolve => setTimeout(resolve, 250)), +]) +process.exitCode = exitCode + +async function waitForLease() { + const deadline = Date.now() + 10_000 + while (Date.now() < deadline) { + const lease = await acquireLease() + if (lease) return lease + await new Promise(resolve => setTimeout(resolve, 50)) + } + return null +} + +function acquireLease() { + return new Promise(resolve => { + const socket = new WebSocket(leaseUrl) + const timeout = setTimeout(() => finish(null), 500) + let settled = false + + const finish = result => { + if (settled) return + settled = true + clearTimeout(timeout) + if (!result) socket.close() + resolve(result) + } + + socket.once("message", data => { + try { + const message = JSON.parse(String(data)) + finish(message.protocol === "coln-sync-relay" ? socket : null) + } catch { + finish(null) + } + }) + socket.once("error", () => finish(null)) + socket.once("close", () => finish(null)) + }) +} diff --git a/examples/lab/src/app/Home.svelte b/examples/lab/src/app/Home.svelte new file mode 100644 index 00000000..c980cd1e --- /dev/null +++ b/examples/lab/src/app/Home.svelte @@ -0,0 +1,74 @@ + + + + + +
+
+
+
+

Coln Lab

+

Design theories. Inspect stores. Explore Coln.

+

Coln is a data-oriented proof assistant that treats proofs as structured, inspectable data.

+
+ +
+ +
+
+

Workbench

+ Define / Inspect +
+ +
+ +
+
+

Demos

+ Explore +
+ router.follow(event, graphDemo.tool)}> +
+ {graphDemo.number} + Demos / {graphDemo.name} +
+
+

{graphDemo.name}

+

{graphDemo.description}

+
+
+ {graphDemo.action}→ +
+
+
+
+
diff --git a/examples/lab/src/app/LabApp.svelte b/examples/lab/src/app/LabApp.svelte new file mode 100644 index 00000000..e8255ab5 --- /dev/null +++ b/examples/lab/src/app/LabApp.svelte @@ -0,0 +1,197 @@ + + + + + +{pageTitle} + +
+
+ + +
+ +
+ {#if route.tool === "home"} + + {:else if route.tool === "compiler"} + {#await loadCompilerTool()} +
OPENING THEORY EDITOR
+ {:then CompilerTool} + {#key `compiler:${route.documentUrl}`} + + {/key} + {/await} + {:else if route.tool === "editor"} + {#await loadEditorTool()} +
OPENING STORE EDITOR
+ {:then EditorTool} + {#key `editor:${route.documentUrl}`} + recentStoreUrl = url} /> + {/key} + {/await} + {:else if route.tool === "sync"} + {#await loadGraphTool()} +
OPENING GRAPH DEMO
+ {:then GraphTool} + {#key `sync:${route.documentUrl}`} + + {/key} + {/await} + {:else} +

PAGE NOT FOUND

No Coln Lab page exists at this address.

router.follow(event, "home")}>Return to Coln Lab
+ {/if} +
+
diff --git a/examples/lab/src/app/components/DocumentLoadError.svelte b/examples/lab/src/app/components/DocumentLoadError.svelte new file mode 100644 index 00000000..21584905 --- /dev/null +++ b/examples/lab/src/app/components/DocumentLoadError.svelte @@ -0,0 +1,30 @@ + + + + + +
+
+
+

{label}

+

{heading}

+

{detail}

+
+ {#if documentUrl} + {documentUrl} + {/if} + +
+
diff --git a/examples/lab/src/app/components/FeedbackNotice.svelte b/examples/lab/src/app/components/FeedbackNotice.svelte new file mode 100644 index 00000000..def7f8fb --- /dev/null +++ b/examples/lab/src/app/components/FeedbackNotice.svelte @@ -0,0 +1,26 @@ + + + + + + + + diff --git a/examples/lab/src/app/components/SyncStatus.svelte b/examples/lab/src/app/components/SyncStatus.svelte new file mode 100644 index 00000000..761a85ca --- /dev/null +++ b/examples/lab/src/app/components/SyncStatus.svelte @@ -0,0 +1,36 @@ + + + + + +
+ {label}{#if detail}/ {detail}{/if} +
+ + diff --git a/examples/lab/src/lib/CodeMirrorEditor.svelte b/examples/lab/src/lib/CodeMirrorEditor.svelte new file mode 100644 index 00000000..551b7544 --- /dev/null +++ b/examples/lab/src/lib/CodeMirrorEditor.svelte @@ -0,0 +1,95 @@ + + + + + +
diff --git a/examples/lab/src/lib/LabPaneResizer.svelte b/examples/lab/src/lib/LabPaneResizer.svelte new file mode 100644 index 00000000..e27c633a --- /dev/null +++ b/examples/lab/src/lib/LabPaneResizer.svelte @@ -0,0 +1,22 @@ + + + + + + + + diff --git a/examples/lab/src/lib/codemirror-theme.ts b/examples/lab/src/lib/codemirror-theme.ts new file mode 100644 index 00000000..77f78b0f --- /dev/null +++ b/examples/lab/src/lib/codemirror-theme.ts @@ -0,0 +1,73 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import { EditorView } from "@codemirror/view" + +export const labEditorTheme = EditorView.theme( + { + "&": { + height: "100%", + backgroundColor: "#0b1112", + color: "#e8ece8", + fontFamily: "'DM Mono', monospace", + fontSize: "14px", + }, + "&.cm-focused": { + outline: "1px solid #d8ff57", + outlineOffset: "-1px", + }, + ".cm-scroller": { + fontFamily: "inherit", + lineHeight: "1.75", + overflow: "auto", + }, + ".cm-content": { caretColor: "#d8ff57", padding: "20px 0" }, + ".cm-line": { padding: "0 20px 0 12px" }, + ".cm-cursor, .cm-dropCursor": { borderLeftColor: "#d8ff57" }, + ".cm-selectionBackground": { backgroundColor: "#657a32" }, + "&.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground": { + backgroundColor: "#657a32", + }, + ".cm-content ::selection, .cm-content::selection": { + backgroundColor: "#657a32", + color: "#e8ece8", + }, + ".cm-selectionMatch": { backgroundColor: "transparent", outline: "none" }, + ".cm-content .cm-activeLine": { + backgroundColor: "transparent", + position: "relative", + }, + ".cm-content .cm-activeLine::before": { + backgroundColor: "#111a1b", + content: "''", + inset: "0", + pointerEvents: "none", + position: "absolute", + zIndex: "-3", + }, + ".cm-gutters": { + backgroundColor: "#0b1112", + borderRight: "1px solid #253233", + color: "#536163", + }, + ".cm-activeLineGutter": { + backgroundColor: "#111a1b", + color: "#91a0a1", + }, + ".cm-foldPlaceholder, .cm-tooltip, .cm-panels": { + backgroundColor: "#182122", + borderColor: "#304041", + color: "#e8ece8", + }, + ".cm-panels.cm-panels-top": { borderBottom: "1px solid #304041" }, + ".cm-searchMatch": { + backgroundColor: "#7f8f3f66", + outline: "1px solid #d8ff57", + }, + ".cm-searchMatch.cm-searchMatch-selected": { + backgroundColor: "#d8ff5744", + }, + ".cm-tooltip": { border: "1px solid #304041" }, + }, + { dark: true }, +) diff --git a/examples/lab/src/lib/document-sync.svelte.ts b/examples/lab/src/lib/document-sync.svelte.ts new file mode 100644 index 00000000..0c27d452 --- /dev/null +++ b/examples/lab/src/lib/document-sync.svelte.ts @@ -0,0 +1,93 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import type { DocumentId, Repo } from "@automerge/automerge-repo" +import { createSubscriber } from "svelte/reactivity" + +type FlushableHandle = { + documentId: DocumentId + on(event: "change", listener: () => void): void + off(event: "change", listener: () => void): void +} + +export type SyncStatus = "offline" | "syncing" | "synced" | "error" + +export class DocumentSync { + readonly #subscribe: () => void + #status: SyncStatus + #error: unknown + + constructor( + private readonly repo: Repo, + private readonly handle: FlushableHandle, + ) { + this.#status = repo.isSubductionConnected() ? "syncing" : "offline" + this.#subscribe = createSubscriber((update) => { + let active = true + let generation = 0 + let currentAttempt: symbol | undefined + + const setStatus = (status: SyncStatus, error?: unknown) => { + if (this.#status === status && this.#error === error) return + this.#status = status + this.#error = error + update() + } + const finishFlush = ( + attempt: symbol, + flushGeneration: number, + failed: boolean, + error?: unknown, + ) => { + if (!active || currentAttempt !== attempt) return + currentAttempt = undefined + if (!this.repo.isSubductionConnected()) setStatus("offline") + else if (flushGeneration !== generation) startFlush() + else if (failed) setStatus("error", error) + else setStatus("synced") + } + const startFlush = () => { + if (currentAttempt || !active || !this.repo.isSubductionConnected()) + return + const attempt = Symbol() + const flushGeneration = generation + currentAttempt = attempt + void this.repo.flush([this.handle.documentId]).then( + () => finishFlush(attempt, flushGeneration, false), + (error) => finishFlush(attempt, flushGeneration, true, error), + ) + } + const requestFlush = () => { + generation += 1 + if (!this.repo.isSubductionConnected()) { + currentAttempt = undefined + setStatus("offline") + return + } + setStatus("syncing") + startFlush() + } + + this.handle.on("change", requestFlush) + this.repo.on("subduction-connection", requestFlush) + requestFlush() + return () => { + active = false + generation += 1 + currentAttempt = undefined + this.handle.off("change", requestFlush) + this.repo.off("subduction-connection", requestFlush) + } + }) + } + + get status(): SyncStatus { + this.#subscribe() + return this.#status + } + + get error(): unknown { + this.#subscribe() + return this.#error + } +} diff --git a/examples/lab/src/lib/router.svelte.ts b/examples/lab/src/lib/router.svelte.ts new file mode 100644 index 00000000..26b33a67 --- /dev/null +++ b/examples/lab/src/lib/router.svelte.ts @@ -0,0 +1,123 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import { createSubscriber } from "svelte/reactivity" + +export type Tool = "home" | "compiler" | "editor" | "sync" | "not-found" + +export interface Route { + tool: Tool + documentUrl: string + theoryUrl: string + pathname: string +} + +const paths: Record, string> = { + home: "", + compiler: "compiler/", + editor: "editor/", + sync: "sync/", +} + +export class Router { + readonly #subscribe: () => void + + constructor() { + this.#subscribe = createSubscriber((update) => { + addEventListener("popstate", update) + addEventListener("hashchange", update) + return () => { + removeEventListener("popstate", update) + removeEventListener("hashchange", update) + } + }) + } + + get current(): Route { + this.#subscribe() + return routeFromUrl(new URL(location.href), import.meta.env.BASE_URL) + } + + href( + tool: Exclude, + documentUrl = "", + theoryUrl = "", + ): string { + const url = new URL(paths[tool], appBaseUrl()) + if (theoryUrl) url.searchParams.set("theory", theoryUrl) + url.hash = documentUrl + return `${url.pathname}${url.search}${url.hash}` + } + + navigate( + tool: Exclude, + documentUrl = "", + theoryUrl = "", + ): void { + history.pushState(null, "", this.href(tool, documentUrl, theoryUrl)) + dispatchEvent(new PopStateEvent("popstate")) + } + + follow( + event: MouseEvent, + tool: Exclude, + documentUrl = "", + theoryUrl = "", + ): void { + if ( + event.defaultPrevented || + event.button !== 0 || + event.metaKey || + event.ctrlKey || + event.shiftKey || + event.altKey + ) + return + event.preventDefault() + this.navigate(tool, documentUrl, theoryUrl) + } + + replace( + tool: Exclude, + documentUrl = "", + theoryUrl = "", + ): void { + history.replaceState(null, "", this.href(tool, documentUrl, theoryUrl)) + dispatchEvent(new PopStateEvent("popstate")) + } +} + +export function readRoute(): Route { + return routeFromUrl(new URL(location.href), import.meta.env.BASE_URL) +} + +export function routeFromUrl(url: URL, basePath = "/"): Route { + const base = new URL(basePath, url.origin).pathname + const relativePath = url.pathname.startsWith(base) + ? url.pathname.slice(base.length) + : url.pathname.slice(1) + const segments = relativePath + .replace(/^\/+|\/+$/g, "") + .split("/") + .filter(Boolean) + const segment = segments[0] ?? "" + const tool: Tool = + segments.length > 1 + ? "not-found" + : segment === "" + ? "home" + : segment === "compiler" || segment === "editor" || segment === "sync" + ? segment + : "not-found" + const theoryUrl = url.searchParams.get("theory") ?? "" + return { + tool, + documentUrl: url.hash.slice(1), + theoryUrl, + pathname: url.pathname, + } +} + +function appBaseUrl(): URL { + return new URL(import.meta.env.BASE_URL, location.origin) +} diff --git a/examples/lab/src/main.ts b/examples/lab/src/main.ts new file mode 100644 index 00000000..cd00f59f --- /dev/null +++ b/examples/lab/src/main.ts @@ -0,0 +1,43 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import { Repo, type DocumentId } from "@automerge/automerge-repo" +import { IndexedDBStorageAdapter } from "@automerge/automerge-repo-storage-indexeddb" +// @ts-expect-error initSync is exported at runtime but absent from declarations +import { initSync } from "@automerge/automerge-subduction/slim" +// @ts-expect-error wasm-base64 is exported without declarations +import { wasmBase64 } from "@automerge/automerge-subduction/wasm-base64" +import { mount } from "svelte" +import LabApp from "./app/LabApp.svelte" +import "./style.css" + +initSync({ + module: Uint8Array.from(atob(wasmBase64), (char) => char.charCodeAt(0)), +}) + +const endpoint = + import.meta.env.VITE_SUBDUCTION_ENDPOINT ?? + "wss://coln.sync.inkandswitch.com" +const repo = new Repo({ + storage: new IndexedDBStorageAdapter("coln-lab", "documents"), + subductionWebsocketEndpoints: [endpoint], +}) +const openedDocuments = new Set() +const trackDocument = (documentId: string) => + openedDocuments.add(documentId as DocumentId) +const flush = () => { + if (openedDocuments.size > 0) void repo.flush([...openedDocuments]) +} + +addEventListener("pagehide", flush) +mount(LabApp, { + target: document.getElementById("app")!, + props: { repo, endpoint, trackDocument }, +}) + +if (import.meta.hot) { + import.meta.hot.dispose(() => { + removeEventListener("pagehide", flush) + void repo.shutdown() + }) +} diff --git a/examples/lab/src/style.css b/examples/lab/src/style.css new file mode 100644 index 00000000..17cc4934 --- /dev/null +++ b/examples/lab/src/style.css @@ -0,0 +1,249 @@ +/* SPDX-FileCopyrightText: 2026 Coln contributors */ +/* SPDX-License-Identifier: Apache-2.0 OR MIT */ + +@import url("https://fonts.googleapis.com/css2?family=DM+Mono:wght@400;500&family=Manrope:wght@400;600;700&display=swap"); +@import "tailwindcss"; + +:root { + color-scheme: dark; + font-variant-numeric: tabular-nums; + --lab-bg: #0d1211; + --lab-surface: #101718; + --lab-surface-raised: #182122; + --lab-editor: #0b1112; + --lab-border: #304041; + --lab-border-strong: #6b7a7b; + --lab-text: #e8ece8; + --lab-text-muted: #91a0a1; + --lab-text-faint: #667576; + --lab-accent: #d8ff57; + --lab-error: #ff9a86; + --lab-error-strong: #ff7657; + --lab-info: #77b7ff; + --lab-warning: #ffd166; + --lab-null: #bba4ff; + --lab-mono: "DM Mono", monospace; + --lab-sans: "Manrope", sans-serif; +} +html { background: var(--lab-bg); } +body { margin: 0; } +button, input, select { font: inherit; } +::selection { background: #657a32; color: #f0f1e8; } + +:where(a, button, input, select):focus-visible { + outline: 1px solid var(--lab-accent); + outline-offset: 3px; +} + +.lab-pane-resizer { + align-items: center; + background: var(--lab-border); + display: flex; + flex: 0 0 7px; + justify-content: center; + position: relative; + z-index: 20; +} + +.lab-pane-resizer[data-direction="vertical"] { + flex-basis: 7px; +} + +.lab-pane-resizer > span { + background: var(--lab-border-strong); + height: 32px; + transition: background-color 120ms ease; + width: 1px; +} + +.lab-pane-resizer::before { + content: ""; + inset: -3px; + position: absolute; +} + +.lab-pane-resizer[data-direction="vertical"] > span { + height: 1px; + width: 32px; +} + +.lab-pane-resizer:hover > span, +.lab-pane-resizer:focus-visible > span, +.lab-pane-resizer[data-active] > span { + background: var(--lab-accent); +} + +.lab-pane-resizer:focus-visible { + outline: 1px solid var(--lab-accent); + outline-offset: -1px; +} + +@media (max-width: 760px) { + .lab-desktop-pane-group, + .lab-repl-pane-group, + .theory-pane-group { + display: block !important; + height: auto !important; + overflow: visible !important; + } + + :is(.lab-desktop-pane-group, .lab-repl-pane-group, .theory-pane-group) > [data-pane] { + flex: none !important; + overflow: visible !important; + } + + :is(.lab-desktop-pane-group, .lab-repl-pane-group, .theory-pane-group) > .lab-pane-resizer { + display: none; + } + + .lab-repl-pane-group > [data-pane]:last-child { + min-height: 9rem; + } +} + +@media (min-width: 761px) and (max-width: 1099px) { + .theory-pane-group { + display: grid !important; + grid-template-columns: minmax(320px, .9fr) minmax(0, 1.1fr); + } + + .theory-pane-group > [data-pane] { + flex: none !important; + } + + .theory-pane-group > .lab-pane-resizer { + display: none; + } + + .theory-stores-pane { + grid-column: 1 / -1; + } +} + +.lab-tool { + background: var(--lab-surface); + color: var(--lab-text); +} + +.lab-secondary-action { + background: transparent; + border: 1px solid var(--lab-border-strong); + color: var(--lab-text); + cursor: pointer; + font-family: var(--lab-mono); + font-size: .875rem; + height: 2rem; + letter-spacing: .08em; + padding-inline: .625rem; + text-transform: uppercase; +} + +.lab-secondary-action:hover { + border-color: var(--lab-accent); + color: var(--lab-accent); +} + +.lab-outlined-action { + background: transparent; + border: 1px solid var(--lab-border-strong); + color: var(--lab-text); + cursor: pointer; +} + +.lab-outlined-action:hover:not(:disabled) { + border-color: var(--lab-accent); + color: var(--lab-accent); +} + +.lab-primary-action { + background: var(--lab-accent); + border: 1px solid var(--lab-accent); + color: var(--lab-surface); + cursor: pointer; +} + +.lab-primary-action:hover:not(:disabled) { + background: transparent; + color: var(--lab-accent); +} + +.lab-secondary-action:disabled { + cursor: wait; + opacity: .4; +} + +.lab-outlined-action:disabled, +.lab-primary-action:disabled { + cursor: not-allowed; + opacity: .4; +} + +.lab-pending-action:disabled { cursor: wait; } + +.lab-alert { + background: #211918; + border-left: 3px solid var(--lab-error-strong); + color: var(--lab-error); + font-family: var(--lab-mono); + font-size: .875rem; + line-height: 1.625; + padding: 1rem; +} + +.lab-loading { + background: var(--lab-surface); + display: grid; + height: 100%; + place-items: center; +} + +.lab-loading > p, +.lab-loading-label { + color: var(--lab-accent); + font-family: var(--lab-mono); + font-size: .875rem; + letter-spacing: .18em; +} + +.lab-grid-field { + background-color: #0f1513; + background-image: + linear-gradient(#26312d66 1px, transparent 1px), + linear-gradient(90deg, #26312d33 1px, transparent 1px); + background-size: 100% 32px, 32px 100%; +} + +.diagnostics pre { margin: 0; overflow-x: auto; white-space: pre-wrap; } +.diagnostics > :not(:last-child) { border-bottom: 1px solid #304041; padding-bottom: 1rem; } +.diag-debug { --diagnostic-color: #aab6b7; } +.diag-info { --diagnostic-color: #77b7ff; } +.diag-warning { --diagnostic-color: #ffd166; } +.diag-error { --diagnostic-color: #ff9a86; } +[class^="ann-"] { color: var(--diagnostic-color); font-weight: 500; } + +.lab-json-output { + min-width: max-content; +} + +.lab-json .json-formatter-row { + font-family: var(--lab-mono); + line-height: 1.65; +} + +.lab-json .json-formatter-row, +.lab-json .json-formatter-row a, +.lab-json .json-formatter-row a:hover { color: var(--lab-text-muted); } +.lab-json .json-formatter-key { color: var(--lab-info); } +.lab-json .json-formatter-string, +.lab-json .json-formatter-stringifiable { color: var(--lab-accent); } +.lab-json .json-formatter-number { color: var(--lab-warning); } +.lab-json .json-formatter-boolean { color: var(--lab-error); } +.lab-json .json-formatter-null { color: var(--lab-null); } +.lab-json .json-formatter-bracket { color: var(--lab-text-faint); } +.lab-json .json-formatter-toggler { color: var(--lab-accent); } +.lab-json .json-formatter-toggler { font-size: .875rem !important; } +.lab-json .json-fallback { + margin: 0; + overflow-wrap: anywhere; + white-space: pre-wrap; +} diff --git a/examples/lab/src/tools/compiler/CompilerTool.svelte b/examples/lab/src/tools/compiler/CompilerTool.svelte new file mode 100644 index 00000000..9dc008f1 --- /dev/null +++ b/examples/lab/src/tools/compiler/CompilerTool.svelte @@ -0,0 +1,340 @@ + + + + + +{#if loadError} + router.navigate("compiler")} /> +{:else if handle && theoryHandle} + {@const currentTheoryUrl = handle.url} +
+ {#if feedback}{/if} + + +
+
+
+

THEORY SOURCE

Source for this Coln theory

+ +
+
+
{statusLabel}
+ +
+
+
+ + {#if theory.source === ""} + + {/if} +
+
+
+ + +
+ {#if error}{/if} + {#if syncStatus === "error"}{/if} + {#if compilation.diagnosticsHtml.length === 0}

No compilation diagnostics

{:else}
{#each compilation.diagnosticsHtml as diagnostic}
{@html diagnostic}
{/each}
{/if}
+ {#if compilation.prettyIr.length === 0}

No compiled intermediate representation

{:else}
{#each compilation.prettyIr as realm}
{realm}
{/each}
{/if}
+ {#if compilation.irJson === ""}

No compiled IR JSON

{:else}{/if}
+
+
+ + + (selectedRealmName = name)} oncreate={createSelectedStore} oncopy={(url) => void copyText(url, "Store URL copied")} openhref={(url) => router.href("editor", url, currentTheoryUrl)} onopen={(event, url) => router.follow(event, "editor", url, currentTheoryUrl)} /> + +
+
+{:else} +

LOADING THEORY EDITOR

+{/if} diff --git a/examples/lab/src/tools/compiler/JsonViewer.svelte b/examples/lab/src/tools/compiler/JsonViewer.svelte new file mode 100644 index 00000000..112e7823 --- /dev/null +++ b/examples/lab/src/tools/compiler/JsonViewer.svelte @@ -0,0 +1,32 @@ + + + + + +
diff --git a/examples/lab/src/tools/compiler/OutputPanel.svelte b/examples/lab/src/tools/compiler/OutputPanel.svelte new file mode 100644 index 00000000..06fbeddc --- /dev/null +++ b/examples/lab/src/tools/compiler/OutputPanel.svelte @@ -0,0 +1,18 @@ + + + + + +
+
+

{label}

+ {index} +
+
+ {@render children()} +
+
diff --git a/examples/lab/src/tools/compiler/SourceEditor.svelte b/examples/lab/src/tools/compiler/SourceEditor.svelte new file mode 100644 index 00000000..cec2adbf --- /dev/null +++ b/examples/lab/src/tools/compiler/SourceEditor.svelte @@ -0,0 +1,20 @@ + + + + + +{#key handle} + +{/key} diff --git a/examples/lab/src/tools/compiler/StoresPanel.svelte b/examples/lab/src/tools/compiler/StoresPanel.svelte new file mode 100644 index 00000000..c57414fb --- /dev/null +++ b/examples/lab/src/tools/compiler/StoresPanel.svelte @@ -0,0 +1,119 @@ + + + + + + diff --git a/examples/lab/src/tools/compiler/compiled-realms.ts b/examples/lab/src/tools/compiler/compiled-realms.ts new file mode 100644 index 00000000..b2c8de94 --- /dev/null +++ b/examples/lab/src/tools/compiler/compiled-realms.ts @@ -0,0 +1,34 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import { isRealmSchema, type RealmSchema } from "./theory-document.ts" + +export type CompiledRealm = { + index: number + name: string + schema: RealmSchema +} + +export function parseCompiledRealms(irJson: string): CompiledRealm[] { + if (irJson === "") return [] + const value: unknown = JSON.parse(irJson) + if (!Array.isArray(value)) throw new TypeError("Compiler IR must be an array of realms") + + return value.map((schema, index) => { + if (!isRealmSchema(schema)) { + throw new TypeError(`Compiler IR realm ${index + 1} is malformed`) + } + return { index, name: realmName(schema, index), schema } + }) +} + +function realmName(schema: RealmSchema, index: number): string { + for (const entry of [...schema.entities, ...schema.rules]) { + if (typeof entry !== "object" || entry === null) continue + const path = (entry as { path?: unknown }).path + if (!Array.isArray(path) || !Array.isArray(path[0])) continue + if (path[0].every(part => typeof part === "string")) return path[0].join(".") + } + return `Realm ${index + 1}` +} diff --git a/examples/lab/src/tools/compiler/compiler.ts b/examples/lab/src/tools/compiler/compiler.ts new file mode 100644 index 00000000..f53ce6a8 --- /dev/null +++ b/examples/lab/src/tools/compiler/compiler.ts @@ -0,0 +1,72 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +export type Compilation = { + diagnosticsHtml: string[] + prettyIr: string[] + irJson: string +} + +type CompileResultPointer = unknown + +type CompilerExports = { + compile(source: string): Promise + freeCompileResult(pointer: CompileResultPointer): Promise | void + getDiagnostics(asHtml: boolean, pointer: CompileResultPointer): Promise | string[] + prettyIr(pointer: CompileResultPointer): Promise | string[] + irToJson(pointer: CompileResultPointer): Promise | string +} + +type WasmLoader = (options: { + wasmUrl: string + ghc_wasm_jsffi: (exports: CompilerExports) => WebAssembly.Imports +}) => Promise + +export type Compiler = { + compile(source: string): Promise +} + +let compilerPromise: Promise | undefined + +export function loadCompiler(): Promise { + compilerPromise ??= initializeCompiler().catch(error => { + compilerPromise = undefined + throw error + }) + return compilerPromise +} + +async function initializeCompiler(): Promise { + const distUrl = `${import.meta.env.BASE_URL}dist/` + const [loaderModule, jsffiModule] = await Promise.all([ + import(/* @vite-ignore */ `${distUrl}loadHaskellWasm.js`), + import(/* @vite-ignore */ `${distUrl}ghc_wasm_jsffi.js`), + ]) + const loadHaskellWasm = loaderModule.default as WasmLoader + const exports = await loadHaskellWasm({ + wasmUrl: `${distUrl}coln.wasm`, + ghc_wasm_jsffi: jsffiModule.default, + }) + let previousCompilation = Promise.resolve() + + return { + compile(source) { + const run = async () => { + const pointer = await exports.compile(source) + try { + // GHC reactor exports must not be entered concurrently. + const prettyIr = await exports.prettyIr(pointer) + const diagnosticsHtml = await exports.getDiagnostics(true, pointer) + const irJson = await exports.irToJson(pointer) + return { diagnosticsHtml, prettyIr, irJson } + } finally { + await exports.freeCompileResult(pointer) + } + } + const compilation = previousCompilation.then(run, run) + previousCompilation = compilation.then(() => undefined, () => undefined) + return compilation + }, + } +} diff --git a/examples/lab/src/tools/compiler/stores.ts b/examples/lab/src/tools/compiler/stores.ts new file mode 100644 index 00000000..60a4d4dc --- /dev/null +++ b/examples/lab/src/tools/compiler/stores.ts @@ -0,0 +1,37 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import type { Repo } from "@automerge/automerge-repo" +import { colnDocType } from "@coln-project/repo" +import type { CompiledRealm } from "./compiled-realms.ts" +import type { TheoryHandle } from "./theory-handle.svelte.ts" +import type { StoreRecord } from "./theory-document.ts" + +export async function createStore( + repo: Repo, + theory: TheoryHandle, + realm: CompiledRealm, +): Promise<{ record: StoreRecord; theoryFlushError?: unknown }> { + const createdAt = Date.now() + const sourceHeads = theory.heads + const store = repo.create(realm.schema, colnDocType) + const record: StoreRecord = { + url: store.url, + createdAt, + sourceHeads, + realmIndex: realm.index, + realmName: realm.name, + ir: realm.schema, + } + + await repo.flush([store.documentId]) + theory.appendStore(record) + try { + await repo.flush([theory.documentId]) + } catch (theoryFlushError) { + return { record, theoryFlushError } + } + + return { record } +} diff --git a/examples/lab/src/tools/compiler/theory-document.ts b/examples/lab/src/tools/compiler/theory-document.ts new file mode 100644 index 00000000..9804d5c0 --- /dev/null +++ b/examples/lab/src/tools/compiler/theory-document.ts @@ -0,0 +1,59 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import type { DocHandle } from "@automerge/automerge-repo" + +export type RealmSchema = { + entities: unknown[] + rules: unknown[] +} + +export type StoreRecord = { + url: string + createdAt: number + sourceHeads: string[] + realmIndex: number + realmName: string + ir: RealmSchema +} + +export type TheoryDocument = { + version: 1 + source: string + stores: StoreRecord[] +} + +export type TheoryDocumentHandle = DocHandle + +export function newTheoryDocument(): TheoryDocument { + return { version: 1, source: "", stores: [] } +} + +export function isTheoryDocument(value: unknown): value is TheoryDocument { + if (typeof value !== "object" || value === null) return false + const document = value as Partial + return document.version === 1 + && typeof document.source === "string" + && Array.isArray(document.stores) + && document.stores.every(isStoreRecord) +} + +export function isRealmSchema(value: unknown): value is RealmSchema { + if (typeof value !== "object" || value === null) return false + const schema = value as Partial + return Array.isArray(schema.entities) && Array.isArray(schema.rules) +} + +function isStoreRecord(value: unknown): value is StoreRecord { + if (typeof value !== "object" || value === null) return false + const store = value as Partial + return typeof store.url === "string" + && typeof store.createdAt === "number" + && Number.isFinite(store.createdAt) + && Array.isArray(store.sourceHeads) + && store.sourceHeads.every(head => typeof head === "string") + && Number.isInteger(store.realmIndex) + && typeof store.realmName === "string" + && isRealmSchema(store.ir) +} diff --git a/examples/lab/src/tools/compiler/theory-handle.svelte.ts b/examples/lab/src/tools/compiler/theory-handle.svelte.ts new file mode 100644 index 00000000..3e0cc119 --- /dev/null +++ b/examples/lab/src/tools/compiler/theory-handle.svelte.ts @@ -0,0 +1,40 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import { createSubscriber } from "svelte/reactivity" +import type { + StoreRecord, + TheoryDocument, + TheoryDocumentHandle, +} from "./theory-document.ts" + +export class TheoryHandle { + readonly #subscribe: () => void + + constructor(private readonly handle: TheoryDocumentHandle) { + this.#subscribe = createSubscriber(update => { + handle.on("change", update) + return () => handle.off("change", update) + }) + } + + get state(): TheoryDocument { + this.#subscribe() + return this.handle.doc() + } + + get documentId(): TheoryDocumentHandle["documentId"] { + return this.handle.documentId + } + + get heads(): string[] { + return [...this.handle.heads()] + } + + appendStore(record: StoreRecord): void { + this.handle.change(document => { + document.stores.push(record) + }) + } +} diff --git a/examples/lab/src/tools/editor/EditorTool.svelte b/examples/lab/src/tools/editor/EditorTool.svelte new file mode 100644 index 00000000..6d62c943 --- /dev/null +++ b/examples/lab/src/tools/editor/EditorTool.svelte @@ -0,0 +1,239 @@ + + + + + +{#if !store} + +{:else} + {@const storeUrl = store.handle.url} +
+ {#if actionError} + + {/if} + + + + + {#snippet toolbar()} +
+
+ + {#if theoryHref} + router.follow(event, "compiler", theoryUrl)}>Theory + {/if} + +
+ {storeUrl} +
+ {/snippet} +
+
+ + + + +
+
+{/if} diff --git a/examples/lab/src/tools/editor/ReplEditor.svelte b/examples/lab/src/tools/editor/ReplEditor.svelte new file mode 100644 index 00000000..feb21ef1 --- /dev/null +++ b/examples/lab/src/tools/editor/ReplEditor.svelte @@ -0,0 +1,42 @@ + + + + + + diff --git a/examples/lab/src/tools/editor/ReplOutput.svelte b/examples/lab/src/tools/editor/ReplOutput.svelte new file mode 100644 index 00000000..ac57791f --- /dev/null +++ b/examples/lab/src/tools/editor/ReplOutput.svelte @@ -0,0 +1,35 @@ + + + + + +
+ {#if !evaluation} +

Run a program to see its result and console output.

+ {:else} + {#each evaluation.console as entry} +
+ {entry.level} +
+ {#each entry.values as value} + + {/each} +
+
+ {/each} +
+
+ {evaluation.ok ? "Result" : `${evaluation.phase} error`} + {evaluation.durationMs.toFixed(1)} ms +
+
+ +
+
+ {/if} +
diff --git a/examples/lab/src/tools/editor/StoreLoader.svelte b/examples/lab/src/tools/editor/StoreLoader.svelte new file mode 100644 index 00000000..5b363c41 --- /dev/null +++ b/examples/lab/src/tools/editor/StoreLoader.svelte @@ -0,0 +1,65 @@ + + + + + +
+
{ + event.preventDefault() + onload(documentUrl) + }} + > +
+

STORE EDITOR / OPEN STORE

+

Inspect a Coln store.

+

Enter a Coln store URL to inspect its schema, tables, rows, and references.

+
+
+ + + {#if error} + + {/if} +
+ {#if recentUrl} +
+
+

Recent store

+ {displayUrl(recentUrl)} +
+ +
+ {/if} +
+
diff --git a/examples/lab/src/tools/editor/StoreRepl.svelte b/examples/lab/src/tools/editor/StoreRepl.svelte new file mode 100644 index 00000000..9131ed65 --- /dev/null +++ b/examples/lab/src/tools/editor/StoreRepl.svelte @@ -0,0 +1,61 @@ + + + + + +
+
+
+

JAVASCRIPT REPL

+

Run only code you trust / handle is in scope / use return for the result

+
+ +
+ + + (source = value)} onrun={runProgram} /> + + + + + + +
diff --git a/examples/lab/src/tools/editor/StoreSummary.svelte b/examples/lab/src/tools/editor/StoreSummary.svelte new file mode 100644 index 00000000..7c01ddbb --- /dev/null +++ b/examples/lab/src/tools/editor/StoreSummary.svelte @@ -0,0 +1,52 @@ + + + + + +
+
+
+

STORE SUMMARY

+

Tables backing this graph

+
+ {headCount} {headCount === 1 ? "head" : "heads"} +
+ +
+
{tables.length}tables
+
{rowCount}rows
+
{headCount}heads
+
+ +
+ {#each tables as table} +
+
+ {table.name} + {table.rowCount} {table.rowCount === 1 ? "row" : "rows"} +
+ {#if table.columns.length === 0} +

No non-key value columns

+ {:else} +
+ {#each table.columns as column} +
+ {column.name}{column.primary ? " *" : ""} + {column.type} +
+ {/each} +
+ {/if} +
+ {/each} +
+
diff --git a/examples/lab/src/tools/editor/TableBrowser.svelte b/examples/lab/src/tools/editor/TableBrowser.svelte new file mode 100644 index 00000000..c7fc7c96 --- /dev/null +++ b/examples/lab/src/tools/editor/TableBrowser.svelte @@ -0,0 +1,143 @@ + + + + + +
+
+
+
+

STORE CONTENTS

+

{tables.length} {tables.length === 1 ? "table" : "tables"}

+
+ {#if toolbar}
{@render toolbar()}
{/if} +
+ +
+ +
+ + +
+ {#if selected} + + + + + {#each selected.columns as column} + + {/each} + + + + {#each rows as row (displayRowRef(row.rowId.value).full)} + {@const rowId = displayRowRef(row.rowId.value)} + + + {#each row.values as value, index} + {@const displayed = displayValue(value)} + + {/each} + + {:else} + + + + {/each} + +
Row ID + {column.name}{column.primary ? " *" : ""} + {column.type} +
{rowId.compact} + {#if displayed.kind === "ref" && canfollow(selected.columns[index]?.referenceTable, value)} + + {:else} + {displayed.compact} + {/if} +
No rows in {selected.name}.
+ {:else} +
This store has no tables.
+ {/if} +
+
+
diff --git a/examples/lab/src/tools/editor/ValueInspector.svelte b/examples/lab/src/tools/editor/ValueInspector.svelte new file mode 100644 index 00000000..b2426fed --- /dev/null +++ b/examples/lab/src/tools/editor/ValueInspector.svelte @@ -0,0 +1,62 @@ + + + + + +
+
+ {#if structured} + + + {/if} + +
+ + {#if structured} +
+ {:else} + {primitive} + {/if} +
diff --git a/examples/lab/src/tools/editor/coln-handle.svelte.ts b/examples/lab/src/tools/editor/coln-handle.svelte.ts new file mode 100644 index 00000000..b6ae3a94 --- /dev/null +++ b/examples/lab/src/tools/editor/coln-handle.svelte.ts @@ -0,0 +1,24 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import type { + ColnDocument, + ColnHandle as RepoColnHandle, +} from "@coln-project/repo" +import { createSubscriber } from "svelte/reactivity" + +export class ColnHandleState { + readonly #subscribe: () => void + + constructor(private readonly handle: RepoColnHandle) { + this.#subscribe = createSubscriber((update) => { + handle.on("change", update) + return () => handle.off("change", update) + }) + } + + get current(): ColnDocument { + this.#subscribe() + return this.handle.doc() + } +} diff --git a/examples/lab/src/tools/editor/evaluate.ts b/examples/lab/src/tools/editor/evaluate.ts new file mode 100644 index 00000000..ccad8408 --- /dev/null +++ b/examples/lab/src/tools/editor/evaluate.ts @@ -0,0 +1,96 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import type { ColnHandle, RealmBindings } from "@coln-project/repo" +import { snapshot, type Snapshot } from "./snapshot.ts" + +export type ConsoleLevel = "debug" | "info" | "log" | "warn" | "error" + +export interface ConsoleEntry { + level: ConsoleLevel + values: Snapshot[] +} + +export interface EvaluationSuccess { + ok: true + result: Snapshot + console: ConsoleEntry[] + durationMs: number +} + +export interface EvaluationFailure { + ok: false + phase: "syntax" | "runtime" + error: Snapshot + console: ConsoleEntry[] + durationMs: number +} + +export type Evaluation = EvaluationSuccess | EvaluationFailure + +type AsyncProgram = ( + handle: unknown, + console: Pick, +) => Promise + +const AsyncFunction = Object.getPrototypeOf(async function () {}) + .constructor as new (...parameters: string[]) => AsyncProgram + +export async function evaluate( + source: string, + handle: ColnHandle, +): Promise { + const entries: ConsoleEntry[] = [] + let active = true + const capturedConsole = Object.fromEntries( + (["debug", "info", "log", "warn", "error"] as const).map((level) => [ + level, + (...values: unknown[]) => { + if (active) + entries.push({ + level, + values: values.map((value) => snapshot(value)), + }) + }, + ]), + ) as Pick + + let program: AsyncProgram + const started = performance.now() + try { + program = new AsyncFunction( + "handle", + "console", + `"use strict";\n${source}\n//# sourceURL=coln-store-lab-repl.js`, + ) + } catch (error) { + active = false + return { + ok: false, + phase: "syntax", + error: snapshot(error), + console: entries, + durationMs: performance.now() - started, + } + } + + try { + const result = await program(handle, capturedConsole) + active = false + return { + ok: true, + result: snapshot(result), + console: entries, + durationMs: performance.now() - started, + } + } catch (error) { + active = false + return { + ok: false, + phase: "runtime", + error: snapshot(error), + console: entries, + durationMs: performance.now() - started, + } + } +} diff --git a/examples/lab/src/tools/editor/format.ts b/examples/lab/src/tools/editor/format.ts new file mode 100644 index 00000000..16f796a9 --- /dev/null +++ b/examples/lab/src/tools/editor/format.ts @@ -0,0 +1,61 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +export interface DisplayValue { + compact: string + full: string + kind: "int" | "string" | "ref" | "unknown" +} + +type JsonObject = Record + +export function displayValue(value: unknown): DisplayValue { + if (!isObject(value) || typeof value.tag !== "string") { + return unknownValue(value) + } + if (value.tag === "int" && typeof value.value === "number") { + const rendered = String(value.value) + return { compact: rendered, full: rendered, kind: "int" } + } + if (value.tag === "string" && typeof value.value === "string") { + return { compact: value.value, full: value.value, kind: "string" } + } + if (value.tag === "row_id") return displayRowRef(value.value) + return unknownValue(value) +} + +export function displayRowRef(value: unknown): DisplayValue { + if (!isObject(value)) return unknownValue(value) + if (isObject(value.existing)) { + const commit = value.existing.commit + const counter = value.existing.counter + if (typeof commit === "string" && typeof counter === "number") { + const full = `${commit}:${counter}` + const compact = `${commit.slice(0, 8)}…${commit.slice(-4)}:${counter}` + return { compact, full, kind: "ref" } + } + } + if (isObject(value.pending)) { + const txId = value.pending.txId + const counter = value.pending.counter + if (typeof txId === "number" && typeof counter === "number") { + const rendered = `pending:${txId}:${counter}` + return { compact: rendered, full: rendered, kind: "ref" } + } + } + return unknownValue(value) +} + +function unknownValue(value: unknown): DisplayValue { + let rendered: string + try { + rendered = JSON.stringify(value) ?? String(value) + } catch { + rendered = String(value) + } + return { compact: rendered, full: rendered, kind: "unknown" } +} + +function isObject(value: unknown): value is JsonObject { + return value !== null && typeof value === "object" && !Array.isArray(value) +} diff --git a/examples/lab/src/tools/editor/recent-store.ts b/examples/lab/src/tools/editor/recent-store.ts new file mode 100644 index 00000000..62a89301 --- /dev/null +++ b/examples/lab/src/tools/editor/recent-store.ts @@ -0,0 +1,20 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +const key = "coln-store-editor:recent-store" + +export function loadRecentStore(): string { + try { + return localStorage.getItem(key) ?? "" + } catch { + return "" + } +} + +export function saveRecentStore(documentUrl: string): void { + try { + localStorage.setItem(key, documentUrl) + } catch { + // Store loading remains usable when storage is unavailable. + } +} diff --git a/examples/lab/src/tools/editor/schema.ts b/examples/lab/src/tools/editor/schema.ts new file mode 100644 index 00000000..6029cb63 --- /dev/null +++ b/examples/lab/src/tools/editor/schema.ts @@ -0,0 +1,121 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +export interface StoreColumn { + name: string + type: string + primary: boolean + referenceTable?: string +} + +export interface StoreTable { + name: string + columns: StoreColumn[] +} + +type JsonObject = Record +type IrPath = string[][] + +export function readTables(json: string): StoreTable[] { + const schema: unknown = JSON.parse(json) + if (!isObject(schema) || !Array.isArray(schema.entities)) { + throw new TypeError("Store IR does not contain an entities array") + } + + return schema.entities.map((entry, index) => readTable(entry, index)) +} + +function readTable(entry: unknown, index: number): StoreTable { + if (!isObject(entry)) throw new TypeError(`Entity ${index} is not an object`) + const path = readPath(entry.path, `Entity ${index} path`) + const definition = entry.value + if (!isObject(definition)) { + throw new TypeError(`Entity ${pathName(path)} has no definition`) + } + const variant = definition.entityVariant + if (!isObject(variant) || variant.tag !== "table") { + throw new TypeError(`Entity ${pathName(path)} is not a table`) + } + if (!Array.isArray(definition.columns)) { + throw new TypeError(`Table ${pathName(path)} has no columns array`) + } + + const primaryKey = definition.primaryKey + const primaryPaths = Array.isArray(primaryKey) + ? primaryKey.map((value, column) => + pathName(readPath(value, `Primary key ${column}`)), + ) + : [] + + return { + name: pathName(path), + columns: definition.columns.map((column, columnIndex) => { + if (!isObject(column)) { + throw new TypeError( + `Column ${columnIndex} in ${pathName(path)} is invalid`, + ) + } + const columnPath = readPath( + column.path, + `Column ${columnIndex} in ${pathName(path)}`, + ) + const name = pathName(columnPath) + const referenceTable = referenceTableName(column.type) + return { + name, + type: typeName(column.type), + primary: primaryPaths.includes(name), + ...(referenceTable ? { referenceTable } : {}), + } + }), + } +} + +function readPath(value: unknown, label: string): IrPath { + if ( + !Array.isArray(value) || + value.length === 0 || + value.some( + (segment) => + !Array.isArray(segment) || + segment.length === 0 || + segment.some((part) => typeof part !== "string" || part === ""), + ) + ) { + throw new TypeError(`${label} is invalid`) + } + return value as IrPath +} + +function pathName(path: IrPath): string { + return path.flat().join(".") +} + +function typeName(value: unknown): string { + if (!isObject(value) || typeof value.tag !== "string") return "unknown" + if (value.tag === "builtin") { + if (value.type === "builtinInt") return "int" + if (value.type === "builtinString") return "string" + } + if (value.tag === "rowId") { + try { + return `ref ${pathName(readPath(value.path, "Row reference path"))}` + } catch { + return "ref" + } + } + return value.tag +} + +function referenceTableName(value: unknown): string | undefined { + if (!isObject(value) || value.tag !== "rowId") return undefined + try { + return pathName(readPath(value.path, "Row reference path")) + } catch { + return undefined + } +} + +function isObject(value: unknown): value is JsonObject { + return value !== null && typeof value === "object" && !Array.isArray(value) +} diff --git a/examples/lab/src/tools/editor/snapshot.ts b/examples/lab/src/tools/editor/snapshot.ts new file mode 100644 index 00000000..5fc6b9cd --- /dev/null +++ b/examples/lab/src/tools/editor/snapshot.ts @@ -0,0 +1,81 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +export type Snapshot = + | null + | boolean + | number + | string + | Snapshot[] + | { [key: string]: Snapshot } + +export function snapshot(value: unknown, maxDepth = 8): Snapshot { + try { + return visit(value, new WeakSet(), 0, maxDepth) + } catch { + return tagged("uninspectable") + } +} + +function visit( + value: unknown, + seen: WeakSet, + depth: number, + maxDepth: number, +): Snapshot { + if ( + value === null || + typeof value === "boolean" || + typeof value === "string" + ) { + return value + } + if (typeof value === "number") { + return Number.isFinite(value) ? value : tagged("number", String(value)) + } + if (typeof value === "undefined") return tagged("undefined") + if (typeof value === "bigint") return tagged("bigint", value.toString()) + if (typeof value === "symbol") + return tagged("symbol", value.description ?? "") + if (typeof value === "function") return tagged("function", value.name) + if (typeof value !== "object") return tagged("unknown", String(value)) + if (seen.has(value)) return tagged("circular") + if (depth >= maxDepth) return tagged("truncated") + + seen.add(value) + try { + if (value instanceof Error) { + return { + $type: "error", + name: value.name, + message: value.message, + ...(value.stack ? { stack: value.stack } : {}), + } + } + if (Array.isArray(value)) { + return value.map((item) => visit(item, seen, depth + 1, maxDepth)) + } + + const prototype = Object.getPrototypeOf(value) + if (prototype !== Object.prototype && prototype !== null) { + return tagged("instance", prototype?.constructor?.name ?? "Object") + } + + return Object.fromEntries( + Object.entries(Object.getOwnPropertyDescriptors(value)).map( + ([key, descriptor]) => [ + key, + "value" in descriptor + ? visit(descriptor.value, seen, depth + 1, maxDepth) + : tagged("accessor"), + ], + ), + ) + } finally { + seen.delete(value) + } +} + +function tagged(type: string, value?: string): { [key: string]: Snapshot } { + return value === undefined ? { $type: type } : { $type: type, value } +} diff --git a/examples/lab/src/tools/editor/source-storage.ts b/examples/lab/src/tools/editor/source-storage.ts new file mode 100644 index 00000000..1f1c5556 --- /dev/null +++ b/examples/lab/src/tools/editor/source-storage.ts @@ -0,0 +1,24 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +const prefix = "coln-store-lab:source:" + +export const starterSource = `const doc = handle.doc() +console.log(JSON.parse(doc.jsonIR())) +return doc.heads()` + +export function loadSource(documentUrl: string): string { + try { + return localStorage.getItem(prefix + documentUrl) ?? starterSource + } catch { + return starterSource + } +} + +export function saveSource(documentUrl: string, source: string): void { + try { + localStorage.setItem(prefix + documentUrl, source) + } catch { + // The REPL remains usable when storage is unavailable. + } +} diff --git a/examples/lab/src/tools/graph/GraphCanvas.svelte b/examples/lab/src/tools/graph/GraphCanvas.svelte new file mode 100644 index 00000000..9846336f --- /dev/null +++ b/examples/lab/src/tools/graph/GraphCanvas.svelte @@ -0,0 +1,213 @@ + + + + + + + +
+
+ GRAPH DOCUMENT URL +
+ event.currentTarget.select()} data-testid="doc-url" /> + +
+
+ + {#if graph.vertices.length === 0} + + {/if} + + + + + + + + + + + + + + + + {#each drawableEdges as edge (edge.id)} + { + event.stopPropagation() + onselectedge(edge.id) + }} + onkeydown={(event) => event.key === "Enter" && onselectedge(edge.id)} + role="button" + tabindex="0" + aria-label={`Directed edge from ${vertexLabel(edge.fromId)} to ${vertexLabel(edge.toId)}`} + data-testid="graph-edge" + > + {/each} + + {#if previewEdgePath} + + {/if} + + {#each points as vertex (vertex.id)} + onselectvertex(vertex)} + onkeydown={(event) => event.key === "Enter" && onselectvertex(vertex)} + role="button" + tabindex="0" + data-testid="graph-vertex" + > + {vertex.label}: {vertex.id} + + {vertex.label} + + {/each} + + +
+ edge source + edge target +
+
diff --git a/examples/lab/src/tools/graph/GraphControls.svelte b/examples/lab/src/tools/graph/GraphControls.svelte new file mode 100644 index 00000000..b56e710a --- /dev/null +++ b/examples/lab/src/tools/graph/GraphControls.svelte @@ -0,0 +1,88 @@ + + + + + +
+
+

EDIT GRAPH

+
+ + + +
+
+ + +
+
↓
+
+ + +
+ +
+ + {#if selectedEdge} +
+ SELECTED EDGE + {vertexLabel(selectedEdge.fromId)} → {vertexLabel(selectedEdge.toId)} + {shortId(selectedEdge.id)} +
+ {:else if from} +
+ {to ? "EDGE READY TO ADD" : "SOURCE SELECTED"} + {from.label}{to ? ` → ${to.label}` : ""} + {shortId(from.id)} +
+ {/if} + + {#if error}{/if} + +
diff --git a/examples/lab/src/tools/graph/GraphTool.svelte b/examples/lab/src/tools/graph/GraphTool.svelte new file mode 100644 index 00000000..a8838af1 --- /dev/null +++ b/examples/lab/src/tools/graph/GraphTool.svelte @@ -0,0 +1,287 @@ + + + + + +{#if loadError} + +{:else if handle} +
+ {#if feedback} + {#key feedback.id} + + {/key} + {/if} + + + + selectedEdgeId = id} + onselectvertex={selectVertex} + /> + + + + + + +
+{:else} +
+

LOADING GRAPH DEMO

+
+{/if} diff --git a/examples/lab/src/tools/graph/coln-handle.svelte.ts b/examples/lab/src/tools/graph/coln-handle.svelte.ts new file mode 100644 index 00000000..a58871b3 --- /dev/null +++ b/examples/lab/src/tools/graph/coln-handle.svelte.ts @@ -0,0 +1,36 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import type { + ColnChange, + ColnDocument, + ColnHandle as RepoColnHandle, + RealmBindings, +} from "@coln-project/repo"; +import { createSubscriber } from "svelte/reactivity"; + +export class ColnHandle { + readonly #subscribe: () => void; + + constructor(private readonly handle: RepoColnHandle) { + this.#subscribe = createSubscriber((update) => { + handle.on("change", update); + handle.on("heads-changed", update); + + return () => { + handle.off("change", update); + handle.off("heads-changed", update); + }; + }); + } + + get state(): ColnDocument { + this.#subscribe(); + return this.handle.doc(); + } + + change(change: ColnChange): void { + this.handle.change(change); + } +} diff --git a/examples/lab/src/tools/graph/compile.sh b/examples/lab/src/tools/graph/compile.sh new file mode 100644 index 00000000..3ad2e163 --- /dev/null +++ b/examples/lab/src/tools/graph/compile.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash +# SPDX-FileCopyrightText: 2026 Coln contributors +# SPDX-License-Identifier: Apache-2.0 OR MIT + +set -euo pipefail + +script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" + +if command -v coln >/dev/null 2>&1; then + coln_cli=(coln) +elif command -v coln-cli >/dev/null 2>&1; then + coln_cli=(coln-cli) +else + coln_cli=(cabal run coln-cli --) +fi + +"${coln_cli[@]}" generate-ts "$script_dir/graph.coln" --output-dir "$script_dir/generated" +"${coln_cli[@]}" generate-ir "$script_dir/graph.coln" --output-dir "$script_dir/generated" diff --git a/examples/sync-demo/src/generated/Graph.ts b/examples/lab/src/tools/graph/generated/Graph.ts similarity index 99% rename from examples/sync-demo/src/generated/Graph.ts rename to examples/lab/src/tools/graph/generated/Graph.ts index 3257e861..b8fc8138 100644 --- a/examples/sync-demo/src/generated/Graph.ts +++ b/examples/lab/src/tools/graph/generated/Graph.ts @@ -8,4 +8,4 @@ export interface View { export interface Transaction extends View { V: runtime.ColnSet.Transaction; E: (x: runtime.Value) => (x: runtime.Value) => runtime.ColnSet.Transaction; -} +} \ No newline at end of file diff --git a/examples/lab/src/tools/graph/generated/GraphRealm.json b/examples/lab/src/tools/graph/generated/GraphRealm.json new file mode 100644 index 00000000..b83d89f3 --- /dev/null +++ b/examples/lab/src/tools/graph/generated/GraphRealm.json @@ -0,0 +1 @@ +{"entities":[{"path":[["GraphRealm"],["V"]],"value":{"entityVariant":{"tag":"table"},"columns":[],"primaryKey":null}},{"path":[["GraphRealm"],["E"]],"value":{"entityVariant":{"tag":"table"},"columns":[{"path":[["a"]],"type":{"tag":"rowId","path":[["GraphRealm"],["V"]]}},{"path":[["b"]],"type":{"tag":"rowId","path":[["GraphRealm"],["V"]]}}],"primaryKey":null}}],"rules":[{"path":[["GraphRealm"],["V"],["foreignKey"]],"value":{"ruleVariant":"enforced","varNames":[],"varTypes":[],"antecedents":[{"tag":"atom","atom":{"entity":[["GraphRealm"],["V"]],"rowId":null,"values":[]}}],"consequents":[]}},{"path":[["GraphRealm"],["E"],["foreignKey"]],"value":{"ruleVariant":"enforced","varNames":[[["a"]],[["b"]]],"varTypes":[{"tag":"rowId","path":[["GraphRealm"],["V"]]},{"tag":"rowId","path":[["GraphRealm"],["V"]]}],"antecedents":[{"tag":"atom","atom":{"entity":[["GraphRealm"],["E"]],"rowId":null,"values":[{"column":0,"term":{"tag":"var","index":0}},{"column":1,"term":{"tag":"var","index":1}}]}}],"consequents":[{"tag":"atom","atom":{"entity":[["GraphRealm"],["V"]],"rowId":{"tag":"var","index":0},"values":[]}},{"tag":"atom","atom":{"entity":[["GraphRealm"],["V"]],"rowId":{"tag":"var","index":1},"values":[]}}]}}]} \ No newline at end of file diff --git a/examples/lab/src/tools/graph/generated/GraphRealm.pretty b/examples/lab/src/tools/graph/generated/GraphRealm.pretty new file mode 100644 index 00000000..b4a7d502 --- /dev/null +++ b/examples/lab/src/tools/graph/generated/GraphRealm.pretty @@ -0,0 +1,13 @@ +flatrealm + entities + table ℜ.V := [] + table ℜ.E := [.a : ℜ.V, .b : ℜ.V] + end + rules + enforced ℜ.V.foreignKey := ℜ.V [] ⊢ ⊤ + enforced ℜ.E.foreignKey a b := ℜ.E [ + .a ↦ a, + .b ↦ b + ] ⊢ a ∈ ℜ.V [] ∧ b ∈ ℜ.V [] + end +end \ No newline at end of file diff --git a/examples/sync-demo/src/generated/GraphRealm.ts b/examples/lab/src/tools/graph/generated/GraphRealm.ts similarity index 99% rename from examples/sync-demo/src/generated/GraphRealm.ts rename to examples/lab/src/tools/graph/generated/GraphRealm.ts index e7a54556..9b2db543 100644 --- a/examples/sync-demo/src/generated/GraphRealm.ts +++ b/examples/lab/src/tools/graph/generated/GraphRealm.ts @@ -45,4 +45,4 @@ export class Transaction extends View { } }; } -} +} \ No newline at end of file diff --git a/examples/sync-demo/graph.coln b/examples/lab/src/tools/graph/graph.coln similarity index 100% rename from examples/sync-demo/graph.coln rename to examples/lab/src/tools/graph/graph.coln diff --git a/examples/lab/src/tools/graph/graph.ts b/examples/lab/src/tools/graph/graph.ts new file mode 100644 index 00000000..e84f5df4 --- /dev/null +++ b/examples/lab/src/tools/graph/graph.ts @@ -0,0 +1,114 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import type { ColnDocument, ColnHandle } from "@coln-project/repo" +import type { RowRef, RowView, Value } from "@coln-project/runtime" +import * as GraphRealm from "./generated/GraphRealm.ts" + +export type GraphHandle = ColnHandle +type GraphDocument = ColnDocument +type GraphWriter = Pick +type RowIdValue = Extract + +export type Vertex = { + id: string + label: string + ref: RowRef +} + +export type Edge = { + id: string + fromId: string + toId: string +} + +export type Graph = { + vertices: Vertex[] + edges: Edge[] + heads: string[] +} + +export function readGraph(document: GraphDocument): Graph { + const vertices = iteratorToArray(document.root.V.values()) + .map(rowRef) + .filter(ref => ref !== null) + .map(ref => ({ id: refId(ref), ref })) + .map((vertex, index) => ({ ...vertex, label: `V${index + 1}` })) + const vertexIds = new Set(vertices.map(vertex => vertex.id)) + + const edges = document.scanTable("GraphRealm.E") + .flatMap(row => projectEdge(row, vertexIds)) + .sort((left, right) => left.id.localeCompare(right.id)) + + return { vertices, edges, heads: [...document.heads()].sort() } +} + +export function addVertex(writer: GraphWriter): void { + writer.change(tx => { + tx.root.V.add() + }) +} + +export function addEdge( + writer: GraphWriter, + from: Vertex, + to: Vertex, +): void { + writer.change(tx => { + tx.root.E(rowValue(from.ref))(rowValue(to.ref)).add() + }) +} + +export function watchGraph( + handle: GraphHandle, + update: (graph: Graph) => void, +): () => void { + const refresh = () => update(readGraph(handle.doc())) + handle.on("change", refresh) + handle.on("heads-changed", refresh) + refresh() + + return () => { + handle.off("change", refresh) + handle.off("heads-changed", refresh) + } +} + +function projectEdge(row: RowView, vertexIds: Set): Edge[] { + const from = valueRef(row.values[0]) + const to = valueRef(row.values[1]) + const edge = rowRef(row) + if (!from || !to || !edge) return [] + + const fromId = refId(from) + const toId = refId(to) + if (!vertexIds.has(fromId) || !vertexIds.has(toId)) return [] + + return [{ id: refId(edge), fromId, toId }] +} + +function iteratorToArray(iterator: Iterator): T[] { + const values: T[] = [] + for (let next = iterator.next(); !next.done; next = iterator.next()) { + values.push(next.value) + } + return values +} + +function rowRef(row: RowView): RowRef | null { + return valueRef(row.rowId) +} + +function valueRef(value: Value | undefined): RowRef | null { + return value?.tag === "row_id" ? value.value : null +} + +function rowValue(ref: RowRef): RowIdValue { + return { tag: "row_id", value: ref } +} + +function refId(ref: RowRef): string { + if ("existing" in ref) return `${ref.existing.commit}:${ref.existing.counter}` + return `pending:${ref.pending.txId}:${ref.pending.counter}` +} diff --git a/examples/lab/tests/coln-handle.unit.ts b/examples/lab/tests/coln-handle.unit.ts new file mode 100644 index 00000000..7643d656 --- /dev/null +++ b/examples/lab/tests/coln-handle.unit.ts @@ -0,0 +1,99 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import { flushSync } from "svelte" +import { describe, expect, test, vi } from "vitest" +import type { ColnChange, ColnDocument, ColnHandle as RepoColnHandle } from "@coln-project/repo" +import { ColnHandle } from "../src/tools/graph/coln-handle.svelte.ts" +import * as GraphRealm from "../src/tools/graph/generated/GraphRealm.ts" +import { observe } from "./observe.svelte.ts" + +type GraphHandle = RepoColnHandle +type GraphDocument = ColnDocument +type Change = ColnChange +type ChangePayload = { doc: GraphDocument } +type Event = "change" | "heads-changed" + +describe("ColnHandle", () => { + test("returns the current document", () => { + const handle = new FakeHandle() + const colnHandle = new ColnHandle(handle as unknown as GraphHandle) + + expect(colnHandle.state).toBe(handle.document) + + handle.document = fakeDocument() + expect(colnHandle.state).toBe(handle.document) + }) + + test("delegates changes to the handle", () => { + const handle = new FakeHandle() + const colnHandle = new ColnHandle(handle as unknown as GraphHandle) + const change = vi.fn() + + colnHandle.change(change) + + expect(handle.change).toHaveBeenCalledOnce() + expect(handle.change).toHaveBeenCalledWith(change) + }) + + test("reacts to changes and releases its listener", async () => { + const handle = new FakeHandle() + const colnHandle = new ColnHandle(handle as unknown as GraphHandle) + let observed: GraphDocument | undefined + const stop = observe(() => colnHandle.state, state => { + observed = state + }) + flushSync() + + expect(observed).toBe(handle.document) + expect(handle.listenerCount("change")).toBe(1) + expect(handle.listenerCount("heads-changed")).toBe(1) + handle.document = fakeDocument() + handle.emit("change", { doc: handle.document }) + flushSync() + expect(observed).toBe(handle.document) + handle.document = fakeDocument() + handle.emit("heads-changed", { doc: handle.document }) + flushSync() + expect(observed).toBe(handle.document) + + stop() + await Promise.resolve() + expect(handle.listenerCount("change")).toBe(0) + expect(handle.listenerCount("heads-changed")).toBe(0) + }) +}) + +class FakeHandle { + document = fakeDocument() + readonly change = vi.fn((_change: Change) => {}) + readonly #listeners: Record void>> = { + change: new Set(), + "heads-changed": new Set(), + } + + doc(): GraphDocument { + return this.document + } + + on(event: Event, listener: (payload: ChangePayload) => void): void { + this.#listeners[event].add(listener) + } + + off(event: Event, listener: (payload: ChangePayload) => void): void { + this.#listeners[event].delete(listener) + } + + emit(event: Event, payload: ChangePayload): void { + for (const listener of this.#listeners[event]) listener(payload) + } + + listenerCount(event: Event): number { + return this.#listeners[event].size + } +} + +function fakeDocument(): GraphDocument { + return {} as GraphDocument +} diff --git a/examples/lab/tests/document-sync.unit.ts b/examples/lab/tests/document-sync.unit.ts new file mode 100644 index 00000000..cadfe4da --- /dev/null +++ b/examples/lab/tests/document-sync.unit.ts @@ -0,0 +1,227 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import type { Repo } from "@automerge/automerge-repo" +import { flushSync } from "svelte" +import { describe, expect, test } from "vitest" +import { + DocumentSync, + type SyncStatus, +} from "../src/lib/document-sync.svelte.ts" +import { observe } from "./observe.svelte.ts" + +describe("DocumentSync", () => { + test("moves from offline through syncing to synced and cleans up", async () => { + const repo = new FakeRepo(false) + const handle = new FakeHandle() + const sync = new DocumentSync(repo as unknown as Repo, handle) + let status: SyncStatus | undefined + const stop = observe(() => sync.status, value => (status = value)) + flushSync() + + expect(status).toBe("offline") + expect(repo.listenerCount()).toBe(1) + expect(handle.listenerCount()).toBe(1) + expect(repo.flushes).toHaveLength(0) + + repo.connect() + flushSync() + expect(status).toBe("syncing") + expect(repo.flushedDocuments[0]).toEqual([handle.documentId]) + await repo.resolveFlush(0) + flushSync() + expect(status).toBe("synced") + + stop() + await Promise.resolve() + expect(repo.listenerCount()).toBe(0) + expect(handle.listenerCount()).toBe(0) + }) + + test("flushes a newer change before reporting synced", async () => { + const repo = new FakeRepo(true) + const handle = new FakeHandle() + const sync = new DocumentSync(repo as unknown as Repo, handle) + let status: SyncStatus | undefined + const stop = observe(() => sync.status, value => (status = value)) + flushSync() + + handle.emitChange() + await repo.resolveFlush(0) + flushSync() + expect(status).toBe("syncing") + expect(repo.flushes).toHaveLength(2) + await repo.resolveFlush(1) + flushSync() + expect(status).toBe("synced") + stop() + }) + + test("reports failures and retries after another change", async () => { + const repo = new FakeRepo(true) + const handle = new FakeHandle() + const sync = new DocumentSync(repo as unknown as Repo, handle) + let observed: { status: SyncStatus; error: unknown } | undefined + const stop = observe( + () => ({ status: sync.status, error: sync.error }), + value => (observed = value), + ) + flushSync() + + const failure = new Error("save failed") + await repo.rejectFlush(0, failure) + flushSync() + expect(observed).toEqual({ status: "error", error: failure }) + + handle.emitChange() + flushSync() + await repo.resolveFlush(1) + flushSync() + expect(observed).toEqual({ status: "synced", error: undefined }) + stop() + }) + + test("reconnects without waiting for an obsolete flush", async () => { + const repo = new FakeRepo(true) + const handle = new FakeHandle() + const sync = new DocumentSync(repo as unknown as Repo, handle) + let status: SyncStatus | undefined + const stop = observe(() => sync.status, value => (status = value)) + flushSync() + + repo.disconnect() + flushSync() + expect(status).toBe("offline") + repo.connect() + flushSync() + expect(repo.flushes).toHaveLength(2) + await repo.resolveFlush(1) + flushSync() + expect(status).toBe("synced") + await repo.rejectFlush(0, new Error("obsolete")) + flushSync() + expect(status).toBe("synced") + stop() + }) + + test("ignores completion after cleanup and can resubscribe", async () => { + const repo = new FakeRepo(true) + const handle = new FakeHandle() + const sync = new DocumentSync(repo as unknown as Repo, handle) + let status: SyncStatus | undefined + const firstStop = observe(() => sync.status, value => (status = value)) + flushSync() + + firstStop() + await Promise.resolve() + await repo.rejectFlush(0, new Error("late")) + expect(repo.listenerCount()).toBe(0) + expect(handle.listenerCount()).toBe(0) + + const secondStop = observe(() => sync.status, value => (status = value)) + flushSync() + expect(repo.flushes).toHaveLength(2) + expect(status).toBe("syncing") + await repo.resolveFlush(1) + flushSync() + expect(status).toBe("synced") + secondStop() + }) +}) + +class FakeHandle { + readonly documentId = "test-document" as never + readonly #listeners = new Set<() => void>() + + on(_event: "change", listener: () => void): void { + this.#listeners.add(listener) + } + + off(_event: "change", listener: () => void): void { + this.#listeners.delete(listener) + } + + emitChange(): void { + for (const listener of this.#listeners) listener() + } + + listenerCount(): number { + return this.#listeners.size + } +} + +class FakeRepo { + readonly flushes: Deferred[] = [] + readonly flushedDocuments: unknown[] = [] + #connected: boolean + readonly #listeners = new Set<() => void>() + + constructor(connected: boolean) { + this.#connected = connected + } + + isSubductionConnected(): boolean { + return this.#connected + } + + on(_event: "subduction-connection", listener: () => void): void { + this.#listeners.add(listener) + } + + off(_event: "subduction-connection", listener: () => void): void { + this.#listeners.delete(listener) + } + + flush(documents?: unknown): Promise { + const flush = deferred() + this.flushes.push(flush) + this.flushedDocuments.push(documents) + return flush.promise + } + + connect(): void { + this.#connected = true + for (const listener of this.#listeners) listener() + } + + disconnect(): void { + this.#connected = false + for (const listener of this.#listeners) listener() + } + + async resolveFlush(index: number): Promise { + const flush = this.flushes[index] + if (!flush) throw new Error(`Missing flush ${index}`) + flush.resolve(undefined) + await flush.promise + await Promise.resolve() + } + + async rejectFlush(index: number, error: unknown): Promise { + const flush = this.flushes[index] + if (!flush) throw new Error(`Missing flush ${index}`) + flush.reject(error) + await flush.promise.catch(() => undefined) + await Promise.resolve() + } + + listenerCount(): number { + return this.#listeners.size + } +} + +type Deferred = { + promise: Promise + resolve: (value: T | PromiseLike) => void + reject: (error: unknown) => void +} + +function deferred(): Deferred { + let resolve!: Deferred["resolve"] + let reject!: Deferred["reject"] + const promise = new Promise((resolvePromise, rejectPromise) => { + resolve = resolvePromise + reject = rejectPromise + }) + return { promise, resolve, reject } +} diff --git a/examples/lab/tests/evaluate.unit.ts b/examples/lab/tests/evaluate.unit.ts new file mode 100644 index 00000000..52ee2dad --- /dev/null +++ b/examples/lab/tests/evaluate.unit.ts @@ -0,0 +1,34 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import type { ColnHandle } from "@coln-project/repo" +import { describe, expect, it } from "vitest" +import { evaluate } from "../src/tools/editor/evaluate.ts" + +const handle = { marker: "handle" } as unknown as ColnHandle + +describe("evaluate", () => { + it("awaits programs and captures console output", async () => { + const result = await evaluate( + 'console.log("ready", 2); await Promise.resolve(); return handle.marker', + handle, + ) + expect(result).toMatchObject({ + ok: true, + result: "handle", + console: [{ level: "log", values: ["ready", 2] }], + }) + }) + + it("separates syntax and runtime failures", async () => { + expect(await evaluate("return )", handle)).toMatchObject({ + ok: false, + phase: "syntax", + }) + expect(await evaluate('throw new Error("broken")', handle)).toMatchObject({ + ok: false, + phase: "runtime", + error: { $type: "error", message: "broken" }, + }) + }) +}) diff --git a/examples/lab/tests/format.unit.ts b/examples/lab/tests/format.unit.ts new file mode 100644 index 00000000..1956c007 --- /dev/null +++ b/examples/lab/tests/format.unit.ts @@ -0,0 +1,28 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import { describe, expect, it } from "vitest" +import { displayRowRef, displayValue } from "../src/tools/editor/format.ts" + +describe("value formatting", () => { + it("formats primitive values", () => { + expect(displayValue({ tag: "string", value: "Ada" })).toMatchObject({ + compact: "Ada", + kind: "string", + }) + expect(displayValue({ tag: "int", value: 37 })).toMatchObject({ + compact: "37", + kind: "int", + }) + }) + + it("compacts committed references without losing the full ID", () => { + const commit = + "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef" + expect(displayRowRef({ existing: { commit, counter: 4 } })).toEqual({ + compact: "12345678…cdef:4", + full: `${commit}:4`, + kind: "ref", + }) + }) +}) diff --git a/examples/lab/tests/lab.spec.ts b/examples/lab/tests/lab.spec.ts new file mode 100644 index 00000000..dffddcef --- /dev/null +++ b/examples/lab/tests/lab.spec.ts @@ -0,0 +1,356 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import { expect, test, type Locator } from "@playwright/test" + +async function expectActiveLineHighlight(editor: Locator): Promise { + await editor.click() + const activeLine = editor.locator(".cm-activeLine") + await expect(activeLine).toBeVisible() + expect(await activeLine.evaluate((element) => getComputedStyle(element).backgroundColor)) + .toBe("rgba(0, 0, 0, 0)") + expect( + await activeLine.evaluate( + (element) => getComputedStyle(element, "::before").backgroundColor, + ), + ).toBe("rgb(17, 26, 27)") +} + +async function dragResizer(page: import("@playwright/test").Page, resizer: Locator, deltaX: number, deltaY: number): Promise { + const box = await resizer.boundingBox() + expect(box).not.toBeNull() + const x = box!.x + box!.width / 2 + const y = box!.y + box!.height / 2 + await page.mouse.move(x, y) + await page.mouse.down() + await page.mouse.move(x + deltaX, y + deltaY, { steps: 5 }) + await page.mouse.up() +} + +async function expectMinimumTextSize( + page: import("@playwright/test").Page, +): Promise { + const violations = await page.locator("body *").evaluateAll((elements) => + elements.flatMap((element) => { + const style = getComputedStyle(element) + const hasDirectText = [...element.childNodes].some( + (node) => node.nodeType === Node.TEXT_NODE && node.textContent?.trim(), + ) + const isTextControl = + element instanceof HTMLInputElement || + element instanceof HTMLSelectElement || + element instanceof HTMLTextAreaElement + if ( + (!hasDirectText && !isTextControl) || + style.display === "none" || + style.visibility === "hidden" || + element.getClientRects().length === 0 + ) return [] + let size = Number.parseFloat(style.fontSize) + if (element instanceof SVGTextElement) { + const transform = element.getScreenCTM() + if (transform) size *= Math.hypot(transform.a, transform.b) + } + const minimum = element.closest("[data-small-detail]") ? 12 : 14 + return size < minimum + ? [`${element.tagName.toLowerCase()}: ${size}px (${element.textContent?.trim().slice(0, 40)})`] + : [] + }), + ) + expect(violations).toEqual([]) +} + +test("loads every tool from its static route", async ({ page }) => { + await page.goto("/compiler/") + await expect(page).toHaveTitle("Theory Editor — Coln Lab") + await expect(page.getByTestId("active-tool-identity")).toHaveText("Theory Editor") + await expect(page.getByTestId("active-tool-identity")).toBeVisible() + await expect(page.getByTestId("active-tool-action")).toHaveAttribute("aria-label", "New theory") + await expect(page.getByTestId("active-tool-action")).toHaveAttribute("href", "/compiler/") + await expect(page.locator("main > header")).toHaveCount(1) + + await page.goto("/editor/") + await expect(page).toHaveTitle("Store Editor — Coln Lab") + await expect(page.getByTestId("active-tool-identity")).toHaveText("Store Editor") + await expect(page.getByTestId("active-tool-identity")).toBeVisible() + await expect(page.getByTestId("active-tool-action")).toHaveAttribute("aria-label", "Open store") + await expect(page.getByTestId("active-tool-action")).toHaveAttribute("href", "/editor/") + await expect( + page.getByRole("heading", { name: "Inspect a Coln store." }), + ).toBeVisible() + + await page.goto("/sync/") + await expect(page).toHaveTitle("Graph Demo — Coln Lab") + await expect(page.getByTestId("active-tool-identity")).toHaveText("Graph Demo") + await expect(page.getByTestId("active-tool-identity")).toBeVisible() + await expect(page.getByTestId("active-tool-action")).toHaveAttribute("aria-label", "New graph") + await expect(page.getByTestId("active-tool-action")).toHaveAttribute("href", "/sync/") +}) + +test("highlights the active compiler line", async ({ page }) => { + await page.goto("/compiler/") + await expectActiveLineHighlight(page.locator(".cm-content")) +}) + +test("resizes and persists graph panes", async ({ page }) => { + await page.setViewportSize({ width: 1280, height: 800 }) + await page.goto("/sync/") + + const canvas = page.getByTestId("graph-canvas") + const workspaceResizer = page.getByTestId("graph-workspace-resizer") + await expect(workspaceResizer).toBeVisible() + const canvasPaneBox = await page.locator("#graph-canvas-pane").boundingBox() + const canvasBox = await canvas.boundingBox() + expect(canvasPaneBox).not.toBeNull() + expect(canvasBox).not.toBeNull() + expect(Math.abs(canvasBox!.height - canvasPaneBox!.height)).toBeLessThan(2) + const graphPanelBox = await page.locator("#graph-graph-panel").boundingBox() + const storeSummaryBox = await page.getByTestId("store-summary").boundingBox() + expect(graphPanelBox).not.toBeNull() + expect(storeSummaryBox).not.toBeNull() + expect(Math.abs( + storeSummaryBox!.y + storeSummaryBox!.height - (graphPanelBox!.y + graphPanelBox!.height), + )).toBeLessThanOrEqual(2) + const initialCanvasWidth = (await canvas.boundingBox())!.width + await dragResizer(page, workspaceResizer, -120, 0) + const resizedCanvasWidth = (await canvas.boundingBox())!.width + expect(resizedCanvasWidth).toBeLessThan(initialCanvasWidth - 80) + + await expect.poll(() => page.evaluate(() => { + const value = localStorage.getItem("paneforge:coln-lab-graph-workspace") + if (!value) return false + return Object.values(JSON.parse(value) as Record) + .some(({ layout }) => layout[0] < 60) + })).toBe(true) + const persistedWidth = (await canvas.boundingBox())!.width + await page.reload() + await expect(page.getByTestId("graph-canvas")).toBeVisible() + expect(Math.abs((await canvas.boundingBox())!.width - persistedWidth)).toBeLessThan(10) + + await workspaceResizer.focus() + const keyboardStartWidth = (await canvas.boundingBox())!.width + await page.keyboard.press("ArrowRight") + expect((await canvas.boundingBox())!.width).toBeGreaterThan(keyboardStartWidth) + + await page.getByRole("tab", { name: "Store REPL" }).click() + const output = page.getByTestId("repl-output") + const outputResizer = page.getByTestId("repl-output-resizer") + const initialOutputHeight = (await output.boundingBox())!.height + await dragResizer(page, outputResizer, 0, -80) + expect((await output.boundingBox())!.height).toBeGreaterThan(initialOutputHeight + 50) + + await page.setViewportSize({ width: 390, height: 844 }) + await expect(workspaceResizer).toBeHidden() + await expect(outputResizer).toBeHidden() + expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true) +}) + +test("uses the graph demo and moves from theories into their stores", async ({ + page, +}) => { + await page.setViewportSize({ width: 390, height: 844 }) + await page.goto("/") + await expect( + page.getByRole("heading", { name: "Design theories. Inspect stores. Explore Coln." }), + ).toBeVisible() + await expect( + page.getByRole("navigation", { name: "Coln Lab" }), + ).toBeVisible() + await expect(page.getByRole("group", { name: "Workbench" })).toBeVisible() + await expect(page.getByRole("group", { name: "Demos" })).toBeVisible() + expect( + await page.evaluate( + () => document.documentElement.scrollWidth <= innerWidth, + ), + ).toBe(true) + await expectMinimumTextSize(page) + await page.setViewportSize({ width: 1280, height: 800 }) + + await page.getByRole("link", { name: /Open Graph Demo/ }).click() + await expect(page).toHaveURL(/\/sync\/#coln:/) + const graphUrl = page.url() + await expect(page.getByTestId("active-tool-identity")).toHaveText("Graph Demo") + await expect(page.getByTestId("sync-status")).toBeVisible() + await expect(page.getByTestId("head-count")).toHaveText(/^\d+ heads?$/) + const graphCanvasBox = await page.getByTestId("graph-canvas").boundingBox() + const graphControlsBox = await page.getByTestId("graph-sidebar").boundingBox() + expect(graphCanvasBox).not.toBeNull() + expect(graphControlsBox).not.toBeNull() + expect(graphControlsBox!.x).toBeGreaterThan(graphCanvasBox!.x) + expect(Math.abs(graphControlsBox!.y - graphCanvasBox!.y)).toBeLessThan(2) + expect( + await page + .getByTestId("graph-canvas") + .evaluate((element) => getComputedStyle(element).minHeight), + ).toBe("680px") + await expect(page.getByTestId("store-summary-table")).toHaveCount(2) + await expectMinimumTextSize(page) + const vertexSummary = page.locator('[data-table-name="GraphRealm.V"]') + const edgeSummary = page.locator('[data-table-name="GraphRealm.E"]') + await page.getByTestId("add-vertex").click() + await expect(page.getByTestId("graph-vertex")).toHaveCount(1) + await expect(vertexSummary.getByTestId("store-summary-row-count")).toHaveText("1 row") + await page.getByTestId("add-vertex").click() + await expect(page.getByTestId("graph-vertex")).toHaveCount(2) + await page.getByTestId("graph-vertex").nth(0).click() + await page.getByTestId("graph-vertex").nth(1).click() + await expect(page.getByTestId("add-edge")).toBeEnabled() + await page.getByTestId("add-edge").click() + await expect(page.getByTestId("graph-edge")).toHaveCount(1) + await expect(vertexSummary.getByTestId("store-summary-row-count")).toHaveText("2 rows") + await expect(edgeSummary.getByTestId("store-summary-row-count")).toHaveText("1 row") + + await page.getByRole("tab", { name: "Store REPL" }).click() + await expect(page).toHaveURL(graphUrl) + await expect(page.getByTestId("store-repl")).toBeVisible() + await expectActiveLineHighlight(page.getByTestId("repl-editor")) + await expect(page.getByTestId("run-program")).toHaveText("Run Ctrl+Enter") + await page.keyboard.press("Control+Enter") + await expect(page.getByTestId("repl-result")).toBeVisible() + await page.getByRole("tab", { name: "Graph" }).click() + await expect(page.getByTestId("graph-edge")).toHaveCount(1) + await page.setViewportSize({ width: 390, height: 844 }) + await expect(page.getByRole("tab", { name: "Store REPL" })).toBeVisible() + expect( + await page.evaluate( + () => document.documentElement.scrollWidth <= innerWidth, + ), + ).toBe(true) + await expectMinimumTextSize(page) + await page.setViewportSize({ width: 1280, height: 800 }) + + const labTools = page.getByRole("navigation", { name: "Coln Lab" }) + await labTools.getByRole("link", { name: /Store Editor/ }).click() + await expect(page).toHaveURL(/\/editor\/$/) + await expect(page.getByTestId("recent-store")).toHaveCount(0) + + await labTools.getByRole("link", { name: /Theory Editor/ }).click() + await expect(page).toHaveURL(/\/compiler\/#automerge:/) + const theoryUrl = page.url() + const theoryDocumentUrl = new URL(theoryUrl).hash.slice(1) + await expect(page.getByTestId("active-tool-identity")).toHaveText("Theory Editor") + await expect(page.getByText("THEORY SOURCE", { exact: true })).toBeVisible() + await expect(page.getByText("Diagnostics", { exact: true })).toBeVisible() + await expect(page.getByTestId("theory-source-resizer")).toBeVisible() + await expect(page.getByTestId("theory-stores-resizer")).toBeVisible() + await expectMinimumTextSize(page) + await page.setViewportSize({ width: 390, height: 844 }) + await expect(page.getByTestId("active-tool-action")).toBeVisible() + expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true) + await expectMinimumTextSize(page) + await page.setViewportSize({ width: 1280, height: 800 }) + const editor = page.locator(".cm-content") + const graphExample = page.getByRole("button", { name: "Use basic graph example" }) + await expect(graphExample).toBeVisible() + await graphExample.click() + await expect(editor).toContainText("theory Graph") + await expect(editor).toContainText("realm GraphRealm") + await expect(graphExample).toHaveCount(0) + const createStore = page.getByRole("button", { name: /Create store/ }) + await expect(createStore).toBeEnabled({ timeout: 60_000 }) + const sourcePanelBox = await page + .getByText("THEORY SOURCE", { exact: true }) + .locator("xpath=ancestor::section[1]") + .boundingBox() + const storesPanelBox = await page + .locator("aside") + .filter({ hasText: "STORES" }) + .boundingBox() + expect(sourcePanelBox).not.toBeNull() + expect(storesPanelBox).not.toBeNull() + expect(storesPanelBox!.x).toBeGreaterThan(sourcePanelBox!.x) + expect(Math.abs(storesPanelBox!.y - sourcePanelBox!.y)).toBeLessThan(2) + await createStore.click() + await expect( + page.getByRole("link", { name: "Open in Store Editor", exact: true }), + ).toBeVisible() + + await page.getByRole("link", { name: "Open in Store Editor", exact: true }).click() + await expect(page).toHaveURL(/\/editor\/\?theory=automerge%3A.*#coln:/) + await expect(page.getByTestId("table-option")).toHaveCount(2) + await expectMinimumTextSize(page) + await page.setViewportSize({ width: 390, height: 844 }) + await expect(page.getByRole("link", { name: "Return to Theory Editor" })).toBeVisible() + expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true) + await expectMinimumTextSize(page) + await page.setViewportSize({ width: 1280, height: 800 }) + const storeBrowser = page.locator("#store-browser-pane") + const storeBrowserWidth = (await storeBrowser.boundingBox())!.width + await page.getByTestId("store-workspace-resizer").focus() + await page.keyboard.press("ArrowRight") + expect((await storeBrowser.boundingBox())!.width).toBeGreaterThan(storeBrowserWidth) + await expect( + page.getByRole("link", { name: "Return to Theory Editor", exact: true }), + ).toBeVisible() + const replEditor = page.getByTestId("repl-editor") + await replEditor.click() + await page.keyboard.press("ControlOrMeta+A") + await page.keyboard.insertText(`handle.change((transaction) => { + const from = transaction.add("GraphRealm.V", []) + const to = transaction.add("GraphRealm.V", []) + transaction.add("GraphRealm.E", [from, to]) +}) +return handle.doc().heads()`) + await page.getByTestId("run-program").click() + await expect(page.getByTestId("repl-result")).toBeVisible() + const edgeTable = page.getByTestId("table-option").filter({ hasText: "GraphRealm.E" }) + await edgeTable.click() + const references = page.getByTestId("table-reference") + await expect(references).toHaveCount(2) + const targetId = (await references.first().getAttribute("aria-label"))! + .replace("Open referenced row ", "") + await references.first().click() + await expect( + page.getByTestId("table-option").filter({ hasText: "GraphRealm.V" }), + ).toHaveAttribute("data-selected", "true") + const targetRow = page.getByTestId("table-row").filter({ + has: page.locator(`td[title="${targetId}"]`), + }) + await expect(targetRow).toHaveAttribute("data-selected", "true") + await expect(targetRow).toBeFocused() + await edgeTable.click() + await references.first().click() + await expect(targetRow).toBeFocused() + const storeDocumentUrl = new URL(page.url()).hash.slice(1) + + await labTools.getByRole("link", { name: /Graph Demo/ }).click() + await expect(page).toHaveURL(graphUrl) + await expect(page.getByTestId("graph-vertex")).toHaveCount(2) + await expect(page.getByTestId("graph-edge")).toHaveCount(1) + + await labTools.getByRole("link", { name: /Theory Editor/ }).click() + await expect(page).toHaveURL(theoryUrl) + await expect(page.locator(".cm-content")).toContainText("theory Graph") + + await labTools.getByRole("link", { name: /Store Editor/ }).click() + await expect(page).toHaveURL(/\/editor\/#coln:/) + expect(new URL(page.url()).hash.slice(1)).toBe(storeDocumentUrl) + await expect(page.getByTestId("table-option")).toHaveCount(2) + await page.getByTestId("active-tool-action").click() + await expect(page).toHaveURL(/\/editor\/$/) + await expect(page.getByTestId("recent-store").locator("code")).toHaveAttribute( + "title", + storeDocumentUrl, + ) + + await page + .getByTestId("document-url-input") + .fill(storeDocumentUrl.replace(/^coln:/, "automerge:")) + await page.getByTestId("open-store").click() + await expect(page.getByTestId("load-error")).toBeVisible() + await expect(page.getByTestId("recent-store")).toBeVisible() + + await page.reload() + await expect(page.getByTestId("recent-store")).toBeVisible() + await page.goto( + `/editor/?theory=${encodeURIComponent(theoryDocumentUrl)}#invalid`, + ) + await expect(page.getByTestId("load-error")).toBeVisible() + await expect(page.getByTestId("recent-store")).toBeVisible() + await page.getByRole("button", { name: "Reopen store" }).click() + await expect(page).toHaveURL(/\/editor\/#coln:/) + await expect(page.getByTestId("table-option")).toHaveCount(2) + await expect( + page.getByRole("link", { name: "Return to Theory Editor", exact: true }), + ).toHaveCount(0) +}) diff --git a/examples/lab/tests/observe.svelte.ts b/examples/lab/tests/observe.svelte.ts new file mode 100644 index 00000000..b9afeb19 --- /dev/null +++ b/examples/lab/tests/observe.svelte.ts @@ -0,0 +1,9 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +export function observe(read: () => T, update: (value: T) => void): () => void { + return $effect.root(() => { + $effect(() => update(read())) + }) +} diff --git a/examples/lab/tests/recent-store.unit.ts b/examples/lab/tests/recent-store.unit.ts new file mode 100644 index 00000000..b8a5ed7a --- /dev/null +++ b/examples/lab/tests/recent-store.unit.ts @@ -0,0 +1,38 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest" +import { + loadRecentStore, + saveRecentStore, +} from "../src/tools/editor/recent-store.ts" + +const values = new Map() + +beforeEach(() => { + values.clear() + vi.stubGlobal("localStorage", { + getItem: (key: string) => values.get(key) ?? null, + setItem: (key: string, value: string) => values.set(key, value), + }) +}) + +afterEach(() => vi.unstubAllGlobals()) + +describe("recent store storage", () => { + it("persists the latest store", () => { + expect(loadRecentStore()).toBe("") + saveRecentStore("coln:first") + saveRecentStore("coln:latest") + expect(loadRecentStore()).toBe("coln:latest") + }) + + it("tolerates unavailable storage", () => { + vi.stubGlobal("localStorage", { + getItem: () => { throw new Error("unavailable") }, + setItem: () => { throw new Error("unavailable") }, + }) + expect(loadRecentStore()).toBe("") + expect(() => saveRecentStore("coln:store")).not.toThrow() + }) +}) diff --git a/examples/lab/tests/router.unit.ts b/examples/lab/tests/router.unit.ts new file mode 100644 index 00000000..58380c70 --- /dev/null +++ b/examples/lab/tests/router.unit.ts @@ -0,0 +1,47 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import { describe, expect, it } from "vitest" +import { routeFromUrl } from "../src/lib/router.svelte.ts" + +describe("Coln Lab routes", () => { + it("separates tool paths from document hashes", () => { + expect( + routeFromUrl( + new URL( + "https://lab.example/editor/?theory=automerge%3Atheory#coln:store", + ), + ), + ).toEqual({ + tool: "editor", + documentUrl: "coln:store", + theoryUrl: "automerge:theory", + pathname: "/editor/", + }) + }) + + it("supports a static deployment base", () => { + expect( + routeFromUrl( + new URL("https://example.test/lab/sync/#coln:graph"), + "/lab/", + ), + ).toMatchObject({ tool: "sync", documentUrl: "coln:graph" }) + }) + + it("reports unknown instruments", () => { + expect(routeFromUrl(new URL("https://lab.example/unknown/"))).toMatchObject( + { + tool: "not-found", + }, + ) + }) + + it("rejects extra path segments", () => { + expect( + routeFromUrl(new URL("https://lab.example/editor/extra/")), + ).toMatchObject({ + tool: "not-found", + }) + }) +}) diff --git a/examples/lab/tests/schema.unit.ts b/examples/lab/tests/schema.unit.ts new file mode 100644 index 00000000..117d0471 --- /dev/null +++ b/examples/lab/tests/schema.unit.ts @@ -0,0 +1,62 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import { describe, expect, it } from "vitest" +import { readTables } from "../src/tools/editor/schema.ts" + +describe("readTables", () => { + it("extracts paths, types, and primary columns", () => { + const tables = readTables( + JSON.stringify({ + entities: [ + { + path: [["Realm"], ["profile"]], + value: { + entityVariant: { tag: "table" }, + columns: [ + { + path: [["owner"]], + type: { tag: "rowId", path: [["Realm"], ["Person"]] }, + }, + { + path: [["details"], ["name"]], + type: { tag: "builtin", type: "builtinString" }, + }, + ], + primaryKey: [[["owner"]]], + }, + }, + ], + rules: [], + }), + ) + + expect(tables).toEqual([ + { + name: "Realm.profile", + columns: [ + { + name: "owner", + type: "ref Realm.Person", + primary: true, + referenceTable: "Realm.Person", + }, + { name: "details.name", type: "string", primary: false }, + ], + }, + ]) + }) + + it("rejects malformed IR", () => { + expect(() => readTables('{"rules":[]}')).toThrow("entities array") + expect(() => + readTables( + JSON.stringify({ + entities: [ + { path: [["Realm"]], value: { entityVariant: { tag: "view" } } }, + ], + }), + ), + ).toThrow("is not a table") + }) +}) diff --git a/examples/lab/tests/snapshot.unit.ts b/examples/lab/tests/snapshot.unit.ts new file mode 100644 index 00000000..3344e200 --- /dev/null +++ b/examples/lab/tests/snapshot.unit.ts @@ -0,0 +1,41 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import { describe, expect, it } from "vitest" +import { snapshot } from "../src/tools/editor/snapshot.ts" + +describe("snapshot", () => { + it("copies plain values and marks cycles", () => { + const value: Record = { answer: 42 } + value.self = value + expect(snapshot(value)).toEqual({ answer: 42, self: { $type: "circular" } }) + }) + + it("does not invoke accessors or retain class instances", () => { + let accessed = false + const value = Object.defineProperty({}, "secret", { + enumerable: true, + get() { + accessed = true + return 1 + }, + }) + expect(snapshot(value)).toEqual({ secret: { $type: "accessor" } }) + expect(accessed).toBe(false) + expect(snapshot(new Date(0))).toEqual({ $type: "instance", value: "Date" }) + }) + + it("contains objects that throw during inspection", () => { + const { proxy, revoke } = Proxy.revocable({}, {}) + revoke() + expect(snapshot(proxy)).toEqual({ $type: "uninspectable" }) + }) + + it("preserves an own __proto__ property", () => { + const value = Object.create(null) as Record + value.__proto__ = "data" + const result = snapshot(value) + expect(Object.prototype.hasOwnProperty.call(result, "__proto__")).toBe(true) + expect((result as Record).__proto__).toBe("data") + }) +}) diff --git a/examples/lab/tests/source-storage.unit.ts b/examples/lab/tests/source-storage.unit.ts new file mode 100644 index 00000000..a22ba2f0 --- /dev/null +++ b/examples/lab/tests/source-storage.unit.ts @@ -0,0 +1,31 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest" +import { + loadSource, + saveSource, + starterSource, +} from "../src/tools/editor/source-storage.ts" + +const values = new Map() + +beforeEach(() => { + values.clear() + vi.stubGlobal("localStorage", { + getItem: (key: string) => values.get(key) ?? null, + setItem: (key: string, value: string) => values.set(key, value), + }) +}) + +afterEach(() => vi.unstubAllGlobals()) + +describe("source storage", () => { + it("stores source independently by document URL", () => { + expect(loadSource("coln:one")).toBe(starterSource) + saveSource("coln:one", "return 1") + saveSource("coln:two", "return 2") + expect(loadSource("coln:one")).toBe("return 1") + expect(loadSource("coln:two")).toBe("return 2") + }) +}) diff --git a/examples/lab/tsconfig.json b/examples/lab/tsconfig.json new file mode 100644 index 00000000..b53da86b --- /dev/null +++ b/examples/lab/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["DOM", "DOM.Iterable", "ES2022"], + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "module": "ESNext", + "moduleResolution": "Bundler", + "resolveJsonModule": true, + "allowImportingTsExtensions": true, + "isolatedModules": true, + "noEmit": true, + "types": ["vite/client", "node"] + }, + "include": [ + "src", + "tests", + "vite.config.ts", + "vitest.config.ts", + "playwright.config.ts" + ] +} diff --git a/examples/lab/vite.config.ts b/examples/lab/vite.config.ts new file mode 100644 index 00000000..e2d336f1 --- /dev/null +++ b/examples/lab/vite.config.ts @@ -0,0 +1,64 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import { svelte } from "@sveltejs/vite-plugin-svelte" +import tailwindcss from "@tailwindcss/vite" +import { copyFile, mkdir } from "node:fs/promises" +import { defineConfig, type Plugin } from "vite" +import wasm from "vite-plugin-wasm" + +function staticToolRoutes(): Plugin { + let outputDirectory = "" + return { + name: "coln-lab-static-routes", + configResolved(config) { + outputDirectory = config.build.outDir + }, + async closeBundle() { + await Promise.all( + ["compiler", "editor", "sync"].map(async (route) => { + const directory = `${outputDirectory}/${route}` + await mkdir(directory, { recursive: true }) + await copyFile( + `${outputDirectory}/index.html`, + `${directory}/index.html`, + ) + }), + ) + }, + } +} + +export default defineConfig({ + base: process.env.VITE_BASE || "/", + plugins: [tailwindcss(), wasm(), svelte(), staticToolRoutes()], + resolve: { + dedupe: [ + "svelte", + "@coln-project/runtime", + "@automerge/automerge", + "@automerge/automerge-codemirror", + "@automerge/automerge-repo", + "@automerge/automerge-subduction", + "@codemirror/state", + "@codemirror/view", + "codemirror", + ], + }, + optimizeDeps: { + include: [ + "@automerge/automerge-repo/slim", + "@automerge/automerge-codemirror", + "@codemirror/lang-javascript", + "@codemirror/state", + "@codemirror/view", + "codemirror", + "json-formatter-js", + ], + }, + build: { + emptyOutDir: true, + outDir: "../../_build/web/lab-app", + target: "esnext", + }, +}) diff --git a/examples/lab/vitest.config.ts b/examples/lab/vitest.config.ts new file mode 100644 index 00000000..224ba833 --- /dev/null +++ b/examples/lab/vitest.config.ts @@ -0,0 +1,20 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import { svelte } from "@sveltejs/vite-plugin-svelte" +import { playwright } from "@vitest/browser-playwright" +import { defineConfig } from "vitest/config" + +export default defineConfig({ + plugins: [svelte()], + resolve: { conditions: ["browser"], dedupe: ["svelte"] }, + test: { + browser: { + enabled: true, + headless: true, + provider: playwright(), + instances: [{ browser: "chromium" }], + }, + include: ["tests/**/*.unit.ts"], + }, +}) diff --git a/examples/patchwork/.gitignore b/examples/patchwork/.gitignore new file mode 100644 index 00000000..b13e28e8 --- /dev/null +++ b/examples/patchwork/.gitignore @@ -0,0 +1,4 @@ +dist +node_modules +.pushwork +src/theory/compiler-dist diff --git a/examples/patchwork/.pushworkattributes b/examples/patchwork/.pushworkattributes new file mode 100644 index 00000000..5bc67716 --- /dev/null +++ b/examples/patchwork/.pushworkattributes @@ -0,0 +1,5 @@ +# The Haskell compiler is a multi-megabyte immutable build product: store it as +# an opaque blob rather than a merge-able CRDT document. Same for the JSFFI glue +# GHC generates beside it, which is machine-written and never merged. +dist/**/*.wasm artifact +dist/**/ghc_wasm_jsffi*.js artifact diff --git a/examples/patchwork/.pushworkignore b/examples/patchwork/.pushworkignore new file mode 100644 index 00000000..4583ad16 --- /dev/null +++ b/examples/patchwork/.pushworkignore @@ -0,0 +1,5 @@ +# Tests, the browser probe and the lockfile are for building and checking the +# tool, not for running it. +tests +probe +pnpm-lock.yaml diff --git a/examples/patchwork/README.md b/examples/patchwork/README.md new file mode 100644 index 00000000..180e42d3 --- /dev/null +++ b/examples/patchwork/README.md @@ -0,0 +1,150 @@ + + +# Coln in Patchwork + +Coln Lab's three tools, as [Patchwork](https://github.com/inkandswitch/patchwork) +plugins: a Theory Editor, a Store Editor and the Graph Demo. One package, six +plugins — a datatype and a tool for each. + +| Tool | Patchwork document | Coln store | +| --- | --- | --- | +| Theory Editor (`coln-theory`) | the theory itself: title, source text, the stores compiled from it | none | +| Store Editor (`coln-store`) | a pointer: title + `coln:` url | the store it points at | +| Graph Demo (`coln-graph`) | a pointer: title + `coln:` url | a store using `src/graph/graph.coln` | + +## Two repos, on purpose + +A theory is an ordinary Patchwork document. `source` is collaborative text +edited through `@automerge/automerge-codemirror`, so a theory syncs, forks, +versions and takes comments like anything else in Patchwork, and the Theory +Editor never touches Coln's repo until you ask it to make a store. + +A Coln store cannot be a Patchwork document. It is not automerge data at all: +it is a Rust store whose commits ride the Subduction sedimentree, registered +with automerge-repo as a custom document type — `defineDocumentType`, +`urlScheme: "coln"`. Neither of those exists in any published automerge-repo +(checked as far as `2.6.0-subduction.48`); they are additions in the fork Coln +depends on. So `window.repo` cannot open a `coln:` url, and this package +bundles the fork and stands up a second `Repo` of its own, pointed at Coln's +relay (`src/coln/repo.ts`). + +What the two repos *share* is the wasm underneath. Patchwork initialises +automerge and Subduction on the main thread at exactly the versions Coln pins — +automerge 3.3.2, subduction 0.16.1 — and `vite.config.ts` keeps both external, +so Coln's repo reuses those instances rather than loading its own. What is +bundled is the fork's JavaScript, the Coln store runtime (`runtime.wasm`, about +1MB) and the Haskell compiler (`coln.wasm`, about 4.4MB). + +Documents never cross between the two repos. A Patchwork handle stays on the +Patchwork side and a store handle stays on the Coln side; `src/patchwork.ts` +types the host boundary by hand so the compiler enforces that. + +### The pointer documents + +Patchwork addresses documents by `automerge:` url, so each store gets a +one-field Patchwork document — `{ title, storeUrl, realmName? }` — to give it +an identity Patchwork can list, name, link and share while the rows live in the +Coln repo. Creating a store in the Theory Editor makes both: the store, flushed +to the relay before it is linked, and its pointer. Theories written by Coln Lab +have no pointer for their stores; the Theory Editor makes one the first time you +open such a store. + +A pointer made from a theory also records that theory's url, so the Store +Editor offers the trip back — Coln Lab did that with a `?theory=` query +parameter, which Patchwork has no place for. + +Copying a pointer does not fork the store behind it. Both copies point at the +same Coln store, which is shared by url. + +## Build + +The compiler and the store runtime are built out of this repository, so from +the repository root: + +```console +just examples/build-web-patchwork # compiler wasm (if needed), then the bundle +just examples/check-web-patchwork # tests and typecheck +``` + +or, once `@coln-project/runtime`, `@coln-project/repo` and the compiler are +built (see `.github/workflows/web-demos.yml` for that sequence): + +```console +pnpm --dir examples/patchwork install +pnpm --dir examples/patchwork build # stages the compiler, then vite +pnpm --dir examples/patchwork test +pnpm --dir examples/patchwork check +``` + +Installing needs pnpm 11 (`nix develop .#web` has it); pnpm 10 cannot prepare +the git-hosted automerge-repo fork. + +Publishing into a Patchwork instance is `pushwork sync` from this directory +(`pnpm push` builds first). `.pushworkattributes` marks `coln.wasm` as an +`artifact` so it is stored as an immutable blob rather than a CRDT document, +and `.pushworkignore` keeps the staging copy of the compiler out of the sync. + +## Two knobs, per browser + +Both are read once, so changing either takes a reload. + +```js +// Which Subduction relay Coln stores sync through. +// Default: wss://coln.sync.inkandswitch.com +localStorage["coln-patchwork:sync-endpoint"] = "ws://127.0.0.1:3030" + +// Where the Theory Editor loads the compiler from. +// Default: beside this bundle, as shipped. +localStorage["coln-patchwork:compiler-dist"] = "https://example.github.io/coln/dist/" +``` + +The endpoint override is how you point these tools at a local relay +(`pnpm --dir examples/lab server`). The compiler override loads the wasm from a +deployed Coln Lab instead of shipping it inside the module. + +## What Coln Lab has that this does not + +- **The router, the home page and the nav.** Patchwork supplies navigation and + the document url; a tool receives its handle and renders. +- **Theory sync status.** Patchwork owns syncing for theories. Coln stores keep + their own indicator (`src/coln/sync.svelte.ts`), since Patchwork's knows + nothing about Coln's relay. +- **The document-load error pages.** Patchwork opens the document; what remains + is a store failing to load, reported inline. +- **`paneforge` and Tailwind.** Panes are CSS grid, and the styling derives + every colour from the host's `--editor-*` / `--studio-*` theme instead of + Coln Lab's fixed dark palette — Tailwind's preflight would have reset + Patchwork's own chrome, since tools render into the light DOM. +- **The CDN import.** GHC's `loadHaskellWasm.js` fetches its WASI shim from + jsdelivr; `src/theory/compiler.ts` does that job with the shim bundled. + +Carried over unchanged: the compiler driver, the IR reader, the store schema +reader, the value formatter, the REPL sandbox and snapshotter, the graph +projection, and the generated `GraphRealm` bindings. + +## Tests + +`pnpm test` runs two projects. `unit` covers the browser-shaped code under +browser export conditions. `store` stands up a real Coln store — forked repo, +store runtime, generated bindings, `graph.ts` — and checks that vertices and +directed edges come back out; it runs under Node, where each wasm module can +initialise itself (`tests/setup-wasm.ts`). + +`node probe/compiler-probe.mjs` (after a build) loads the **built** compiler +chunk in headless Chrome and compiles a theory with it. That is the one part of +this package a unit test cannot reach, and it is where the port's first bug +lived: the compiler resolved its two files against `import.meta.url`, which +points at `dist/assets/` for a chunk, while the files were staged to `dist/`. +Both are now `?url` imports, so vite emits them beside the chunk and writes the +urls. Last run: + +``` +import chunk 25.2 ms +loadCompiler 83.9 ms ← fetch + instantiate 4.4MB of wasm +compile graph theory 16.0 ms 0 diagnostics, 1 realm +compile broken theory 5.5 ms 1 diagnostic (E0314) +recompile graph theory 2.3 ms identical IR +``` diff --git a/examples/patchwork/package.json b/examples/patchwork/package.json new file mode 100644 index 00000000..34649f11 --- /dev/null +++ b/examples/patchwork/package.json @@ -0,0 +1,45 @@ +{ + "name": "coln-patchwork", + "private": true, + "license": "(Apache-2.0 OR MIT)", + "type": "module", + "main": "./dist/index.js", + "exports": { + ".": "./dist/index.js" + }, + "scripts": { + "stage": "node scripts/stage-compiler.mjs", + "build": "pnpm run stage && vite build", + "check": "svelte-check --tsconfig ./tsconfig.json", + "push": "pnpm run build && pushwork sync", + "test": "vitest run", + "test:watch": "vitest" + }, + "dependencies": { + "@bjorn3/browser_wasi_shim": "^0.3.0", + "@automerge/automerge-codemirror": "0.2.0", + "@automerge/automerge-repo": "github:onsetsoftware/automerge-repo#test/custom-url-scemes&path:packages/automerge-repo", + "@automerge/automerge-repo-storage-indexeddb": "2.6.0-subduction.47", + "@codemirror/lang-javascript": "^6.2.5", + "@codemirror/state": "^6.7.2", + "@codemirror/view": "^6.43.11", + "@coln-project/repo": "link:../../packages/coln-repo", + "@coln-project/runtime": "link:../../packages/coln-js-runtime", + "codemirror": "^6.0.2", + "json-formatter-js": "^2.5.23", + "svelte": "^5.57.0" + }, + "devDependencies": { + "@automerge/automerge": "3.3.2", + "@automerge/automerge-subduction": "0.16.1", + "@inkandswitch/patchwork-bootloader": "^0.6.2", + "@sveltejs/vite-plugin-svelte": "^7.3.0", + "@types/node": "^26.4.1", + "happy-dom": "^15.11.7", + "svelte-check": "^4.7.6", + "typescript": "^6.0.3", + "vite": "^8.2.2", + "vite-plugin-wasm": "^3.6.0", + "vitest": "^4.1.11" + } +} diff --git a/examples/patchwork/pnpm-lock.yaml b/examples/patchwork/pnpm-lock.yaml new file mode 100644 index 00000000..4e7ff0a7 --- /dev/null +++ b/examples/patchwork/pnpm-lock.yaml @@ -0,0 +1,1680 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +overrides: + '@automerge/automerge': 3.3.2 + '@automerge/automerge-subduction': 0.16.1 + +importers: + + .: + dependencies: + '@automerge/automerge-codemirror': + specifier: 0.2.0 + version: 0.2.0 + '@automerge/automerge-repo': + specifier: github:onsetsoftware/automerge-repo#test/custom-url-scemes&path:packages/automerge-repo + version: https://codeload.github.com/onsetsoftware/automerge-repo/tar.gz/b39d94c5624a3497150e08ba18546f29222289d8#path:packages/automerge-repo + '@automerge/automerge-repo-storage-indexeddb': + specifier: 2.6.0-subduction.47 + version: 2.6.0-subduction.47 + '@bjorn3/browser_wasi_shim': + specifier: ^0.3.0 + version: 0.3.0 + '@codemirror/lang-javascript': + specifier: ^6.2.5 + version: 6.2.5 + '@codemirror/state': + specifier: ^6.7.2 + version: 6.7.4 + '@codemirror/view': + specifier: ^6.43.11 + version: 6.43.11 + '@coln-project/repo': + specifier: link:../../packages/coln-repo + version: link:../../packages/coln-repo + '@coln-project/runtime': + specifier: link:../../packages/coln-js-runtime + version: link:../../packages/coln-js-runtime + codemirror: + specifier: ^6.0.2 + version: 6.0.2 + json-formatter-js: + specifier: ^2.5.23 + version: 2.5.23 + svelte: + specifier: ^5.57.0 + version: 5.57.0 + devDependencies: + '@automerge/automerge': + specifier: 3.3.2 + version: 3.3.2 + '@automerge/automerge-subduction': + specifier: 0.16.1 + version: 0.16.1 + '@inkandswitch/patchwork-bootloader': + specifier: ^0.6.2 + version: 0.6.2(@automerge/automerge-repo@https://codeload.github.com/onsetsoftware/automerge-repo/tar.gz/b39d94c5624a3497150e08ba18546f29222289d8#path:packages/automerge-repo)(@automerge/automerge@3.3.2) + '@sveltejs/vite-plugin-svelte': + specifier: ^7.3.0 + version: 7.3.0(svelte@5.57.0)(vite@8.2.2(@types/node@26.4.1)) + '@types/node': + specifier: ^26.4.1 + version: 26.4.1 + happy-dom: + specifier: ^15.11.7 + version: 15.11.7 + svelte-check: + specifier: ^4.7.6 + version: 4.7.6(picomatch@4.0.7)(svelte@5.57.0)(typescript@6.0.3) + typescript: + specifier: ^6.0.3 + version: 6.0.3 + vite: + specifier: ^8.2.2 + version: 8.2.2(@types/node@26.4.1) + vite-plugin-wasm: + specifier: ^3.6.0 + version: 3.6.0(vite@8.2.2(@types/node@26.4.1)) + vitest: + specifier: ^4.1.11 + version: 4.1.11(@types/node@26.4.1)(happy-dom@15.11.7)(vite@8.2.2(@types/node@26.4.1)) + +packages: + + '@automerge/automerge-codemirror@0.2.0': + resolution: {integrity: sha512-GBNh4ESZHFNHZKLznf/2G2KdmBD+unEdk64+l621LsjIsJs6HBmQmO8RCY1v7vIq31kFyoVHNcBMGQOnLgy4GA==} + + '@automerge/automerge-repo-network-messagechannel@2.6.0-subduction.47': + resolution: {integrity: sha512-HT8F4eYwggDsCtWp/WzSzzjRlglR3jqYxsDalKoyu7eKIaD/RuP8emvzqY+OolkDkI0Uau+LizOshvcbhrBRGQ==} + engines: {node: '>=22.13'} + + '@automerge/automerge-repo-network-websocket@2.6.0-subduction.47': + resolution: {integrity: sha512-A8aDy9jizU+6Pb5F6GSqSFNHAAXICib7uV60KHytEZjANL+PU2lMTpAhQnOG15NfJXX3mKXIsQ/duOZIsCRcZA==} + engines: {node: '>=22.13'} + + '@automerge/automerge-repo-storage-indexeddb@2.6.0-subduction.47': + resolution: {integrity: sha512-fYn6nse7jQnVb9EploTsBuYUzW7xFin2HZLneSEQ7w4pRCilIU92ghZEjZSWRKEPunSN0MYEvfJJJdlLyYTdkQ==} + engines: {node: '>=22.13'} + + '@automerge/automerge-repo@2.6.0-subduction.47': + resolution: {integrity: sha512-NGBQUjGH67Kyrc8a6zoafvDKT1+pnFi2/yiMgULS7l+apWQT2QkgQZ3vQGms/ngdrmM6ye1fhcwHwBn/DGJ6tA==} + engines: {node: '>=22.13'} + + '@automerge/automerge-repo@https://codeload.github.com/onsetsoftware/automerge-repo/tar.gz/b39d94c5624a3497150e08ba18546f29222289d8#path:packages/automerge-repo': + resolution: {gitHosted: true, path: packages/automerge-repo, tarball: https://codeload.github.com/onsetsoftware/automerge-repo/tar.gz/b39d94c5624a3497150e08ba18546f29222289d8} + version: 2.6.0-subduction.47 + engines: {node: '>=22.13'} + + '@automerge/automerge-subduction@0.16.1': + resolution: {integrity: sha512-alH7U4eYn0O7sT6hNLv7CJhNODJi+QD6qfRQTNfZCxL3Q5JZdJ8lFpdvXUkBf+I3bu0GLvVlTaYPWGrBihjpIw==} + + '@automerge/automerge@3.3.2': + resolution: {integrity: sha512-9vCdCL7pdQwUra66SBxPVHr+/t9epXKni9KDeak2rNBFMzABVh2u6gSpcwxi3jkR5qr047jVqZv9DlrOfVxLFw==} + + '@bjorn3/browser_wasi_shim@0.3.0': + resolution: {integrity: sha512-FlRBYttPRLcWORzBe6g8nmYTafBkOEFeOqMYM4tAHJzFsQy4+xJA94z85a9BCs8S+Uzfh9LrkpII7DXr2iUVFg==} + + '@cbor-extract/cbor-extract-darwin-arm64@2.2.2': + resolution: {integrity: sha512-ZKZ/F8US7JR92J4DMct6cLW/Y66o2K576+zjlEN/MevH70bFIsB10wkZEQPLzl2oNh2SMGy55xpJ9JoBRl5DOA==} + cpu: [arm64] + os: [darwin] + + '@cbor-extract/cbor-extract-darwin-x64@2.2.2': + resolution: {integrity: sha512-32b1mgc+P61Js+KW9VZv/c+xRw5EfmOcPx990JbCBSkYJFY0l25VinvyyWfl+3KjibQmAcYwmyzKF9J4DyKP/Q==} + cpu: [x64] + os: [darwin] + + '@cbor-extract/cbor-extract-linux-arm64@2.2.2': + resolution: {integrity: sha512-wfqgzqCAy/Vn8i6WVIh7qZd0DdBFaWBjPdB6ma+Wihcjv0gHqD/mw3ouVv7kbbUNrab6dKEx/w3xQZEdeXIlzg==} + cpu: [arm64] + os: [linux] + + '@cbor-extract/cbor-extract-linux-arm@2.2.2': + resolution: {integrity: sha512-tNg0za41TpQfkhWjptD+0gSD2fggMiDCSacuIeELyb2xZhr7PrhPe5h66Jc67B/5dmpIhI2QOUtv4SBsricyYQ==} + cpu: [arm] + os: [linux] + + '@cbor-extract/cbor-extract-linux-x64@2.2.2': + resolution: {integrity: sha512-rpiLnVEsqtPJ+mXTdx1rfz4RtUGYIUg2rUAZgd1KjiC1SehYUSkJN7Yh+aVfSjvCGtVP0/bfkQkXpPXKbmSUaA==} + cpu: [x64] + os: [linux] + + '@cbor-extract/cbor-extract-win32-x64@2.2.2': + resolution: {integrity: sha512-dI+9P7cfWxkTQ+oE+7Aa6onEn92PHgfWXZivjNheCRmTBDBf2fx6RyTi0cmgpYLnD1KLZK9ZYrMxaPZ4oiXhGA==} + cpu: [x64] + os: [win32] + + '@codemirror/autocomplete@6.20.3': + resolution: {integrity: sha512-tlosUqb+3BbxCxZdu4tKeRghPFC+QM7q4X5YhKV2eCmPG+1r2F3f4AaSz5sCrFqUtX4Jh20VFTKecl16MgiV9g==} + + '@codemirror/commands@6.11.0': + resolution: {integrity: sha512-/K4Rl5BN0OtTiPWmJCdqODu38XnDMsDxKY5rgrPnCkutPTJf2wVbkoixLfealF5Kwse/s8P8M5jAiURiwSwnFA==} + + '@codemirror/lang-javascript@6.2.5': + resolution: {integrity: sha512-zD4e5mS+50htS7F+TYjBPsiIFGanfVqg4HyUz6WNFikgOPf2BgKlx+TQedI1w6n/IqRBVBbBWmGFdLB/7uxO4A==} + + '@codemirror/language@6.12.4': + resolution: {integrity: sha512-1q4PaT+o6PbgpkJt4Q8Fv5XJxTy4FUZ4MWETtyiDw3J0Pyr9E2vqcKL+k9wcvjNTIsauxvE7OfmWj3FRPHQ76A==} + + '@codemirror/lint@6.9.7': + resolution: {integrity: sha512-28/+iWLYxKxsvGYhSYL7zaCZqLz5+FFFDq9tVsvGv9kv8RY4fFAchJ5WX9M3YrrRlTIsECjsXPqeNgnSmNP2dg==} + + '@codemirror/search@6.7.2': + resolution: {integrity: sha512-gUYkYhT2+n/+VGZ+8EzE5WFkYZUZYm1VOKDudIsNqh42uRVQJ0a6Yss9sdKT3MeOYfuL1N6AZA57oza0Oyr0LA==} + + '@codemirror/state@6.7.4': + resolution: {integrity: sha512-QhQIVRY+xHZDxwOSFrJ1eUMapJBUID3IdeAjf7dHO7zBUzSkyooHiodnalz5MG3iHzwixKMlAAyn7244y537EA==} + + '@codemirror/view@6.43.11': + resolution: {integrity: sha512-2+esucbQX6wB2JYi1eDvdCPFTA31BN8oSy6xCmk3G6CloV11yOvEjYk+gH7kLrP0MuHG94E8WDhjs5oMiu3+Wg==} + + '@inkandswitch/patchwork-bootloader@0.6.2': + resolution: {integrity: sha512-NSZtBWsVNI87q/KJ9+uwrI8EdE7H5dUVy007uvxNWY5o9OVkWUUxeKipO9Q0igs0yUoCh/swri46sRZncPf/dw==} + version: 0.6.2 + peerDependencies: + '@automerge/automerge': 3.3.2 + '@automerge/automerge-repo': 2.6.0-subduction.47 + '@automerge/automerge-repo-keyhive': 0.3.0-alpha.sub.8b + '@automerge/vanillajs': 2.6.0-subduction.47 + peerDependenciesMeta: + '@automerge/automerge': + optional: true + '@automerge/automerge-repo': + optional: true + '@automerge/automerge-repo-keyhive': + optional: true + '@automerge/vanillajs': + optional: true + + '@inkandswitch/patchwork-elements@6.0.1': + resolution: {integrity: sha512-m8mfkS5+lietd8U64Jn8dA6X42EBlwWyREg4O1WAxUUTrLyNZopsCQXykpaVkaF/AGFBSRW3sIPB+dVenSB1HA==} + peerDependencies: + '@automerge/automerge-repo': '*' + '@inkandswitch/patchwork-filesystem': ^0.2.6 + '@inkandswitch/patchwork-plugins': ^1.2.1 + '@inkandswitch/patchwork-providers': ^0.5.0 + '@types/react': '*' + react: '*' + solid-js: '*' + peerDependenciesMeta: + '@types/react': + optional: true + react: + optional: true + solid-js: + optional: true + + '@inkandswitch/patchwork-filesystem@0.2.7': + resolution: {integrity: sha512-8tyhW6Y2c5pJurfUfBioPTLwxC8j1Ye4cVQeQyhzeh8iqriklBCQRd5kacWU8tzDw5ZXcd0e/mbRjI4WxkGB2g==} + version: 0.2.7 + peerDependencies: + '@automerge/automerge': 3.3.2 + '@automerge/automerge-repo': '*' + + '@inkandswitch/patchwork-plugins@1.2.2': + resolution: {integrity: sha512-AGVYS1Sd937YV/R/C/gGd615MOHsk/uPijIa/ONYGKFOESqwis9XrPXVd3fMTnt/8dsgtYoMEXfcpk5eLLL/bQ==} + version: 1.2.2 + peerDependencies: + '@automerge/automerge': 3.3.2 + '@automerge/automerge-repo': '*' + '@inkandswitch/patchwork-filesystem': ^0.2.7 + + '@inkandswitch/patchwork-providers@0.5.1': + resolution: {integrity: sha512-KOZghTF4CVSK0jH9F/4JHtiACJsyQRluVCtfKrbwQPxQKhnuHIsbQHyBQu/Bsx9QMDFGdGcX7rs9jBj7qiQ6WQ==} + version: 0.5.1 + peerDependencies: + '@automerge/automerge-repo': '*' + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.6.0': + resolution: {integrity: sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@keyhive/keyhive@0.1.0-alpha.5': + resolution: {integrity: sha512-RoFimLwO91OR4/794pVT0SyTA4Gn3Jk9KL64ONrLe1UYLBdIPhrqeeQgY6i0FBcS1/zSeIFeK3AJZ8GzCRTRmA==} + + '@lezer/common@1.5.2': + resolution: {integrity: sha512-sxQE460fPZyU3sdc8lafxiPwJHBzZRy/udNFynGQky1SePYBdhkBl1kOagA9uT3pxR8K09bOrmTUqA9wb/PjSQ==} + + '@lezer/highlight@1.2.3': + resolution: {integrity: sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==} + + '@lezer/javascript@1.5.4': + resolution: {integrity: sha512-vvYx3MhWqeZtGPwDStM2dwgljd5smolYD2lR2UyFcHfxbBQebqx8yjmFmxtJ/E6nN6u1D9srOiVWm3Rb4tmcUA==} + + '@lezer/lr@1.4.10': + resolution: {integrity: sha512-rnCpTIBafOx4mRp43xOxDJbFipJm/c0cia/V5TiGlhmMa+wsSdoGmUN3w5Bqrks/09Q/D4tNAmWaT8p6NRi77A==} + + '@marijn/find-cluster-break@1.0.4': + resolution: {integrity: sha512-Wy0V7+SGUjnF9/TkiM1hKVDPj7jKXduPNboMVtHTA8dySMURWqfg/JZ9E2Sq8JgSJmkl7k7Qe9FLeMSrSraWmQ==} + + '@noble/hashes@1.8.0': + resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==} + engines: {node: ^14.21.3 || >=16} + + '@oxc-project/types@0.148.0': + resolution: {integrity: sha512-Nm4s/jB+4FpFsPhWGEC4h7rzksesmtnMXomo6rCMcg/b8zLQuOziRgkCS1fxDCXOlJB/6Q8oABOZ/OP6RIPj9A==} + + '@rolldown/binding-android-arm-eabi@1.2.7': + resolution: {integrity: sha512-EypzgnYCwyVY4NDHKzGmNJT5b+XaQEBniHxsMdeIQLB/tcCzZnhqrzHpZFbX9iaxx+5RiB8caATBtfvZP7zVxQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] + + '@rolldown/binding-android-arm64@1.2.7': + resolution: {integrity: sha512-l17HE9EweWaqJZhuUuNBN/FzM62xw+DECVnJyvMsxn8vJFAGLy5QfLDoYAcronkAN8VxKZHezDpulHDPx95vFw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.2.7': + resolution: {integrity: sha512-8ED8ELFvHXc6OCETIn4gXObPiaR6bckM/ipXtbzlPVDRMBfEGjCKgO90F9YtfdpDatVx/ZQw7aZ1vUMf/+T3Mw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.2.7': + resolution: {integrity: sha512-/WPripjtiAIZ2tWY7ddijORT0Ujg87wxWW/qcoFVCKAWVDPhtY0xr7Dj0M3GyNGz60jGwTElhro/mkF9dT7dDQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.2.7': + resolution: {integrity: sha512-14DI4NcqpvbICxSnGLx3PmtDaWqRP/KGSGb6C+JLLVPeZRl6dKdHba3pGsqT3vpdTqhEYIPG0MMQ8c0xYqoJxA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.2.7': + resolution: {integrity: sha512-bxrWIRvHWQvbJwi+VIie/kDJmQxcNE6xxWwZdqF/ExVAigtHkv54WTLQPb+QsZdnFy18fg7JPfWGL0RH6vwIlQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.2.7': + resolution: {integrity: sha512-toOY2BChBZyuxU7OYX6Tn389di4IzAqPTycVcci0O7FSfBqzRB3RZn+K5Is6ANf4tmgRd/K1yZTsNTXbkXsnLg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-arm64-musl@1.2.7': + resolution: {integrity: sha512-lAIXTH/aiLRLxsTgQvfhjo4K1ydWIp00+V0voOr9beb/9ZmkUFrSIb03dXNFRgMNvkE6oGsF10ioQ6UsI+vS5Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rolldown/binding-linux-ppc64-gnu@1.2.7': + resolution: {integrity: sha512-kdnwS28Pkenp/mZMRwjXXXwxQ7pIsm+bF919LUK93BOyhcLsrVKdP2p9fxpiPNPAbNuch8ypQt0pm2P2LYCAGg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-s390x-gnu@1.2.7': + resolution: {integrity: sha512-516OdsyLdr5E65paF3yBF55t8mfm9+gmtCsK3xI7XKXIT7EfRlHhxL8K/NR6Hu8BWSgF5+1w74lTL0+nxcc8Qw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-gnu@1.2.7': + resolution: {integrity: sha512-r8/z8n7GFaYRln3xmP1Cxy0HH/HLM0uBUPkEuSVEfKGDA89M0FsZRZJRSwe/tJjRx+fpH/gjorfhB8tmEbSFLA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-musl@1.2.7': + resolution: {integrity: sha512-pAsE8iiDxUg1xBqdhrTfg45AVDVpirjz00sblEYClGNNcMnDb+e8beQgqIAw6LvauX/APvgxUnwrgun/YYGBhw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rolldown/binding-openharmony-arm64@1.2.7': + resolution: {integrity: sha512-lTcIYmmnQQA8Or/2DatS6oSqcdLHvendjS+zLu+FwgToynWMRSmQdpM65fTANJgIS4mjbMOo5KT2lnT9SAb96w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-win32-arm64-msvc@1.2.7': + resolution: {integrity: sha512-e3Gu3WxbNk/UqQhxqU7YIYO+9ZBvWNz3U+h/qRFosscMFzdRPbXYSaSWgSnklv2fz1TgzBTcti2z35c/7irsHw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.2.7': + resolution: {integrity: sha512-W/jg5qoRSqjsEv0+dZi4e687mcHqmVuU0P4fK6qS/xjetW2Gmc1W8j//z5nAeNcC8Ttm0hV46IjcYeuVwYhuiw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@rolldown/pluginutils@1.0.1': + resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} + + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + + '@sveltejs/acorn-typescript@1.0.13': + resolution: {integrity: sha512-wgKggnhZVL9Bfx1OaKKTrYY9BFRk6C8UAkQNUcIv1+llzYrIqy+RZm5HPKzn0NpEBvTVhTqB4kQyllZywsRBRQ==} + peerDependencies: + acorn: ^8.9.0 + + '@sveltejs/load-config@0.2.3': + resolution: {integrity: sha512-VT3qmUb8pRV2QrZjd8iAmtg8lf4W0TIjZbvXtz5MKei/q96teWZgGJyyidJzOjzZzvdq616eSRVeMYIQChUTAQ==} + engines: {node: '>= 18.0.0'} + + '@sveltejs/vite-plugin-svelte@7.3.0': + resolution: {integrity: sha512-QbRoJyD92e9R0ufeQIWRHrCC0ObcqSv/aBDdrQMoU+sypav3cDx5wytdQ6GLdXjEMO6xjrXGzfkUygng8JMv0A==} + engines: {node: ^20.19 || ^22.12 || >=24} + peerDependencies: + svelte: ^5.46.4 + vite: ^8.0.0-beta.7 || ^8.0.0 + + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + + '@types/debug@4.1.13': + resolution: {integrity: sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + + '@types/ms@2.1.0': + resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} + + '@types/node@20.19.43': + resolution: {integrity: sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==} + + '@types/node@26.4.1': + resolution: {integrity: sha512-k97ENvZWtvA6yqz5/FS6a7duDgOPEeOQOc2iKS/nY6mX6qJUKtLnWzQS+Xj6tXweyj6ZcTAK2Qecetnvi9nCLA==} + + '@types/serviceworker@0.0.153': + resolution: {integrity: sha512-/cg6dFEkNchJLyRCGo4Gb8mF200qr3xskM5dCPgbtK0OzXxcFcXa6BEBdyG7JksRsTrvCR+V6aFPncoOYAwYhQ==} + + '@vitest/expect@4.1.11': + resolution: {integrity: sha512-VX2x5vNJXET47KAFzwERI+KRMtTTCSWTfSMKsW7JsUsXV4psq++e3DvZpuTDOpHcxytiDs6p2nhVb2tVDiiUYw==} + + '@vitest/mocker@4.1.11': + resolution: {integrity: sha512-2XJVD55d1o5AZous5CCGKS74g/riOj9odEt2bQpCVZeblHyHdnMeFl4jl0XjU21stf4mbjUkew2eXQZt65g5CQ==} + peerDependencies: + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@4.1.11': + resolution: {integrity: sha512-yiZzPbGTS9Sr/JpFl8zHrcIkAofNbFV6k21vIgQN/cY/oxZeXhJv5sc/MBJ5jFKWmWs+oJHw0UXLZjmf931+Vw==} + + '@vitest/runner@4.1.11': + resolution: {integrity: sha512-LztvUgdwMNJMIkj3hQnnxiC2Xy1zNxq928W/xhjCLaNCzqTZOudjwbQf6v9IntZGPw132i2Lq2rgTRZHD3JHNw==} + + '@vitest/snapshot@4.1.11': + resolution: {integrity: sha512-pN7ikn1ON7h8ee4gIAp4AzyK+zBtJPzVbqOgu5LCEh4VaJVbPQcgYQYJIMGQPXVeJJq1fnfazis7a5pFNPahog==} + + '@vitest/spy@4.1.11': + resolution: {integrity: sha512-apNa/prQy2qCeywhnixOHPRCgGNhvg7T4Dapfl1GahLp/R+uhBm5cPyFoNVyqsNd2h1nJxL6BqqdIjiABL60YA==} + + '@vitest/utils@4.1.11': + resolution: {integrity: sha512-zTCVGpyFsGWBhllOyKlTw/vnr6D9qxsfSDyfbyZmTyjHw5N/VuvzHpHoQjm2ZJzn4RJgx5w4r7V0er69CmLgPQ==} + + acorn@8.18.0: + resolution: {integrity: sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==} + engines: {node: '>=0.4.0'} + hasBin: true + + aria-query@5.3.1: + resolution: {integrity: sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==} + engines: {node: '>= 0.4'} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + axobject-query@4.1.0: + resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} + engines: {node: '>= 0.4'} + + base-x@5.0.1: + resolution: {integrity: sha512-M7uio8Zt++eg3jPj+rHMfCC+IuygQHHCOU+IYsVtik6FWjuYpVt/+MRKcgsAMHh8mMFAwnB+Bs+mTrFiXjMzKg==} + + bs58@6.0.0: + resolution: {integrity: sha512-PD0wEnEYg6ijszw/u8s+iI3H17cTymlrwkKhDhPZq+Sokl3AU4htyBFTjAeNAlCCmg0f53g6ih3jATyCKftTfw==} + + bs58check@4.0.0: + resolution: {integrity: sha512-FsGDOnFg9aVI9erdriULkd/JjEWONV/lQE5aYziB5PoBsXRind56lh8doIZIc9X4HoxT5x4bLjMWN1/NB8Zp5g==} + + cbor-extract@2.2.2: + resolution: {integrity: sha512-hlSxxI9XO2yQfe9g6msd3g4xCfDqK5T5P0fRMLuaLHhxn4ViPrm+a+MUfhrvH2W962RGxcBwEGzLQyjbDG1gng==} + hasBin: true + + cbor-x@1.6.6: + resolution: {integrity: sha512-8QiD9PGOxyQHo7s2pzwTBH6lTjqekxPdl9Aq6fXvZgCuCJHOht1puDEA/fTr6mciB76c+M+Gi0qT2i1a4pm4Wg==} + + chai@6.2.2: + resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} + engines: {node: '>=18'} + + chokidar@4.0.3: + resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} + engines: {node: '>= 14.16.0'} + + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + + codemirror@6.0.2: + resolution: {integrity: sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + crelt@1.0.7: + resolution: {integrity: sha512-aK6BbWfhf4U/wCcLHKPJl/xa6VkVstRaPywWtMKGwuOLc/wZTyQYuoxgvZnNsBvv7Kg3YTBQYYBCggcviQczuA==} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + deepmerge@4.3.1: + resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} + engines: {node: '>=0.10.0'} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + devalue@5.9.2: + resolution: {integrity: sha512-po4PAY5c53tw5XMocSnf8A/5OHhbbUftpr93aEN6BBoAdntUmK7vu7wOATqvt7cXO7m1Cl4gMVn6p7n6n4mj0w==} + + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + + es-module-lexer@2.3.2: + resolution: {integrity: sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==} + + esm-env@1.2.2: + resolution: {integrity: sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==} + + esrap@2.3.7: + resolution: {integrity: sha512-n2nf7fZR3c9yXf0BPEuHuXqT+KW0SJVj4cN5FMEkpCZ3scLjOQWpiccyCxVzCC2q1wubTghuEGzngJY/7Ah0Ow==} + peerDependencies: + '@typescript-eslint/types': ^8.2.0 + peerDependenciesMeta: + '@typescript-eslint/types': + optional: true + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + eventemitter3@5.0.4: + resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==} + + expect-type@1.4.0: + resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==} + engines: {node: '>=12.0.0'} + + fast-sha256@1.3.0: + resolution: {integrity: sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + happy-dom@15.11.7: + resolution: {integrity: sha512-KyrFvnl+J9US63TEzwoiJOQzZBJY7KgBushJA8X61DMbNsH+2ONkDuLDnCnwUiPTF42tLoEmrPyoqbenVA5zrg==} + engines: {node: '>=18.0.0'} + + is-reference@3.0.3: + resolution: {integrity: sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==} + + isomorphic-ws@5.0.0: + resolution: {integrity: sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw==} + peerDependencies: + ws: '*' + + json-formatter-js@2.5.23: + resolution: {integrity: sha512-Cbm8wHXjo/C56aCePP1VuKvjxoMEmL7g7Ckss1oWFFlCsvOEEbye1kTeaNNaqba1Cl6YpIOYAnK65pUQ8mDIUQ==} + + lightningcss-android-arm64@1.33.0: + resolution: {integrity: sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.33.0: + resolution: {integrity: sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.33.0: + resolution: {integrity: sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.33.0: + resolution: {integrity: sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.33.0: + resolution: {integrity: sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.33.0: + resolution: {integrity: sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + lightningcss-linux-arm64-musl@1.33.0: + resolution: {integrity: sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + lightningcss-linux-x64-gnu@1.33.0: + resolution: {integrity: sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + lightningcss-linux-x64-musl@1.33.0: + resolution: {integrity: sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + lightningcss-win32-arm64-msvc@1.33.0: + resolution: {integrity: sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.33.0: + resolution: {integrity: sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.33.0: + resolution: {integrity: sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==} + engines: {node: '>= 12.0.0'} + + locate-character@3.0.0: + resolution: {integrity: sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + magic-string@1.2.3: + resolution: {integrity: sha512-Bpb0W2TbLKOZ7vJnOUnVRGq3WL2p+ISV29M6hYPL1AFCpyKZpdr5ytiXoTSSxRVhg8YW7f65+6gbG8WG6PCa/g==} + + mri@1.2.0: + resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} + engines: {node: '>=4'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.18: + resolution: {integrity: sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + node-gyp-build-optional-packages@5.1.1: + resolution: {integrity: sha512-+P72GAjVAbTxjjwUmwjVrqrdZROD4nf8KgpBoDxqXXTiYZZt/ud60dE5yvCSr9lRO8e8yv6kgJIC0K0PfZFVQw==} + hasBin: true + + obug@2.1.4: + resolution: {integrity: sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==} + engines: {node: '>=12.20.0'} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.7: + resolution: {integrity: sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==} + engines: {node: '>=12'} + + postcss@8.5.28: + resolution: {integrity: sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==} + engines: {node: ^10 || ^12 || >=14} + + readdirp@4.1.2: + resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} + engines: {node: '>= 14.18.0'} + + resolve.exports@2.0.3: + resolution: {integrity: sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==} + engines: {node: '>=10'} + + rolldown@1.2.7: + resolution: {integrity: sha512-g0EtLvBjTUB7jhyV0S/TCup3v/XSVl45vUIGbOGU4QPiyjTenCe4mKuFvW9fEgYmS2Fo42AUssRmNuMziXdrig==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + + sade@1.8.1: + resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==} + engines: {node: '>=6'} + + seroval-plugins@1.5.6: + resolution: {integrity: sha512-HXuLAX2pu/UByPpaeo/TaMfvMIi+1QqIoPJYCcAtU8QkVNwgR6MPlGuCQTErV1JwraaMbYaWVIBX7mppzGLATQ==} + engines: {node: '>=10'} + peerDependencies: + seroval: ^1.0 + + seroval@1.5.6: + resolution: {integrity: sha512-rVQVWjjSvlINzaQPZH5JFqsqEsIWdTxY3iJZCnTL/5gQbXIRooVZKI60tVCkOVfzcRPejboxO2t0P89dg5mQaA==} + engines: {node: '>=10'} + + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + solid-js@1.9.15: + resolution: {integrity: sha512-EeiY2xfpZJqPLjXspVEKjAII4yv8NyG//NxZ3IpOFHdUNnnTyL0uJOeS9LWGvA7cFCz5y94cjFwYlmw5Luncsg==} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + std-env@4.2.0: + resolution: {integrity: sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==} + + style-mod@4.1.3: + resolution: {integrity: sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==} + + svelte-check@4.7.6: + resolution: {integrity: sha512-t2scM//ZuVbSY/T2w6FSBw1v9s2NEmh/g+sy1lqtosW5ylBV5AF4wFb1Ts9Kf3MbfPDUDJDZ9L436YT0SPTdvw==} + engines: {node: '>= 18.0.0'} + hasBin: true + peerDependencies: + svelte: ^4.0.0 || ^5.0.0-next.0 + typescript: ^5.0.0 || ^6.0.0 + + svelte@5.57.0: + resolution: {integrity: sha512-NdbDn7fl4be1ViUG0oq/lvG6OZy3oENolV2ONjiqqsfVoeAfzaQAKUcEX3MrQod/Bebv1PgwET9rfXhgn9s4Kg==} + engines: {node: '>=18'} + + tinyargs@0.1.4: + resolution: {integrity: sha512-5OpdhMRRE70j0zT7mrBpx12FJI+y0EGx8zMe8Vl2/aiwGgpW3X70OVHlsHmHeeG0ncnnWdq1o+k/S5d9ONgzGA==} + engines: {node: '>=14'} + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@1.3.1: + resolution: {integrity: sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA==} + engines: {node: '>=18'} + + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + + tinyrainbow@3.1.1: + resolution: {integrity: sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==} + engines: {node: '>=14.0.0'} + + typescript@6.0.3: + resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + + undici-types@8.3.0: + resolution: {integrity: sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==} + + uuid@14.0.2: + resolution: {integrity: sha512-xZe/16rV4aa+HGSOCiY2YeLT1OybRLrrkL/Rqaq7p7GMVXjFh+6wN4oMYgjFmnSnhY8t6Xpdl2l9qmnHYuMHwQ==} + hasBin: true + + vite-plugin-wasm@3.6.0: + resolution: {integrity: sha512-mL/QPziiIA4RAA6DkaZZzOstdwbW5jO4Vz7Zenj0wieKWBlNvIvX5L5ljum9lcUX0ShNfBgCNLKTjNkRVVqcsw==} + peerDependencies: + vite: ^2 || ^3 || ^4 || ^5 || ^6 || ^7 || ^8 + + vite@8.2.2: + resolution: {integrity: sha512-cFKLV/PRgAUlIRm5WjMjJ86jrftzpqcgH+Us+DS8mI3CDNiH30Whrz8uHL3+MOLPAgqbMBAqWdAHAphOAM+z/Q==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + '@vitejs/devtools': ^0.4.0 || ^0.5.0 + esbuild: ^0.27.0 || ^0.28.0 + jiti: '>=1.21.0' + less: ^4.0.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + '@vitejs/devtools': + optional: true + esbuild: + optional: true + jiti: + optional: true + less: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitefu@1.1.3: + resolution: {integrity: sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg==} + peerDependencies: + vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + vite: + optional: true + + vitest@4.1.11: + resolution: {integrity: sha512-fhACrNXUidIbGSBr5FlbuBkO7VWC1ZyLl0DO4CU2DrQoAPxX84Ysxs+HeGQpii5lZWV1Q4gBZTTu49mF+A6Edw==} + engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@opentelemetry/api': ^1.9.0 + '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 4.1.11 + '@vitest/browser-preview': 4.1.11 + '@vitest/browser-webdriverio': 4.1.11 + '@vitest/coverage-istanbul': 4.1.11 + '@vitest/coverage-v8': 4.1.11 + '@vitest/ui': 4.1.11 + happy-dom: '*' + jsdom: '*' + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@opentelemetry/api': + optional: true + '@types/node': + optional: true + '@vitest/browser-playwright': + optional: true + '@vitest/browser-preview': + optional: true + '@vitest/browser-webdriverio': + optional: true + '@vitest/coverage-istanbul': + optional: true + '@vitest/coverage-v8': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + w3c-keyname@2.2.8: + resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==} + + webidl-conversions@7.0.0: + resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} + engines: {node: '>=12'} + + whatwg-mimetype@3.0.0: + resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==} + engines: {node: '>=12'} + + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + + ws@8.21.3: + resolution: {integrity: sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + xstate@5.32.6: + resolution: {integrity: sha512-WfA8WNrh6r9osuGwVm+aIPM4jmMW2LKHV1Yv9thYpOtL4HVF/kobh95K/O8v2jDCpDmP2EoY+6uvuqHXCZ6ZLw==} + + zimmerframe@1.1.5: + resolution: {integrity: sha512-msJxIvYDYcoNL+PJsu+7qmpDWsYmAxTY+2TNYXXF0hzBzBk0BMecOqDOG/EckUoKCuKwObfbugIl8QpqHDXeFA==} + +snapshots: + + '@automerge/automerge-codemirror@0.2.0': + dependencies: + '@automerge/automerge': 3.3.2 + '@codemirror/state': 6.7.4 + '@codemirror/view': 6.43.11 + codemirror: 6.0.2 + + '@automerge/automerge-repo-network-messagechannel@2.6.0-subduction.47': + dependencies: + '@automerge/automerge-repo': 2.6.0-subduction.47 + eventemitter3: 5.0.4 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + '@automerge/automerge-repo-network-websocket@2.6.0-subduction.47': + dependencies: + '@automerge/automerge-repo': 2.6.0-subduction.47 + cbor-x: 1.6.6 + debug: 4.4.3 + eventemitter3: 5.0.4 + ws: 8.21.3 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + '@automerge/automerge-repo-storage-indexeddb@2.6.0-subduction.47': + dependencies: + '@automerge/automerge-repo': 2.6.0-subduction.47 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + '@automerge/automerge-repo@2.6.0-subduction.47': + dependencies: + '@automerge/automerge': 3.3.2 + '@automerge/automerge-subduction': 0.16.1 + bs58check: 4.0.0 + cbor-x: 1.6.6 + debug: 4.4.3 + eventemitter3: 5.0.4 + fast-sha256: 1.3.0 + isomorphic-ws: 5.0.0(ws@8.21.3) + uuid: 14.0.2 + ws: 8.21.3 + xstate: 5.32.6 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + '@automerge/automerge-repo@https://codeload.github.com/onsetsoftware/automerge-repo/tar.gz/b39d94c5624a3497150e08ba18546f29222289d8#path:packages/automerge-repo': + dependencies: + '@automerge/automerge': 3.3.2 + '@automerge/automerge-subduction': 0.16.1 + bs58check: 4.0.0 + cbor-x: 1.6.6 + debug: 4.4.3 + eventemitter3: 5.0.4 + fast-sha256: 1.3.0 + isomorphic-ws: 5.0.0(ws@8.21.3) + uuid: 14.0.2 + ws: 8.21.3 + xstate: 5.32.6 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + '@automerge/automerge-subduction@0.16.1': {} + + '@automerge/automerge@3.3.2': {} + + '@bjorn3/browser_wasi_shim@0.3.0': {} + + '@cbor-extract/cbor-extract-darwin-arm64@2.2.2': + optional: true + + '@cbor-extract/cbor-extract-darwin-x64@2.2.2': + optional: true + + '@cbor-extract/cbor-extract-linux-arm64@2.2.2': + optional: true + + '@cbor-extract/cbor-extract-linux-arm@2.2.2': + optional: true + + '@cbor-extract/cbor-extract-linux-x64@2.2.2': + optional: true + + '@cbor-extract/cbor-extract-win32-x64@2.2.2': + optional: true + + '@codemirror/autocomplete@6.20.3': + dependencies: + '@codemirror/language': 6.12.4 + '@codemirror/state': 6.7.4 + '@codemirror/view': 6.43.11 + '@lezer/common': 1.5.2 + + '@codemirror/commands@6.11.0': + dependencies: + '@codemirror/language': 6.12.4 + '@codemirror/state': 6.7.4 + '@codemirror/view': 6.43.11 + '@lezer/common': 1.5.2 + + '@codemirror/lang-javascript@6.2.5': + dependencies: + '@codemirror/autocomplete': 6.20.3 + '@codemirror/language': 6.12.4 + '@codemirror/lint': 6.9.7 + '@codemirror/state': 6.7.4 + '@codemirror/view': 6.43.11 + '@lezer/common': 1.5.2 + '@lezer/javascript': 1.5.4 + + '@codemirror/language@6.12.4': + dependencies: + '@codemirror/state': 6.7.4 + '@codemirror/view': 6.43.11 + '@lezer/common': 1.5.2 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.10 + style-mod: 4.1.3 + + '@codemirror/lint@6.9.7': + dependencies: + '@codemirror/state': 6.7.4 + '@codemirror/view': 6.43.11 + crelt: 1.0.7 + + '@codemirror/search@6.7.2': + dependencies: + '@codemirror/state': 6.7.4 + '@codemirror/view': 6.43.11 + crelt: 1.0.7 + + '@codemirror/state@6.7.4': + dependencies: + '@marijn/find-cluster-break': 1.0.4 + + '@codemirror/view@6.43.11': + dependencies: + '@codemirror/state': 6.7.4 + crelt: 1.0.7 + style-mod: 4.1.3 + w3c-keyname: 2.2.8 + + '@inkandswitch/patchwork-bootloader@0.6.2(@automerge/automerge-repo@https://codeload.github.com/onsetsoftware/automerge-repo/tar.gz/b39d94c5624a3497150e08ba18546f29222289d8#path:packages/automerge-repo)(@automerge/automerge@3.3.2)': + dependencies: + '@automerge/automerge-repo-network-messagechannel': 2.6.0-subduction.47 + '@automerge/automerge-repo-network-websocket': 2.6.0-subduction.47 + '@automerge/automerge-repo-storage-indexeddb': 2.6.0-subduction.47 + '@automerge/automerge-subduction': 0.16.1 + '@codemirror/commands': 6.11.0 + '@codemirror/language': 6.12.4 + '@codemirror/state': 6.7.4 + '@codemirror/view': 6.43.11 + '@inkandswitch/patchwork-elements': 6.0.1(e5022d7ad3da4f3d4c234c88dc4cfb59) + '@inkandswitch/patchwork-filesystem': 0.2.7(@automerge/automerge-repo@https://codeload.github.com/onsetsoftware/automerge-repo/tar.gz/b39d94c5624a3497150e08ba18546f29222289d8#path:packages/automerge-repo)(@automerge/automerge@3.3.2) + '@inkandswitch/patchwork-plugins': 1.2.2(@automerge/automerge-repo@https://codeload.github.com/onsetsoftware/automerge-repo/tar.gz/b39d94c5624a3497150e08ba18546f29222289d8#path:packages/automerge-repo)(@automerge/automerge@3.3.2)(@inkandswitch/patchwork-filesystem@0.2.7(@automerge/automerge-repo@https://codeload.github.com/onsetsoftware/automerge-repo/tar.gz/b39d94c5624a3497150e08ba18546f29222289d8#path:packages/automerge-repo)(@automerge/automerge@3.3.2)) + '@inkandswitch/patchwork-providers': 0.5.1(@automerge/automerge-repo@https://codeload.github.com/onsetsoftware/automerge-repo/tar.gz/b39d94c5624a3497150e08ba18546f29222289d8#path:packages/automerge-repo) + '@keyhive/keyhive': 0.1.0-alpha.5 + '@types/debug': 4.1.13 + debug: 4.4.3 + resolve.exports: 2.0.3 + service-worker-types: '@types/serviceworker@0.0.153' + solid-js: 1.9.15 + tinyargs: 0.1.4 + optionalDependencies: + '@automerge/automerge': 3.3.2 + '@automerge/automerge-repo': https://codeload.github.com/onsetsoftware/automerge-repo/tar.gz/b39d94c5624a3497150e08ba18546f29222289d8#path:packages/automerge-repo + transitivePeerDependencies: + - '@types/react' + - bufferutil + - react + - supports-color + - utf-8-validate + + '@inkandswitch/patchwork-elements@6.0.1(e5022d7ad3da4f3d4c234c88dc4cfb59)': + dependencies: + '@automerge/automerge-repo': https://codeload.github.com/onsetsoftware/automerge-repo/tar.gz/b39d94c5624a3497150e08ba18546f29222289d8#path:packages/automerge-repo + '@inkandswitch/patchwork-filesystem': 0.2.7(@automerge/automerge-repo@https://codeload.github.com/onsetsoftware/automerge-repo/tar.gz/b39d94c5624a3497150e08ba18546f29222289d8#path:packages/automerge-repo)(@automerge/automerge@3.3.2) + '@inkandswitch/patchwork-plugins': 1.2.2(@automerge/automerge-repo@https://codeload.github.com/onsetsoftware/automerge-repo/tar.gz/b39d94c5624a3497150e08ba18546f29222289d8#path:packages/automerge-repo)(@automerge/automerge@3.3.2)(@inkandswitch/patchwork-filesystem@0.2.7(@automerge/automerge-repo@https://codeload.github.com/onsetsoftware/automerge-repo/tar.gz/b39d94c5624a3497150e08ba18546f29222289d8#path:packages/automerge-repo)(@automerge/automerge@3.3.2)) + '@inkandswitch/patchwork-providers': 0.5.1(@automerge/automerge-repo@https://codeload.github.com/onsetsoftware/automerge-repo/tar.gz/b39d94c5624a3497150e08ba18546f29222289d8#path:packages/automerge-repo) + debug: 4.4.3 + optionalDependencies: + solid-js: 1.9.15 + transitivePeerDependencies: + - supports-color + + '@inkandswitch/patchwork-filesystem@0.2.7(@automerge/automerge-repo@https://codeload.github.com/onsetsoftware/automerge-repo/tar.gz/b39d94c5624a3497150e08ba18546f29222289d8#path:packages/automerge-repo)(@automerge/automerge@3.3.2)': + dependencies: + '@automerge/automerge': 3.3.2 + '@automerge/automerge-repo': https://codeload.github.com/onsetsoftware/automerge-repo/tar.gz/b39d94c5624a3497150e08ba18546f29222289d8#path:packages/automerge-repo + debug: 4.4.3 + resolve.exports: 2.0.3 + transitivePeerDependencies: + - supports-color + + '@inkandswitch/patchwork-plugins@1.2.2(@automerge/automerge-repo@https://codeload.github.com/onsetsoftware/automerge-repo/tar.gz/b39d94c5624a3497150e08ba18546f29222289d8#path:packages/automerge-repo)(@automerge/automerge@3.3.2)(@inkandswitch/patchwork-filesystem@0.2.7(@automerge/automerge-repo@https://codeload.github.com/onsetsoftware/automerge-repo/tar.gz/b39d94c5624a3497150e08ba18546f29222289d8#path:packages/automerge-repo)(@automerge/automerge@3.3.2))': + dependencies: + '@automerge/automerge': 3.3.2 + '@automerge/automerge-repo': https://codeload.github.com/onsetsoftware/automerge-repo/tar.gz/b39d94c5624a3497150e08ba18546f29222289d8#path:packages/automerge-repo + '@inkandswitch/patchwork-filesystem': 0.2.7(@automerge/automerge-repo@https://codeload.github.com/onsetsoftware/automerge-repo/tar.gz/b39d94c5624a3497150e08ba18546f29222289d8#path:packages/automerge-repo)(@automerge/automerge@3.3.2) + '@types/debug': 4.1.13 + '@types/node': 20.19.43 + debug: 4.4.3 + eventemitter3: 5.0.4 + resolve.exports: 2.0.3 + transitivePeerDependencies: + - supports-color + + '@inkandswitch/patchwork-providers@0.5.1(@automerge/automerge-repo@https://codeload.github.com/onsetsoftware/automerge-repo/tar.gz/b39d94c5624a3497150e08ba18546f29222289d8#path:packages/automerge-repo)': + dependencies: + '@automerge/automerge-repo': https://codeload.github.com/onsetsoftware/automerge-repo/tar.gz/b39d94c5624a3497150e08ba18546f29222289d8#path:packages/automerge-repo + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.6.0 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.6.0': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.6.0 + + '@keyhive/keyhive@0.1.0-alpha.5': {} + + '@lezer/common@1.5.2': {} + + '@lezer/highlight@1.2.3': + dependencies: + '@lezer/common': 1.5.2 + + '@lezer/javascript@1.5.4': + dependencies: + '@lezer/common': 1.5.2 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.10 + + '@lezer/lr@1.4.10': + dependencies: + '@lezer/common': 1.5.2 + + '@marijn/find-cluster-break@1.0.4': {} + + '@noble/hashes@1.8.0': {} + + '@oxc-project/types@0.148.0': {} + + '@rolldown/binding-android-arm-eabi@1.2.7': + optional: true + + '@rolldown/binding-android-arm64@1.2.7': + optional: true + + '@rolldown/binding-darwin-arm64@1.2.7': + optional: true + + '@rolldown/binding-darwin-x64@1.2.7': + optional: true + + '@rolldown/binding-freebsd-x64@1.2.7': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.2.7': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.2.7': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.2.7': + optional: true + + '@rolldown/binding-linux-ppc64-gnu@1.2.7': + optional: true + + '@rolldown/binding-linux-s390x-gnu@1.2.7': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.2.7': + optional: true + + '@rolldown/binding-linux-x64-musl@1.2.7': + optional: true + + '@rolldown/binding-openharmony-arm64@1.2.7': + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.2.7': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.2.7': + optional: true + + '@rolldown/pluginutils@1.0.1': {} + + '@standard-schema/spec@1.1.0': {} + + '@sveltejs/acorn-typescript@1.0.13(acorn@8.18.0)': + dependencies: + acorn: 8.18.0 + + '@sveltejs/load-config@0.2.3': {} + + '@sveltejs/vite-plugin-svelte@7.3.0(svelte@5.57.0)(vite@8.2.2(@types/node@26.4.1))': + dependencies: + deepmerge: 4.3.1 + magic-string: 1.2.3 + obug: 2.1.4 + svelte: 5.57.0 + vite: 8.2.2(@types/node@26.4.1) + vitefu: 1.1.3(vite@8.2.2(@types/node@26.4.1)) + + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + + '@types/debug@4.1.13': + dependencies: + '@types/ms': 2.1.0 + + '@types/deep-eql@4.0.2': {} + + '@types/estree@1.0.9': {} + + '@types/ms@2.1.0': {} + + '@types/node@20.19.43': + dependencies: + undici-types: 6.21.0 + + '@types/node@26.4.1': + dependencies: + undici-types: 8.3.0 + + '@types/serviceworker@0.0.153': {} + + '@vitest/expect@4.1.11': + dependencies: + '@standard-schema/spec': 1.1.0 + '@types/chai': 5.2.3 + '@vitest/spy': 4.1.11 + '@vitest/utils': 4.1.11 + chai: 6.2.2 + tinyrainbow: 3.1.1 + + '@vitest/mocker@4.1.11(vite@8.2.2(@types/node@26.4.1))': + dependencies: + '@vitest/spy': 4.1.11 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 8.2.2(@types/node@26.4.1) + + '@vitest/pretty-format@4.1.11': + dependencies: + tinyrainbow: 3.1.1 + + '@vitest/runner@4.1.11': + dependencies: + '@vitest/utils': 4.1.11 + pathe: 2.0.3 + + '@vitest/snapshot@4.1.11': + dependencies: + '@vitest/pretty-format': 4.1.11 + '@vitest/utils': 4.1.11 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@4.1.11': {} + + '@vitest/utils@4.1.11': + dependencies: + '@vitest/pretty-format': 4.1.11 + convert-source-map: 2.0.0 + tinyrainbow: 3.1.1 + + acorn@8.18.0: {} + + aria-query@5.3.1: {} + + assertion-error@2.0.1: {} + + axobject-query@4.1.0: {} + + base-x@5.0.1: {} + + bs58@6.0.0: + dependencies: + base-x: 5.0.1 + + bs58check@4.0.0: + dependencies: + '@noble/hashes': 1.8.0 + bs58: 6.0.0 + + cbor-extract@2.2.2: + dependencies: + node-gyp-build-optional-packages: 5.1.1 + optionalDependencies: + '@cbor-extract/cbor-extract-darwin-arm64': 2.2.2 + '@cbor-extract/cbor-extract-darwin-x64': 2.2.2 + '@cbor-extract/cbor-extract-linux-arm': 2.2.2 + '@cbor-extract/cbor-extract-linux-arm64': 2.2.2 + '@cbor-extract/cbor-extract-linux-x64': 2.2.2 + '@cbor-extract/cbor-extract-win32-x64': 2.2.2 + optional: true + + cbor-x@1.6.6: + optionalDependencies: + cbor-extract: 2.2.2 + + chai@6.2.2: {} + + chokidar@4.0.3: + dependencies: + readdirp: 4.1.2 + + clsx@2.1.1: {} + + codemirror@6.0.2: + dependencies: + '@codemirror/autocomplete': 6.20.3 + '@codemirror/commands': 6.11.0 + '@codemirror/language': 6.12.4 + '@codemirror/lint': 6.9.7 + '@codemirror/search': 6.7.2 + '@codemirror/state': 6.7.4 + '@codemirror/view': 6.43.11 + + convert-source-map@2.0.0: {} + + crelt@1.0.7: {} + + csstype@3.2.3: {} + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + deepmerge@4.3.1: {} + + detect-libc@2.1.2: {} + + devalue@5.9.2: {} + + entities@4.5.0: {} + + es-module-lexer@2.3.2: {} + + esm-env@1.2.2: {} + + esrap@2.3.7: + dependencies: + '@jridgewell/sourcemap-codec': 1.6.0 + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.9 + + eventemitter3@5.0.4: {} + + expect-type@1.4.0: {} + + fast-sha256@1.3.0: {} + + fdir@6.5.0(picomatch@4.0.7): + optionalDependencies: + picomatch: 4.0.7 + + fsevents@2.3.3: + optional: true + + happy-dom@15.11.7: + dependencies: + entities: 4.5.0 + webidl-conversions: 7.0.0 + whatwg-mimetype: 3.0.0 + + is-reference@3.0.3: + dependencies: + '@types/estree': 1.0.9 + + isomorphic-ws@5.0.0(ws@8.21.3): + dependencies: + ws: 8.21.3 + + json-formatter-js@2.5.23: {} + + lightningcss-android-arm64@1.33.0: + optional: true + + lightningcss-darwin-arm64@1.33.0: + optional: true + + lightningcss-darwin-x64@1.33.0: + optional: true + + lightningcss-freebsd-x64@1.33.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.33.0: + optional: true + + lightningcss-linux-arm64-gnu@1.33.0: + optional: true + + lightningcss-linux-arm64-musl@1.33.0: + optional: true + + lightningcss-linux-x64-gnu@1.33.0: + optional: true + + lightningcss-linux-x64-musl@1.33.0: + optional: true + + lightningcss-win32-arm64-msvc@1.33.0: + optional: true + + lightningcss-win32-x64-msvc@1.33.0: + optional: true + + lightningcss@1.33.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.33.0 + lightningcss-darwin-arm64: 1.33.0 + lightningcss-darwin-x64: 1.33.0 + lightningcss-freebsd-x64: 1.33.0 + lightningcss-linux-arm-gnueabihf: 1.33.0 + lightningcss-linux-arm64-gnu: 1.33.0 + lightningcss-linux-arm64-musl: 1.33.0 + lightningcss-linux-x64-gnu: 1.33.0 + lightningcss-linux-x64-musl: 1.33.0 + lightningcss-win32-arm64-msvc: 1.33.0 + lightningcss-win32-x64-msvc: 1.33.0 + + locate-character@3.0.0: {} + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.6.0 + + magic-string@1.2.3: + dependencies: + '@jridgewell/sourcemap-codec': 1.6.0 + + mri@1.2.0: {} + + ms@2.1.3: {} + + nanoid@3.3.18: {} + + node-gyp-build-optional-packages@5.1.1: + dependencies: + detect-libc: 2.1.2 + optional: true + + obug@2.1.4: {} + + pathe@2.0.3: {} + + picocolors@1.1.1: {} + + picomatch@4.0.7: {} + + postcss@8.5.28: + dependencies: + nanoid: 3.3.18 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + readdirp@4.1.2: {} + + resolve.exports@2.0.3: {} + + rolldown@1.2.7: + dependencies: + '@oxc-project/types': 0.148.0 + '@rolldown/pluginutils': 1.0.1 + optionalDependencies: + '@rolldown/binding-android-arm-eabi': 1.2.7 + '@rolldown/binding-android-arm64': 1.2.7 + '@rolldown/binding-darwin-arm64': 1.2.7 + '@rolldown/binding-darwin-x64': 1.2.7 + '@rolldown/binding-freebsd-x64': 1.2.7 + '@rolldown/binding-linux-arm-gnueabihf': 1.2.7 + '@rolldown/binding-linux-arm64-gnu': 1.2.7 + '@rolldown/binding-linux-arm64-musl': 1.2.7 + '@rolldown/binding-linux-ppc64-gnu': 1.2.7 + '@rolldown/binding-linux-s390x-gnu': 1.2.7 + '@rolldown/binding-linux-x64-gnu': 1.2.7 + '@rolldown/binding-linux-x64-musl': 1.2.7 + '@rolldown/binding-openharmony-arm64': 1.2.7 + '@rolldown/binding-win32-arm64-msvc': 1.2.7 + '@rolldown/binding-win32-x64-msvc': 1.2.7 + + sade@1.8.1: + dependencies: + mri: 1.2.0 + + seroval-plugins@1.5.6(seroval@1.5.6): + dependencies: + seroval: 1.5.6 + + seroval@1.5.6: {} + + siginfo@2.0.0: {} + + solid-js@1.9.15: + dependencies: + csstype: 3.2.3 + seroval: 1.5.6 + seroval-plugins: 1.5.6(seroval@1.5.6) + + source-map-js@1.2.1: {} + + stackback@0.0.2: {} + + std-env@4.2.0: {} + + style-mod@4.1.3: {} + + svelte-check@4.7.6(picomatch@4.0.7)(svelte@5.57.0)(typescript@6.0.3): + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + '@sveltejs/load-config': 0.2.3 + chokidar: 4.0.3 + fdir: 6.5.0(picomatch@4.0.7) + picocolors: 1.1.1 + sade: 1.8.1 + svelte: 5.57.0 + typescript: 6.0.3 + transitivePeerDependencies: + - picomatch + + svelte@5.57.0: + dependencies: + '@jridgewell/remapping': 2.3.5 + '@jridgewell/sourcemap-codec': 1.6.0 + '@sveltejs/acorn-typescript': 1.0.13(acorn@8.18.0) + '@types/estree': 1.0.9 + acorn: 8.18.0 + aria-query: 5.3.1 + axobject-query: 4.1.0 + clsx: 2.1.1 + devalue: 5.9.2 + esm-env: 1.2.2 + esrap: 2.3.7 + is-reference: 3.0.3 + locate-character: 3.0.0 + magic-string: 0.30.21 + zimmerframe: 1.1.5 + transitivePeerDependencies: + - '@typescript-eslint/types' + + tinyargs@0.1.4: {} + + tinybench@2.9.0: {} + + tinyexec@1.3.1: {} + + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.7) + picomatch: 4.0.7 + + tinyrainbow@3.1.1: {} + + typescript@6.0.3: {} + + undici-types@6.21.0: {} + + undici-types@8.3.0: {} + + uuid@14.0.2: {} + + vite-plugin-wasm@3.6.0(vite@8.2.2(@types/node@26.4.1)): + dependencies: + vite: 8.2.2(@types/node@26.4.1) + + vite@8.2.2(@types/node@26.4.1): + dependencies: + lightningcss: 1.33.0 + picomatch: 4.0.7 + postcss: 8.5.28 + rolldown: 1.2.7 + tinyglobby: 0.2.17 + optionalDependencies: + '@types/node': 26.4.1 + fsevents: 2.3.3 + + vitefu@1.1.3(vite@8.2.2(@types/node@26.4.1)): + optionalDependencies: + vite: 8.2.2(@types/node@26.4.1) + + vitest@4.1.11(@types/node@26.4.1)(happy-dom@15.11.7)(vite@8.2.2(@types/node@26.4.1)): + dependencies: + '@vitest/expect': 4.1.11 + '@vitest/mocker': 4.1.11(vite@8.2.2(@types/node@26.4.1)) + '@vitest/pretty-format': 4.1.11 + '@vitest/runner': 4.1.11 + '@vitest/snapshot': 4.1.11 + '@vitest/spy': 4.1.11 + '@vitest/utils': 4.1.11 + es-module-lexer: 2.3.2 + expect-type: 1.4.0 + magic-string: 0.30.21 + obug: 2.1.4 + pathe: 2.0.3 + picomatch: 4.0.7 + std-env: 4.2.0 + tinybench: 2.9.0 + tinyexec: 1.3.1 + tinyglobby: 0.2.17 + tinyrainbow: 3.1.1 + vite: 8.2.2(@types/node@26.4.1) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 26.4.1 + happy-dom: 15.11.7 + transitivePeerDependencies: + - msw + + w3c-keyname@2.2.8: {} + + webidl-conversions@7.0.0: {} + + whatwg-mimetype@3.0.0: {} + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + + ws@8.21.3: {} + + xstate@5.32.6: {} + + zimmerframe@1.1.5: {} diff --git a/examples/patchwork/pnpm-workspace.yaml b/examples/patchwork/pnpm-workspace.yaml new file mode 100644 index 00000000..8f558344 --- /dev/null +++ b/examples/patchwork/pnpm-workspace.yaml @@ -0,0 +1,7 @@ +allowBuilds: + cbor-extract: false + esbuild: true + "@automerge/automerge-repo@https://codeload.github.com/onsetsoftware/automerge-repo/tar.gz/b39d94c5624a3497150e08ba18546f29222289d8#path:packages/automerge-repo": true +overrides: + "@automerge/automerge": 3.3.2 + "@automerge/automerge-subduction": 0.16.1 diff --git a/examples/patchwork/probe/compiler-probe.mjs b/examples/patchwork/probe/compiler-probe.mjs new file mode 100644 index 00000000..d1900dbe --- /dev/null +++ b/examples/patchwork/probe/compiler-probe.mjs @@ -0,0 +1,168 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +/** + * Loads the built compiler chunk in headless Chrome and compiles a theory with + * it, the way the Theory Editor does. + * + * This exists because the first version of src/theory/compiler.ts resolved its + * two files against `import.meta.url`, which pointed at dist/assets/ while the + * files sat at dist/ — a 404 no unit test could see, and the only thing the + * tool needs the browser for. The probe imports the *built* chunk, so it is + * testing the emitted urls and not a copy of the source. + * + * node probe/compiler-probe.mjs [--headed] + * + * Needs `pnpm build` first. Exits non-zero with the report on any failure. + */ + +import { spawn } from "node:child_process" +import { createReadStream, existsSync, readdirSync, statSync } from "node:fs" +import { rm } from "node:fs/promises" +import { createServer } from "node:http" +import { extname, join, resolve } from "node:path" + +const dist = resolve(import.meta.dirname, "..", "dist") +const port = 4457 +const profile = "/tmp/coln-patchwork-probe-profile" +const headed = process.argv.includes("--headed") +const MIME = { + ".js": "text/javascript", + ".json": "application/json", + ".html": "text/html", + ".wasm": "application/wasm", + ".map": "application/json", +} + +if (!existsSync(dist)) { + console.error("no dist/ — run `pnpm build` first") + process.exit(2) +} + +const chunk = readdirSync(join(dist, "assets")).find( + name => name.startsWith("compiler-") && name.endsWith(".js"), +) +if (!chunk) { + console.error("no compiler chunk in dist/assets — did the build change?") + process.exit(2) +} + +const goodTheory = `theory Graph := sig + V : Set + E : V -> V -> Set +end + +realm GraphRealm @ Graph +end` +const brokenTheory = "realm Nope @ MissingTheory\nend" + +const page = `coln compiler probe +` + +let reported +const server = createServer((request, response) => { + response.setHeader("Access-Control-Allow-Origin", "*") + if (request.method === "POST" && request.url === "/report") { + let body = "" + request.on("data", chunk => (body += chunk)) + request.on("end", () => { + response.writeHead(204).end() + reported = body + finish() + }) + return + } + const { pathname } = new URL(request.url, "http://probe") + if (pathname === "/" || pathname === "/probe.html") { + response.writeHead(200, { "content-type": "text/html" }) + response.end(page) + return + } + const file = join(dist, pathname) + if (!file.startsWith(dist) || !existsSync(file) || statSync(file).isDirectory()) { + response.writeHead(404).end() + return + } + response.writeHead(200, { + "content-type": MIME[extname(file)] ?? "application/octet-stream", + }) + createReadStream(file).pipe(response) +}) + +server.listen(port) + +const chrome = spawn( + "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", + [ + ...(headed ? [] : ["--headless=new"]), + "--disable-gpu", + "--no-first-run", + "--no-default-browser-check", + `--user-data-dir=${profile}`, + `http://localhost:${port}/probe.html`, + ], + { stdio: "ignore" }, +) + +const timeout = setTimeout(() => { + console.error("TIMEOUT: the probe never reported") + finish(2) +}, 120_000) + +let finishing = false +async function finish(code) { + if (finishing) return + finishing = true + clearTimeout(timeout) + chrome.kill() + server.close() + await rm(profile, { force: true, recursive: true }).catch(() => {}) + if (reported) console.log(reported) + const report = reported ? JSON.parse(reported) : undefined + process.exit(code ?? (report?.ok ? 0 : 1)) +} diff --git a/examples/patchwork/scripts/stage-compiler.mjs b/examples/patchwork/scripts/stage-compiler.mjs new file mode 100644 index 00000000..70e75a25 --- /dev/null +++ b/examples/patchwork/scripts/stage-compiler.mjs @@ -0,0 +1,46 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +// Copies the browser build of the Coln compiler into src/theory/compiler-dist/, +// where src/theory/compiler.ts imports both files with `?url` so vite emits +// them as hashed assets and owns their urls. They were once staged into +// `public/` instead, which put them at `dist/` while the code asking for them +// ran from `dist/assets/` — a 404 at the only moment that matters. +// +// Build the compiler first with `just examples/build-web-patchwork`, or point +// COLN_COMPILER_DIST at an existing build. + +import { access, copyFile, mkdir } from "node:fs/promises" +import { dirname, join, resolve } from "node:path" +import { fileURLToPath } from "node:url" + +const here = dirname(fileURLToPath(import.meta.url)) +const source = resolve( + process.env.COLN_COMPILER_DIST ?? join(here, "..", "..", "..", "_build", "web", "compiler", "dist"), +) +const target = join(here, "..", "src", "theory", "compiler-dist") +// `loadHaskellWasm.js` is deliberately not staged: src/theory/compiler.ts +// does that job with a bundled WASI shim instead of a CDN import. +const files = ["coln.wasm", "ghc_wasm_jsffi.js"] + +const missing = [] +for (const file of files) { + try { + await access(join(source, file)) + } catch { + missing.push(file) + } +} + +if (missing.length > 0) { + console.error(`stage-compiler: ${source} is missing ${missing.join(", ")}.`) + console.error("Build the compiler with `just packages/coln-compiler-wasm::build`,") + console.error("or set COLN_COMPILER_DIST to a directory that has it.") + process.exit(1) +} + +await mkdir(target, { recursive: true }) +for (const file of files) { + await copyFile(join(source, file), join(target, file)) + console.log(`staged ${file}`) +} diff --git a/examples/patchwork/src/coln/PointerSetup.svelte b/examples/patchwork/src/coln/PointerSetup.svelte new file mode 100644 index 00000000..b5c16848 --- /dev/null +++ b/examples/patchwork/src/coln/PointerSetup.svelte @@ -0,0 +1,143 @@ + + + + + +
+
{ + event.preventDefault() + onopen(storeUrl) + }} + > +
+

Coln store

+

{heading}

+

{blurb}

+
+ +
+ + + {#if error} + + {/if} +
+ + {#if oncreate} +
+
+

Or start fresh

+

Creates a new store on {syncEndpoint()}

+
+ +
+ {/if} +
+
+ + diff --git a/examples/patchwork/src/coln/handle.svelte.ts b/examples/patchwork/src/coln/handle.svelte.ts new file mode 100644 index 00000000..47d8593e --- /dev/null +++ b/examples/patchwork/src/coln/handle.svelte.ts @@ -0,0 +1,39 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import type { + ColnChange, + ColnDocument, + ColnHandle as RepoColnHandle, + RealmBindings, +} from "@coln-project/repo" +import { createSubscriber } from "svelte/reactivity" + +/** + * A Coln store handle as reactive Svelte state. Coln documents change through + * both `change` (a local transaction) and `heads-changed` (commits arriving + * from the relay), so both are subscribed. + */ +export class ColnStore { + readonly #subscribe: () => void + + constructor(private readonly handle: RepoColnHandle) { + this.#subscribe = createSubscriber(update => { + handle.on("change", update) + handle.on("heads-changed", update) + return () => { + handle.off("change", update) + handle.off("heads-changed", update) + } + }) + } + + get state(): ColnDocument { + this.#subscribe() + return this.handle.doc() + } + + change(change: ColnChange): void { + this.handle.change(change) + } +} diff --git a/examples/patchwork/src/coln/pointer.ts b/examples/patchwork/src/coln/pointer.ts new file mode 100644 index 00000000..689d162a --- /dev/null +++ b/examples/patchwork/src/coln/pointer.ts @@ -0,0 +1,69 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import { patchworkRepo, type PatchworkMeta } from "../patchwork.ts" + +/** + * A Patchwork document that points at a Coln store. + * + * Patchwork addresses documents by `automerge:` url and opens them through its + * own repo, so a `coln:` store can never be a Patchwork document itself. This + * one-field document is the bridge: Patchwork owns the identity — title, + * sidebar entry, links, sharing — while the rows live in the Coln repo at + * `storeUrl`. Every Coln tool here edits one of these. + */ +export interface ColnPointerDoc { + "@patchwork"?: PatchworkMeta + title: string + /** A `coln:` document url, or "" for a pointer that has yet to be aimed. */ + storeUrl: string + /** Realm this store was created from, when known. Display only. */ + realmName?: string + /** The theory this store was compiled from, for the trip back. */ + theoryUrl?: string +} + +/** Create a Patchwork pointer document for an existing Coln store. */ +export async function createPointerDoc(pointer: { + type: string + title: string + storeUrl: string + realmName?: string + theoryUrl?: string +}): Promise { + const { type, title, storeUrl, realmName, theoryUrl } = pointer + const handle = await patchworkRepo().create2({ + "@patchwork": { type }, + title, + storeUrl, + // Spread rather than assigned: automerge has no undefined to store. + ...(realmName ? { realmName } : {}), + ...(theoryUrl ? { theoryUrl } : {}), + }) + return handle.url +} + +/** + * The datatype half of a Coln tool. Both Coln datatypes are the same document + * with a different id, so they share this. + */ +export function pointerDatatype(type: string, fallbackTitle: string) { + return { + init(doc: ColnPointerDoc) { + doc["@patchwork"] = { type } + doc.title = fallbackTitle + doc.storeUrl = "" + }, + getTitle(doc: ColnPointerDoc) { + return doc.title || fallbackTitle + }, + setTitle(doc: ColnPointerDoc, title: string) { + doc.title = title + }, + markCopy(doc: ColnPointerDoc) { + // A copy points at the same store: Coln stores are shared by url, and + // copying the pointer does not fork the store behind it. + doc.title = `Copy of ${doc.title || fallbackTitle}` + }, + } +} diff --git a/examples/patchwork/src/coln/repo.ts b/examples/patchwork/src/coln/repo.ts new file mode 100644 index 00000000..fef18a35 --- /dev/null +++ b/examples/patchwork/src/coln/repo.ts @@ -0,0 +1,64 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import { Repo, type DocumentId } from "@automerge/automerge-repo" +import { IndexedDBStorageAdapter } from "@automerge/automerge-repo-storage-indexeddb" + +/** + * Coln's own repo, living beside Patchwork's. + * + * A Coln store is not an automerge document: it is a Rust store whose commits + * ride the Subduction sedimentree, registered with automerge-repo through + * `defineDocumentType({ urlScheme: "coln" })`. That API only exists in the + * automerge-repo fork this package bundles, and the stores sync through Coln's + * relay rather than Patchwork's, so they cannot be served by `window.repo`. + * + * What is shared is the wasm underneath: automerge and Subduction come from + * Patchwork's importmap (already initialised on the main thread by the host), + * so this repo adds a websocket and an IndexedDB database, not another CRDT. + */ + +const ENDPOINT_KEY = "coln-patchwork:sync-endpoint" +const DEFAULT_ENDPOINT = "wss://coln.sync.inkandswitch.com" + +/** + * The relay this page syncs Coln stores through. Override it per-browser with + * + * localStorage["coln-patchwork:sync-endpoint"] = "ws://127.0.0.1:3030" + * + * which is how you point these tools at a local Subduction relay (`pnpm --dir + * examples/lab server`). Read once, at first use: changing it takes a reload. + */ +export function syncEndpoint(): string { + try { + return localStorage.getItem(ENDPOINT_KEY) || DEFAULT_ENDPOINT + } catch { + return DEFAULT_ENDPOINT + } +} + +let repo: Repo | undefined +const openDocuments = new Set() + +export function colnRepo(): Repo { + if (!repo) { + repo = new Repo({ + storage: new IndexedDBStorageAdapter("coln-patchwork", "documents"), + subductionWebsocketEndpoints: [syncEndpoint()], + }) + addEventListener("pagehide", flush) + } + return repo +} + +/** + * Coln stores are flushed explicitly rather than on a timer, so every store a + * tool opens is tracked and written out before the page goes away. + */ +export function trackDocument(documentId: DocumentId): void { + openDocuments.add(documentId) +} + +function flush(): void { + if (repo && openDocuments.size > 0) void repo.flush([...openDocuments]) +} diff --git a/examples/patchwork/src/coln/sync.svelte.ts b/examples/patchwork/src/coln/sync.svelte.ts new file mode 100644 index 00000000..4bd87ee8 --- /dev/null +++ b/examples/patchwork/src/coln/sync.svelte.ts @@ -0,0 +1,103 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import type { DocumentId } from "@automerge/automerge-repo" +import { createSubscriber } from "svelte/reactivity" +import { colnRepo } from "./repo.ts" + +type FlushableHandle = { + documentId: DocumentId + on(event: "change", listener: () => void): void + off(event: "change", listener: () => void): void +} + +export type SyncStatus = "offline" | "syncing" | "synced" | "error" + +/** + * Sync state for one Coln store. Patchwork's own sync indicator watches the + * host repo, which knows nothing about Coln's relay, so Coln tools report + * their own: a store is "synced" once a flush to the relay has completed with + * no newer local change waiting behind it. + */ +export class StoreSync { + readonly #subscribe: () => void + #status: SyncStatus + #error: unknown + + constructor(private readonly handle: FlushableHandle) { + const repo = colnRepo() + this.#status = repo.isSubductionConnected() ? "syncing" : "offline" + this.#subscribe = createSubscriber(update => { + let active = true + let generation = 0 + let currentAttempt: symbol | undefined + + const setStatus = (status: SyncStatus, error?: unknown) => { + if (this.#status === status && this.#error === error) return + this.#status = status + this.#error = error + update() + } + const finishFlush = ( + attempt: symbol, + flushGeneration: number, + failed: boolean, + error?: unknown, + ) => { + if (!active || currentAttempt !== attempt) return + currentAttempt = undefined + if (!repo.isSubductionConnected()) setStatus("offline") + else if (flushGeneration !== generation) startFlush() + else if (failed) setStatus("error", error) + else setStatus("synced") + } + const startFlush = () => { + if (currentAttempt || !active || !repo.isSubductionConnected()) return + const attempt = Symbol() + const flushGeneration = generation + currentAttempt = attempt + void repo.flush([this.handle.documentId]).then( + () => finishFlush(attempt, flushGeneration, false), + (error: unknown) => finishFlush(attempt, flushGeneration, true, error), + ) + } + const requestFlush = () => { + generation += 1 + if (!repo.isSubductionConnected()) { + currentAttempt = undefined + setStatus("offline") + return + } + setStatus("syncing") + startFlush() + } + + this.handle.on("change", requestFlush) + repo.on("subduction-connection", requestFlush) + requestFlush() + return () => { + active = false + generation += 1 + currentAttempt = undefined + this.handle.off("change", requestFlush) + repo.off("subduction-connection", requestFlush) + } + }) + } + + get status(): SyncStatus { + this.#subscribe() + return this.#status + } + + get error(): unknown { + this.#subscribe() + return this.#error + } + + get message(): string { + const error = this.error + if (error === undefined) return "" + return error instanceof Error ? error.message : String(error) + } +} diff --git a/examples/patchwork/src/graph/GraphCanvas.svelte b/examples/patchwork/src/graph/GraphCanvas.svelte new file mode 100644 index 00000000..be56bced --- /dev/null +++ b/examples/patchwork/src/graph/GraphCanvas.svelte @@ -0,0 +1,380 @@ + + + + + + + +
+ {#if graph.vertices.length === 0} + + {/if} + + + + + + + + + + + + + + + + {#each drawableEdges as edge (edge.id)} + onselectedge(edge.id)} + onkeydown={event => event.key === "Enter" && onselectedge(edge.id)} + role="button" + tabindex="0" + aria-label={`Directed edge from ${vertexLabel(edge.fromId)} to ${vertexLabel(edge.toId)}`} + data-testid="graph-edge" + > + {/each} + + {#if previewEdgePath} + + {/if} + + {#each points as vertex (vertex.id)} + onselectvertex(vertex)} + onkeydown={event => event.key === "Enter" && onselectvertex(vertex)} + role="button" + tabindex="0" + data-role={vertex.id === to?.id ? "target" : vertex.id === from?.id ? "source" : undefined} + data-testid="graph-vertex" + > + {vertex.label}: {vertex.id} + + {vertex.label} + + {/each} + + +
+ edge source + edge target +
+
+ + diff --git a/examples/patchwork/src/graph/GraphControls.svelte b/examples/patchwork/src/graph/GraphControls.svelte new file mode 100644 index 00000000..deff894e --- /dev/null +++ b/examples/patchwork/src/graph/GraphControls.svelte @@ -0,0 +1,149 @@ + + + + + +
+

Edit graph

+ + + +
+ + + + +
+ + {#if selectedEdge} +
+ Selected edge + + {vertexLabel(selectedEdge.fromId)} → {vertexLabel(selectedEdge.toId)} + + {shortId(selectedEdge.id)} +
+ {:else if from} +
+ {to ? "Edge ready to add" : "Source selected"} + {from.label}{to ? ` → ${to.label}` : ""} + {shortId(from.id)} +
+ {/if} + + {#if error}{/if} +
+ + diff --git a/examples/patchwork/src/graph/GraphTool.svelte b/examples/patchwork/src/graph/GraphTool.svelte new file mode 100644 index 00000000..5dcbeb8e --- /dev/null +++ b/examples/patchwork/src/graph/GraphTool.svelte @@ -0,0 +1,408 @@ + + + + + +
+ {#if feedback}{/if} + + {#if !store || !graphHandle} + void createGraphStore()} + /> + {:else} +
+
+ (selectedEdgeId = id)} + onselectvertex={selectVertex} + /> +
+ +
+ {/if} +
+ + diff --git a/examples/patchwork/src/graph/StoreSummary.svelte b/examples/patchwork/src/graph/StoreSummary.svelte new file mode 100644 index 00000000..6af0c50e --- /dev/null +++ b/examples/patchwork/src/graph/StoreSummary.svelte @@ -0,0 +1,126 @@ + + + + + +
+
+
+

Store summary

+

Tables backing this graph

+
+ {headCount} {headCount === 1 ? "head" : "heads"} +
+ +
+
{tables.length}tables
+
{rowCount}rows
+
{headCount}heads
+
+ +
+ {#each tables as table (table.name)} +
+
+ {table.name} + + {table.rowCount} {table.rowCount === 1 ? "row" : "rows"} + +
+ {#if table.columns.length === 0} +

No non-key value columns

+ {:else} + {#each table.columns as column (column.name)} +
+ {column.name}{column.primary ? " *" : ""} + {column.type} +
+ {/each} + {/if} +
+ {/each} +
+
+ + diff --git a/examples/patchwork/src/graph/datatype.ts b/examples/patchwork/src/graph/datatype.ts new file mode 100644 index 00000000..ad07ee72 --- /dev/null +++ b/examples/patchwork/src/graph/datatype.ts @@ -0,0 +1,15 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import { pointerDatatype } from "../coln/pointer.ts" + +export const GRAPH_TYPE = "coln-graph" + +/** + * A Patchwork document pointing at a Coln store that uses the checked-in + * `GraphRealm` schema (src/graph/graph.coln). The Graph Demo refuses stores + * with any other schema, since its generated bindings would not match. + */ +export const ColnGraphDatatype = pointerDatatype(GRAPH_TYPE, "Coln graph") + +export default ColnGraphDatatype diff --git a/examples/patchwork/src/graph/generated/Graph.ts b/examples/patchwork/src/graph/generated/Graph.ts new file mode 100644 index 00000000..b8fc8138 --- /dev/null +++ b/examples/patchwork/src/graph/generated/Graph.ts @@ -0,0 +1,11 @@ +import * as runtime from "@coln-project/runtime"; + +export interface View { + V: runtime.ColnSet.View; + E: (x: runtime.Value) => (x: runtime.Value) => runtime.ColnSet.View; +} + +export interface Transaction extends View { + V: runtime.ColnSet.Transaction; + E: (x: runtime.Value) => (x: runtime.Value) => runtime.ColnSet.Transaction; +} \ No newline at end of file diff --git a/examples/patchwork/src/graph/generated/GraphRealm.json b/examples/patchwork/src/graph/generated/GraphRealm.json new file mode 100644 index 00000000..b83d89f3 --- /dev/null +++ b/examples/patchwork/src/graph/generated/GraphRealm.json @@ -0,0 +1 @@ +{"entities":[{"path":[["GraphRealm"],["V"]],"value":{"entityVariant":{"tag":"table"},"columns":[],"primaryKey":null}},{"path":[["GraphRealm"],["E"]],"value":{"entityVariant":{"tag":"table"},"columns":[{"path":[["a"]],"type":{"tag":"rowId","path":[["GraphRealm"],["V"]]}},{"path":[["b"]],"type":{"tag":"rowId","path":[["GraphRealm"],["V"]]}}],"primaryKey":null}}],"rules":[{"path":[["GraphRealm"],["V"],["foreignKey"]],"value":{"ruleVariant":"enforced","varNames":[],"varTypes":[],"antecedents":[{"tag":"atom","atom":{"entity":[["GraphRealm"],["V"]],"rowId":null,"values":[]}}],"consequents":[]}},{"path":[["GraphRealm"],["E"],["foreignKey"]],"value":{"ruleVariant":"enforced","varNames":[[["a"]],[["b"]]],"varTypes":[{"tag":"rowId","path":[["GraphRealm"],["V"]]},{"tag":"rowId","path":[["GraphRealm"],["V"]]}],"antecedents":[{"tag":"atom","atom":{"entity":[["GraphRealm"],["E"]],"rowId":null,"values":[{"column":0,"term":{"tag":"var","index":0}},{"column":1,"term":{"tag":"var","index":1}}]}}],"consequents":[{"tag":"atom","atom":{"entity":[["GraphRealm"],["V"]],"rowId":{"tag":"var","index":0},"values":[]}},{"tag":"atom","atom":{"entity":[["GraphRealm"],["V"]],"rowId":{"tag":"var","index":1},"values":[]}}]}}]} \ No newline at end of file diff --git a/examples/patchwork/src/graph/generated/GraphRealm.ts b/examples/patchwork/src/graph/generated/GraphRealm.ts new file mode 100644 index 00000000..9b2db543 --- /dev/null +++ b/examples/patchwork/src/graph/generated/GraphRealm.ts @@ -0,0 +1,48 @@ +import schema from "./GraphRealm.json"; +export {schema}; +import * as runtime from "@coln-project/runtime"; +import * as Graph from "./Graph.ts"; + +export class View { + root: Graph.View; + + constructor(store: runtime.StoreHandle) { + this.root = { + V: (new runtime.RowIdSet.View(store, "GraphRealm.V", [])), + E: (a: runtime.Value) => { + return (b: runtime.Value) => { + return (new runtime.RowIdSet.View(store, "GraphRealm.E", [a, b])); + }; + } + }; + } +} + +export class Transaction extends View { + root: Graph.Transaction; + + constructor( + store: runtime.StoreHandle, + transaction: runtime.TransactionHandle + ) { + super(store); + this.root = { + V: (new runtime.RowIdSet.Transaction( + store, + "GraphRealm.V", + [], + transaction + )), + E: (a: runtime.Value) => { + return (b: runtime.Value) => { + return (new runtime.RowIdSet.Transaction( + store, + "GraphRealm.E", + [a, b], + transaction + )); + }; + } + }; + } +} \ No newline at end of file diff --git a/examples/patchwork/src/graph/graph.coln b/examples/patchwork/src/graph/graph.coln new file mode 100644 index 00000000..04a0be4c --- /dev/null +++ b/examples/patchwork/src/graph/graph.coln @@ -0,0 +1,7 @@ +theory Graph := sig + V : Set + E : V -> V -> Set +end + +realm GraphRealm @ Graph +end diff --git a/examples/patchwork/src/graph/graph.ts b/examples/patchwork/src/graph/graph.ts new file mode 100644 index 00000000..e84f5df4 --- /dev/null +++ b/examples/patchwork/src/graph/graph.ts @@ -0,0 +1,114 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import type { ColnDocument, ColnHandle } from "@coln-project/repo" +import type { RowRef, RowView, Value } from "@coln-project/runtime" +import * as GraphRealm from "./generated/GraphRealm.ts" + +export type GraphHandle = ColnHandle +type GraphDocument = ColnDocument +type GraphWriter = Pick +type RowIdValue = Extract + +export type Vertex = { + id: string + label: string + ref: RowRef +} + +export type Edge = { + id: string + fromId: string + toId: string +} + +export type Graph = { + vertices: Vertex[] + edges: Edge[] + heads: string[] +} + +export function readGraph(document: GraphDocument): Graph { + const vertices = iteratorToArray(document.root.V.values()) + .map(rowRef) + .filter(ref => ref !== null) + .map(ref => ({ id: refId(ref), ref })) + .map((vertex, index) => ({ ...vertex, label: `V${index + 1}` })) + const vertexIds = new Set(vertices.map(vertex => vertex.id)) + + const edges = document.scanTable("GraphRealm.E") + .flatMap(row => projectEdge(row, vertexIds)) + .sort((left, right) => left.id.localeCompare(right.id)) + + return { vertices, edges, heads: [...document.heads()].sort() } +} + +export function addVertex(writer: GraphWriter): void { + writer.change(tx => { + tx.root.V.add() + }) +} + +export function addEdge( + writer: GraphWriter, + from: Vertex, + to: Vertex, +): void { + writer.change(tx => { + tx.root.E(rowValue(from.ref))(rowValue(to.ref)).add() + }) +} + +export function watchGraph( + handle: GraphHandle, + update: (graph: Graph) => void, +): () => void { + const refresh = () => update(readGraph(handle.doc())) + handle.on("change", refresh) + handle.on("heads-changed", refresh) + refresh() + + return () => { + handle.off("change", refresh) + handle.off("heads-changed", refresh) + } +} + +function projectEdge(row: RowView, vertexIds: Set): Edge[] { + const from = valueRef(row.values[0]) + const to = valueRef(row.values[1]) + const edge = rowRef(row) + if (!from || !to || !edge) return [] + + const fromId = refId(from) + const toId = refId(to) + if (!vertexIds.has(fromId) || !vertexIds.has(toId)) return [] + + return [{ id: refId(edge), fromId, toId }] +} + +function iteratorToArray(iterator: Iterator): T[] { + const values: T[] = [] + for (let next = iterator.next(); !next.done; next = iterator.next()) { + values.push(next.value) + } + return values +} + +function rowRef(row: RowView): RowRef | null { + return valueRef(row.rowId) +} + +function valueRef(value: Value | undefined): RowRef | null { + return value?.tag === "row_id" ? value.value : null +} + +function rowValue(ref: RowRef): RowIdValue { + return { tag: "row_id", value: ref } +} + +function refId(ref: RowRef): string { + if ("existing" in ref) return `${ref.existing.commit}:${ref.existing.counter}` + return `pending:${ref.pending.txId}:${ref.pending.counter}` +} diff --git a/examples/patchwork/src/graph/tool.ts b/examples/patchwork/src/graph/tool.ts new file mode 100644 index 00000000..c6f2a010 --- /dev/null +++ b/examples/patchwork/src/graph/tool.ts @@ -0,0 +1,15 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import type { ColnPointerDoc } from "../coln/pointer.ts" +import { mountTool } from "../lib/mount.ts" +import { injectTheme } from "../lib/theme.ts" +import type { PatchworkTool } from "../patchwork.ts" +import GraphTool from "./GraphTool.svelte" + +const tool: PatchworkTool = (handle, element) => { + injectTheme() + return mountTool(GraphTool as never, element, { handle }) +} + +export default tool diff --git a/examples/patchwork/src/index.ts b/examples/patchwork/src/index.ts new file mode 100644 index 00000000..94cb77db --- /dev/null +++ b/examples/patchwork/src/index.ts @@ -0,0 +1,71 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +/** + * Coln in Patchwork: three tools ported from Coln Lab. + * + * Patchwork reads this module inside a Web Worker and structuredClones each + * entry, so it carries plugin metadata only — every datatype and render + * function hides behind `load()`, which runs on the main thread where the + * importmap exists. Nothing above may statically import a bare specifier. + */ + +export const plugins = [ + { + type: "patchwork:datatype", + id: "coln-theory", + name: "Coln Theory", + icon: "FileCode", + async load() { + return (await import("./theory/datatype.ts")).default + }, + }, + { + type: "patchwork:tool", + id: "coln-theory", + name: "Theory Editor", + icon: "FileCode", + supportedDatatypes: ["coln-theory"], + async load() { + return (await import("./theory/tool.ts")).default + }, + }, + { + type: "patchwork:datatype", + id: "coln-store", + name: "Coln Store", + icon: "Database", + async load() { + return (await import("./store/datatype.ts")).default + }, + }, + { + type: "patchwork:tool", + id: "coln-store", + name: "Store Editor", + icon: "Database", + supportedDatatypes: ["coln-store"], + async load() { + return (await import("./store/tool.ts")).default + }, + }, + { + type: "patchwork:datatype", + id: "coln-graph", + name: "Coln Graph", + icon: "Workflow", + async load() { + return (await import("./graph/datatype.ts")).default + }, + }, + { + type: "patchwork:tool", + id: "coln-graph", + name: "Graph Demo", + icon: "Workflow", + supportedDatatypes: ["coln-graph"], + async load() { + return (await import("./graph/tool.ts")).default + }, + }, +] diff --git a/examples/patchwork/src/lib/CodeMirrorEditor.svelte b/examples/patchwork/src/lib/CodeMirrorEditor.svelte new file mode 100644 index 00000000..753b058b --- /dev/null +++ b/examples/patchwork/src/lib/CodeMirrorEditor.svelte @@ -0,0 +1,102 @@ + + + + + +
+ + diff --git a/examples/patchwork/src/lib/Feedback.svelte b/examples/patchwork/src/lib/Feedback.svelte new file mode 100644 index 00000000..5f3f9df3 --- /dev/null +++ b/examples/patchwork/src/lib/Feedback.svelte @@ -0,0 +1,41 @@ + + + + + +

{message}

+ + diff --git a/examples/patchwork/src/lib/JsonViewer.svelte b/examples/patchwork/src/lib/JsonViewer.svelte new file mode 100644 index 00000000..58d48540 --- /dev/null +++ b/examples/patchwork/src/lib/JsonViewer.svelte @@ -0,0 +1,37 @@ + + + + + +
+ + diff --git a/examples/patchwork/src/lib/SyncBadge.svelte b/examples/patchwork/src/lib/SyncBadge.svelte new file mode 100644 index 00000000..0acae68c --- /dev/null +++ b/examples/patchwork/src/lib/SyncBadge.svelte @@ -0,0 +1,31 @@ + + + + + +
+ {label}{#if detail}/ {detail}{/if} +
+ + diff --git a/examples/patchwork/src/lib/codemirror-theme.ts b/examples/patchwork/src/lib/codemirror-theme.ts new file mode 100644 index 00000000..20a135d3 --- /dev/null +++ b/examples/patchwork/src/lib/codemirror-theme.ts @@ -0,0 +1,62 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import { EditorView } from "@codemirror/view" + +/** + * CodeMirror styling for both editors here. Every colour reads a `--coln-*` + * variable (see lib/theme.css) rather than a literal, so the editor follows + * the Patchwork document theme like the rest of the tool. + */ +export const colnEditorTheme = EditorView.theme({ + "&": { + backgroundColor: "var(--coln-fill)", + color: "var(--coln-line)", + fontFamily: "var(--coln-mono)", + fontSize: "0.875rem", + height: "100%", + }, + "&.cm-focused": { + outline: "1px solid var(--coln-accent)", + outlineOffset: "-1px", + }, + ".cm-scroller": { + fontFamily: "inherit", + lineHeight: "1.7", + overflow: "auto", + }, + ".cm-content": { + caretColor: "var(--coln-accent)", + padding: "1rem 0", + }, + ".cm-line": { padding: "0 1rem 0 0.75rem" }, + ".cm-cursor, .cm-dropCursor": { borderLeftColor: "var(--coln-accent)" }, + ".cm-selectionBackground, ::selection": { + backgroundColor: "var(--coln-selected-fill)", + }, + "&.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground": { + backgroundColor: "var(--coln-selected-fill)", + }, + ".cm-selectionMatch": { backgroundColor: "transparent", outline: "none" }, + ".cm-activeLine": { backgroundColor: "var(--coln-surface)" }, + ".cm-gutters": { + backgroundColor: "var(--coln-fill)", + borderRight: "1px solid var(--coln-border)", + color: "var(--coln-faint)", + }, + ".cm-activeLineGutter": { + backgroundColor: "var(--coln-surface)", + color: "var(--coln-muted)", + }, + ".cm-foldPlaceholder, .cm-tooltip, .cm-panels": { + backgroundColor: "var(--coln-surface-raised)", + borderColor: "var(--coln-border)", + color: "var(--coln-line)", + }, + ".cm-panels.cm-panels-top": { borderBottom: "1px solid var(--coln-border)" }, + ".cm-searchMatch": { + backgroundColor: "var(--coln-selected-fill)", + outline: "1px solid var(--coln-accent)", + }, + ".cm-tooltip": { border: "1px solid var(--coln-border)" }, +}) diff --git a/examples/patchwork/src/lib/doc.svelte.ts b/examples/patchwork/src/lib/doc.svelte.ts new file mode 100644 index 00000000..4b21e814 --- /dev/null +++ b/examples/patchwork/src/lib/doc.svelte.ts @@ -0,0 +1,22 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import { createSubscriber } from "svelte/reactivity" +import type { PatchworkHandle } from "../patchwork.ts" + +/** A Patchwork document as reactive Svelte state. */ +export class DocState { + readonly #subscribe: () => void + + constructor(private readonly handle: PatchworkHandle) { + this.#subscribe = createSubscriber(update => { + handle.on("change", update) + return () => handle.off("change", update) + }) + } + + get current(): Doc { + this.#subscribe() + return this.handle.doc() + } +} diff --git a/examples/patchwork/src/lib/mount.ts b/examples/patchwork/src/lib/mount.ts new file mode 100644 index 00000000..0bf086eb --- /dev/null +++ b/examples/patchwork/src/lib/mount.ts @@ -0,0 +1,19 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import { mount, unmount, type Component } from "svelte" + +/** + * Bridge between Patchwork's `(handle, element) => cleanup` contract and + * Svelte 5's mount/unmount. + */ +export function mountTool>( + component: Component, + element: HTMLElement, + props: Props, +): () => void { + const instance = mount(component, { target: element, props }) + return () => { + void unmount(instance) + } +} diff --git a/examples/patchwork/src/lib/theme.css b/examples/patchwork/src/lib/theme.css new file mode 100644 index 00000000..4a5cc8d2 --- /dev/null +++ b/examples/patchwork/src/lib/theme.css @@ -0,0 +1,324 @@ +/* SPDX-FileCopyrightText: 2026 Coln contributors */ +/* SPDX-License-Identifier: Apache-2.0 OR MIT */ + +/* + * Shared surface for the three Coln tools. Coln Lab painted itself a fixed + * dark palette; inside Patchwork the tools follow the document theme instead, + * so every colour here is derived from the host's editor surface and studio + * accent. Variables live in @layer package so a theme switch re-evaluates + * them; the rules below stay unlayered. + */ + +@layer package { + :root, + :host, + [theme] { + --coln-fill: var(--editor-fill, white); + --coln-line: var(--editor-line, black); + --coln-surface: var(--editor-fill-offset-10, #f6f6f6); + --coln-surface-raised: var(--editor-fill-offset-20, #ededed); + --coln-border: var(--editor-fill-offset-20, #dcdcdc); + --coln-border-strong: var(--editor-fill-offset-40, #b4b4b4); + --coln-muted: var(--editor-line-offset-40, #666); + --coln-faint: var(--editor-line-offset-50, #888); + --coln-accent: var(--studio-primary, #35f7ca); + --coln-accent-line: var(--studio-primary-line, black); + --coln-accent-text: var(--editor-primary-text, var(--studio-primary, #35f7ca)); + --coln-danger: var(--studio-danger, #ff7657); + --coln-danger-text: var(--editor-danger-text, var(--studio-danger, #ff7657)); + --coln-warning-text: var(--editor-warning-text, var(--studio-warning, #ffd166)); + --coln-link-text: var(--editor-link-text, var(--studio-link, #77b7ff)); + --coln-alert-fill: color-mix(in oklch, var(--studio-danger, #ff7657), var(--editor-fill, white) 88%); + --coln-selected-fill: color-mix(in oklch, var(--studio-primary, #35f7ca), var(--editor-fill, white) 85%); + --coln-sans: var(--editor-family-sans, system-ui, sans-serif); + --coln-mono: var(--editor-family-code, ui-monospace, monospace); + } +} + +/* Composition ------------------------------------------------------------- */ + +.coln-tool { + background: var(--coln-surface); + color: var(--coln-line); + display: flex; + flex-direction: column; + font-family: var(--coln-sans); + font-variant-numeric: tabular-nums; + height: 100%; + min-height: 0; + position: relative; +} + +.coln-tool .flow > * + * { + margin-top: var(--flow-space, var(--studio-space-sm, 0.5rem)); +} + +.coln-tool .cluster { + align-items: center; + display: flex; + flex-wrap: wrap; + gap: var(--studio-space-xs, 0.375rem); +} + +/* The panes: a horizontal split on wide viewports, stacked when narrow. */ +.coln-tool .panes { + display: grid; + flex: 1; + min-height: 0; +} + +@media (min-width: 900px) { + .coln-tool .panes[data-columns="2"] { + grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); + } + + .coln-tool .panes[data-columns="3"] { + grid-template-columns: minmax(0, 0.9fr) minmax(0, 1.2fr) minmax(0, 0.8fr); + } +} + +@media (max-width: 899px) { + .coln-tool .panes { + display: block; + overflow: auto; + } +} + +/* Blocks ------------------------------------------------------------------ */ + +.coln-tool .pane { + border-right: 1px solid var(--coln-border); + display: flex; + flex-direction: column; + min-height: 0; + min-width: 0; +} + +.coln-tool .pane:last-child { + border-right: 0; +} + +@media (max-width: 899px) { + .coln-tool .pane { + border-bottom: 1px solid var(--coln-border); + border-right: 0; + } +} + +.coln-tool .pane-head { + align-items: flex-start; + border-bottom: 1px solid var(--coln-border); + display: flex; + flex-wrap: wrap; + gap: var(--studio-space-sm, 0.5rem); + justify-content: space-between; + padding: var(--studio-space-sm, 0.75rem) var(--studio-space-md, 1rem); +} + +.coln-tool .pane-body { + min-height: 0; + overflow: auto; + padding: var(--studio-space-md, 1rem); +} + +.coln-tool .pane-body[data-flush] { + padding: 0; +} + +.coln-tool .label { + color: var(--coln-muted); + font-family: var(--coln-mono); + font-size: 0.75rem; + letter-spacing: 0.14em; + margin: 0; + text-transform: uppercase; +} + +.coln-tool .hint { + color: var(--coln-faint); + font-size: 0.875rem; + line-height: 1.5; + margin: 0.25rem 0 0; +} + +.coln-tool .mono { + font-family: var(--coln-mono); + font-size: 0.875rem; +} + +.coln-tool .truncate { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.coln-tool .action { + background: transparent; + border: 1px solid var(--coln-border-strong); + border-radius: var(--studio-radius-sm, 4px); + color: var(--coln-line); + cursor: pointer; + font-family: var(--coln-mono); + font-size: 0.8125rem; + letter-spacing: 0.06em; + padding: 0.375rem 0.625rem; + text-transform: uppercase; + transition: border-color var(--studio-transition-fast, 0.1s ease), + color var(--studio-transition-fast, 0.1s ease); +} + +.coln-tool .action:hover:not(:disabled) { + border-color: var(--coln-accent); + color: var(--coln-accent-text); +} + +.coln-tool .action[data-primary] { + background: var(--coln-accent); + border-color: var(--coln-accent); + color: var(--coln-accent-line); + font-weight: 600; +} + +.coln-tool .action[data-primary]:hover:not(:disabled) { + background: transparent; + color: var(--coln-accent-text); +} + +.coln-tool .action:disabled { + cursor: not-allowed; + opacity: 0.4; +} + +.coln-tool .action[data-pending]:disabled { + cursor: progress; +} + +.coln-tool .field { + background: var(--coln-fill); + border: 1px solid var(--coln-border-strong); + border-radius: var(--studio-radius-sm, 4px); + color: var(--coln-line); + font-family: var(--coln-mono); + font-size: 0.875rem; + min-width: 0; + padding: 0.5rem 0.625rem; +} + +.coln-tool .field:focus-visible { + border-color: var(--coln-accent); + outline: none; +} + +.coln-tool .alert { + background: var(--coln-alert-fill); + border-left: 3px solid var(--coln-danger); + color: var(--coln-danger-text); + font-family: var(--coln-mono); + font-size: 0.875rem; + line-height: 1.6; + margin: 0; + padding: var(--studio-space-sm, 0.75rem); +} + +.coln-tool .empty { + color: var(--coln-faint); + font-size: 0.875rem; + margin: 0; +} + +.coln-tool .card { + background: var(--coln-fill); + border: 1px solid var(--coln-border); + border-radius: var(--studio-radius-sm, 4px); + display: grid; + gap: var(--studio-space-xs, 0.375rem); + padding: var(--studio-space-sm, 0.75rem); +} + +/* Status dot, used for both compile and sync state. */ +.coln-tool .status { + align-items: center; + color: var(--coln-muted); + display: flex; + font-family: var(--coln-mono); + font-size: 0.8125rem; + gap: 0.5rem; + letter-spacing: 0.06em; + text-transform: uppercase; +} + +.coln-tool .status::before { + background: var(--coln-faint); + border-radius: var(--studio-radius-round, 9999px); + content: ""; + height: 0.375rem; + width: 0.375rem; +} + +.coln-tool .status[data-state="ready"], +.coln-tool .status[data-state="synced"] { + color: var(--coln-accent-text); +} + +.coln-tool .status[data-state="ready"]::before, +.coln-tool .status[data-state="synced"]::before { + background: var(--coln-accent); +} + +.coln-tool .status[data-state="error"] { + color: var(--coln-danger-text); +} + +.coln-tool .status[data-state="error"]::before { + background: var(--coln-danger); +} + +/* json-formatter-js paints its own colours; re-point them at the theme. */ +.coln-json .json-formatter-row { + font-family: var(--coln-mono); + font-size: 0.875rem; + line-height: 1.65; +} + +.coln-json .json-formatter-row, +.coln-json .json-formatter-row a, +.coln-json .json-formatter-row a:hover { + color: var(--coln-muted); +} + +.coln-json .json-formatter-key { color: var(--coln-link-text); } +.coln-json .json-formatter-string, +.coln-json .json-formatter-stringifiable { color: var(--coln-accent-text); } +.coln-json .json-formatter-number { color: var(--coln-warning-text); } +.coln-json .json-formatter-boolean { color: var(--coln-danger-text); } +.coln-json .json-formatter-null { color: var(--coln-faint); } +.coln-json .json-formatter-bracket { color: var(--coln-faint); } +.coln-json .json-formatter-toggler { color: var(--coln-accent-text); } + +.coln-json .json-fallback { + margin: 0; + overflow-wrap: anywhere; + white-space: pre-wrap; +} + +/* Compiler diagnostics arrive as HTML with `ann-*` / `diag-*` classes. */ +.coln-diagnostics pre { + margin: 0; + overflow-x: auto; + white-space: pre-wrap; +} + +.coln-diagnostics > :not(:last-child) { + border-bottom: 1px solid var(--coln-border); + padding-bottom: var(--studio-space-sm, 0.75rem); +} + +.coln-diagnostics .diag-debug { --coln-diagnostic: var(--coln-muted); } +.coln-diagnostics .diag-info { --coln-diagnostic: var(--coln-link-text); } +.coln-diagnostics .diag-warning { --coln-diagnostic: var(--coln-warning-text); } +.coln-diagnostics .diag-error { --coln-diagnostic: var(--coln-danger-text); } + +.coln-diagnostics [class^="ann-"] { + color: var(--coln-diagnostic, var(--coln-line)); + font-weight: 500; +} diff --git a/examples/patchwork/src/lib/theme.ts b/examples/patchwork/src/lib/theme.ts new file mode 100644 index 00000000..44429327 --- /dev/null +++ b/examples/patchwork/src/lib/theme.ts @@ -0,0 +1,25 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import css from "./theme.css?inline" + +/** + * Add the shared Coln stylesheet to the page, once. + * + * Patchwork tools render into the light DOM, so this is a plain ` diff --git a/examples/patchwork/src/store/StoreRepl.svelte b/examples/patchwork/src/store/StoreRepl.svelte new file mode 100644 index 00000000..d71f6a5a --- /dev/null +++ b/examples/patchwork/src/store/StoreRepl.svelte @@ -0,0 +1,102 @@ + + + + + +
+
+
+

JavaScript REPL

+

+ Run only code you trust / handle is in scope / use + return for the result +

+
+ +
+
+ (source = value)} + onrun={runProgram} + /> +
+
+ +
+
+ + diff --git a/examples/patchwork/src/store/StoreTool.svelte b/examples/patchwork/src/store/StoreTool.svelte new file mode 100644 index 00000000..8ee1cc34 --- /dev/null +++ b/examples/patchwork/src/store/StoreTool.svelte @@ -0,0 +1,266 @@ + + + + + +
+ {#if feedback}{/if} + + {#if !open} + + {:else} + + {@const openStoreUrl = open.handle.url} +
+
+ + {#snippet toolbar()} +
+
+ + {#if theoryUrl} + + {/if} + + +
+ {openStoreUrl} +
+ {/snippet} +
+
+
+ +
+
+ {/if} +
+ + diff --git a/examples/patchwork/src/store/TableBrowser.svelte b/examples/patchwork/src/store/TableBrowser.svelte new file mode 100644 index 00000000..f7827c64 --- /dev/null +++ b/examples/patchwork/src/store/TableBrowser.svelte @@ -0,0 +1,335 @@ + + + + + +
+
+
+

Store contents

+

{tables.length} {tables.length === 1 ? "table" : "tables"}

+
+ {#if toolbar}
{@render toolbar()}
{/if} + +
+ +
+ + +
+ {#if selected} + + + + + {#each selected.columns as column} + + {/each} + + + + {#each rows as row (displayRowRef(row.rowId.value).full)} + {@const rowId = displayRowRef(row.rowId.value)} + + + {#each row.values as value, index} + {@const displayed = displayValue(value)} + + {/each} + + {:else} + + + + {/each} + +
Row ID + {column.name}{column.primary ? " *" : ""} + {column.type} +
{rowId.compact} + {#if displayed.kind === "ref" && canfollow(selected.columns[index]?.referenceTable, value)} + + {:else} + {displayed.compact} + {/if} +
+ No rows in {selected.name}. +
+ {:else} +

This store has no tables.

+ {/if} +
+
+
+ + diff --git a/examples/patchwork/src/store/ValueInspector.svelte b/examples/patchwork/src/store/ValueInspector.svelte new file mode 100644 index 00000000..6aa6dc1c --- /dev/null +++ b/examples/patchwork/src/store/ValueInspector.svelte @@ -0,0 +1,111 @@ + + + + + +
+
+ {#if structured} + + + {/if} + +
+ + {#if structured} +
+ {:else} + {primitive} + {/if} +
+ + diff --git a/examples/patchwork/src/store/datatype.ts b/examples/patchwork/src/store/datatype.ts new file mode 100644 index 00000000..8adb14ae --- /dev/null +++ b/examples/patchwork/src/store/datatype.ts @@ -0,0 +1,14 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import { pointerDatatype } from "../coln/pointer.ts" + +export const STORE_TYPE = "coln-store" + +/** + * A Patchwork document pointing at one Coln store, browsed by the Store + * Editor. See coln/pointer.ts for why the pointer exists. + */ +export const ColnStoreDatatype = pointerDatatype(STORE_TYPE, "Coln store") + +export default ColnStoreDatatype diff --git a/examples/patchwork/src/store/evaluate.ts b/examples/patchwork/src/store/evaluate.ts new file mode 100644 index 00000000..3caeb954 --- /dev/null +++ b/examples/patchwork/src/store/evaluate.ts @@ -0,0 +1,96 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import type { ColnHandle, RealmBindings } from "@coln-project/repo" +import { snapshot, type Snapshot } from "./snapshot.ts" + +export type ConsoleLevel = "debug" | "info" | "log" | "warn" | "error" + +export interface ConsoleEntry { + level: ConsoleLevel + values: Snapshot[] +} + +export interface EvaluationSuccess { + ok: true + result: Snapshot + console: ConsoleEntry[] + durationMs: number +} + +export interface EvaluationFailure { + ok: false + phase: "syntax" | "runtime" + error: Snapshot + console: ConsoleEntry[] + durationMs: number +} + +export type Evaluation = EvaluationSuccess | EvaluationFailure + +type AsyncProgram = ( + handle: unknown, + console: Pick, +) => Promise + +const AsyncFunction = Object.getPrototypeOf(async function () {}) + .constructor as new (...parameters: string[]) => AsyncProgram + +export async function evaluate( + source: string, + handle: ColnHandle, +): Promise { + const entries: ConsoleEntry[] = [] + let active = true + const capturedConsole = Object.fromEntries( + (["debug", "info", "log", "warn", "error"] as const).map((level) => [ + level, + (...values: unknown[]) => { + if (active) + entries.push({ + level, + values: values.map((value) => snapshot(value)), + }) + }, + ]), + ) as Pick + + let program: AsyncProgram + const started = performance.now() + try { + program = new AsyncFunction( + "handle", + "console", + `"use strict";\n${source}\n//# sourceURL=coln-patchwork-repl.js`, + ) + } catch (error) { + active = false + return { + ok: false, + phase: "syntax", + error: snapshot(error), + console: entries, + durationMs: performance.now() - started, + } + } + + try { + const result = await program(handle, capturedConsole) + active = false + return { + ok: true, + result: snapshot(result), + console: entries, + durationMs: performance.now() - started, + } + } catch (error) { + active = false + return { + ok: false, + phase: "runtime", + error: snapshot(error), + console: entries, + durationMs: performance.now() - started, + } + } +} diff --git a/examples/patchwork/src/store/format.ts b/examples/patchwork/src/store/format.ts new file mode 100644 index 00000000..16f796a9 --- /dev/null +++ b/examples/patchwork/src/store/format.ts @@ -0,0 +1,61 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +export interface DisplayValue { + compact: string + full: string + kind: "int" | "string" | "ref" | "unknown" +} + +type JsonObject = Record + +export function displayValue(value: unknown): DisplayValue { + if (!isObject(value) || typeof value.tag !== "string") { + return unknownValue(value) + } + if (value.tag === "int" && typeof value.value === "number") { + const rendered = String(value.value) + return { compact: rendered, full: rendered, kind: "int" } + } + if (value.tag === "string" && typeof value.value === "string") { + return { compact: value.value, full: value.value, kind: "string" } + } + if (value.tag === "row_id") return displayRowRef(value.value) + return unknownValue(value) +} + +export function displayRowRef(value: unknown): DisplayValue { + if (!isObject(value)) return unknownValue(value) + if (isObject(value.existing)) { + const commit = value.existing.commit + const counter = value.existing.counter + if (typeof commit === "string" && typeof counter === "number") { + const full = `${commit}:${counter}` + const compact = `${commit.slice(0, 8)}…${commit.slice(-4)}:${counter}` + return { compact, full, kind: "ref" } + } + } + if (isObject(value.pending)) { + const txId = value.pending.txId + const counter = value.pending.counter + if (typeof txId === "number" && typeof counter === "number") { + const rendered = `pending:${txId}:${counter}` + return { compact: rendered, full: rendered, kind: "ref" } + } + } + return unknownValue(value) +} + +function unknownValue(value: unknown): DisplayValue { + let rendered: string + try { + rendered = JSON.stringify(value) ?? String(value) + } catch { + rendered = String(value) + } + return { compact: rendered, full: rendered, kind: "unknown" } +} + +function isObject(value: unknown): value is JsonObject { + return value !== null && typeof value === "object" && !Array.isArray(value) +} diff --git a/examples/patchwork/src/store/schema.ts b/examples/patchwork/src/store/schema.ts new file mode 100644 index 00000000..6029cb63 --- /dev/null +++ b/examples/patchwork/src/store/schema.ts @@ -0,0 +1,121 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +export interface StoreColumn { + name: string + type: string + primary: boolean + referenceTable?: string +} + +export interface StoreTable { + name: string + columns: StoreColumn[] +} + +type JsonObject = Record +type IrPath = string[][] + +export function readTables(json: string): StoreTable[] { + const schema: unknown = JSON.parse(json) + if (!isObject(schema) || !Array.isArray(schema.entities)) { + throw new TypeError("Store IR does not contain an entities array") + } + + return schema.entities.map((entry, index) => readTable(entry, index)) +} + +function readTable(entry: unknown, index: number): StoreTable { + if (!isObject(entry)) throw new TypeError(`Entity ${index} is not an object`) + const path = readPath(entry.path, `Entity ${index} path`) + const definition = entry.value + if (!isObject(definition)) { + throw new TypeError(`Entity ${pathName(path)} has no definition`) + } + const variant = definition.entityVariant + if (!isObject(variant) || variant.tag !== "table") { + throw new TypeError(`Entity ${pathName(path)} is not a table`) + } + if (!Array.isArray(definition.columns)) { + throw new TypeError(`Table ${pathName(path)} has no columns array`) + } + + const primaryKey = definition.primaryKey + const primaryPaths = Array.isArray(primaryKey) + ? primaryKey.map((value, column) => + pathName(readPath(value, `Primary key ${column}`)), + ) + : [] + + return { + name: pathName(path), + columns: definition.columns.map((column, columnIndex) => { + if (!isObject(column)) { + throw new TypeError( + `Column ${columnIndex} in ${pathName(path)} is invalid`, + ) + } + const columnPath = readPath( + column.path, + `Column ${columnIndex} in ${pathName(path)}`, + ) + const name = pathName(columnPath) + const referenceTable = referenceTableName(column.type) + return { + name, + type: typeName(column.type), + primary: primaryPaths.includes(name), + ...(referenceTable ? { referenceTable } : {}), + } + }), + } +} + +function readPath(value: unknown, label: string): IrPath { + if ( + !Array.isArray(value) || + value.length === 0 || + value.some( + (segment) => + !Array.isArray(segment) || + segment.length === 0 || + segment.some((part) => typeof part !== "string" || part === ""), + ) + ) { + throw new TypeError(`${label} is invalid`) + } + return value as IrPath +} + +function pathName(path: IrPath): string { + return path.flat().join(".") +} + +function typeName(value: unknown): string { + if (!isObject(value) || typeof value.tag !== "string") return "unknown" + if (value.tag === "builtin") { + if (value.type === "builtinInt") return "int" + if (value.type === "builtinString") return "string" + } + if (value.tag === "rowId") { + try { + return `ref ${pathName(readPath(value.path, "Row reference path"))}` + } catch { + return "ref" + } + } + return value.tag +} + +function referenceTableName(value: unknown): string | undefined { + if (!isObject(value) || value.tag !== "rowId") return undefined + try { + return pathName(readPath(value.path, "Row reference path")) + } catch { + return undefined + } +} + +function isObject(value: unknown): value is JsonObject { + return value !== null && typeof value === "object" && !Array.isArray(value) +} diff --git a/examples/patchwork/src/store/snapshot.ts b/examples/patchwork/src/store/snapshot.ts new file mode 100644 index 00000000..5fc6b9cd --- /dev/null +++ b/examples/patchwork/src/store/snapshot.ts @@ -0,0 +1,81 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +export type Snapshot = + | null + | boolean + | number + | string + | Snapshot[] + | { [key: string]: Snapshot } + +export function snapshot(value: unknown, maxDepth = 8): Snapshot { + try { + return visit(value, new WeakSet(), 0, maxDepth) + } catch { + return tagged("uninspectable") + } +} + +function visit( + value: unknown, + seen: WeakSet, + depth: number, + maxDepth: number, +): Snapshot { + if ( + value === null || + typeof value === "boolean" || + typeof value === "string" + ) { + return value + } + if (typeof value === "number") { + return Number.isFinite(value) ? value : tagged("number", String(value)) + } + if (typeof value === "undefined") return tagged("undefined") + if (typeof value === "bigint") return tagged("bigint", value.toString()) + if (typeof value === "symbol") + return tagged("symbol", value.description ?? "") + if (typeof value === "function") return tagged("function", value.name) + if (typeof value !== "object") return tagged("unknown", String(value)) + if (seen.has(value)) return tagged("circular") + if (depth >= maxDepth) return tagged("truncated") + + seen.add(value) + try { + if (value instanceof Error) { + return { + $type: "error", + name: value.name, + message: value.message, + ...(value.stack ? { stack: value.stack } : {}), + } + } + if (Array.isArray(value)) { + return value.map((item) => visit(item, seen, depth + 1, maxDepth)) + } + + const prototype = Object.getPrototypeOf(value) + if (prototype !== Object.prototype && prototype !== null) { + return tagged("instance", prototype?.constructor?.name ?? "Object") + } + + return Object.fromEntries( + Object.entries(Object.getOwnPropertyDescriptors(value)).map( + ([key, descriptor]) => [ + key, + "value" in descriptor + ? visit(descriptor.value, seen, depth + 1, maxDepth) + : tagged("accessor"), + ], + ), + ) + } finally { + seen.delete(value) + } +} + +function tagged(type: string, value?: string): { [key: string]: Snapshot } { + return value === undefined ? { $type: type } : { $type: type, value } +} diff --git a/examples/patchwork/src/store/source-storage.ts b/examples/patchwork/src/store/source-storage.ts new file mode 100644 index 00000000..86b5059d --- /dev/null +++ b/examples/patchwork/src/store/source-storage.ts @@ -0,0 +1,24 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +const prefix = "coln-patchwork:repl-source:" + +export const starterSource = `const doc = handle.doc() +console.log(JSON.parse(doc.jsonIR())) +return doc.heads()` + +export function loadSource(documentUrl: string): string { + try { + return localStorage.getItem(prefix + documentUrl) ?? starterSource + } catch { + return starterSource + } +} + +export function saveSource(documentUrl: string, source: string): void { + try { + localStorage.setItem(prefix + documentUrl, source) + } catch { + // The REPL remains usable when storage is unavailable. + } +} diff --git a/examples/patchwork/src/store/tool.ts b/examples/patchwork/src/store/tool.ts new file mode 100644 index 00000000..d145a559 --- /dev/null +++ b/examples/patchwork/src/store/tool.ts @@ -0,0 +1,15 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import type { ColnPointerDoc } from "../coln/pointer.ts" +import { mountTool } from "../lib/mount.ts" +import { injectTheme } from "../lib/theme.ts" +import type { PatchworkTool } from "../patchwork.ts" +import StoreTool from "./StoreTool.svelte" + +const tool: PatchworkTool = (handle, element) => { + injectTheme() + return mountTool(StoreTool as never, element, { handle, element }) +} + +export default tool diff --git a/examples/patchwork/src/theory/OutputPanel.svelte b/examples/patchwork/src/theory/OutputPanel.svelte new file mode 100644 index 00000000..c8984bbe --- /dev/null +++ b/examples/patchwork/src/theory/OutputPanel.svelte @@ -0,0 +1,57 @@ + + + + + +
+
+

{label}

+ {index} +
+
{@render children()}
+
+ + diff --git a/examples/patchwork/src/theory/SourceEditor.svelte b/examples/patchwork/src/theory/SourceEditor.svelte new file mode 100644 index 00000000..33872e01 --- /dev/null +++ b/examples/patchwork/src/theory/SourceEditor.svelte @@ -0,0 +1,27 @@ + + + + + + diff --git a/examples/patchwork/src/theory/StoresPanel.svelte b/examples/patchwork/src/theory/StoresPanel.svelte new file mode 100644 index 00000000..80a7a4b8 --- /dev/null +++ b/examples/patchwork/src/theory/StoresPanel.svelte @@ -0,0 +1,177 @@ + + + + + +
+
+

Stores

+

Stores created from compiled realms

+
+ {stores.length.toString().padStart(2, "0")} +
+ +
+ {#if realms.length > 0} + {#if realms.length > 1} + + {:else} +
+ Realm + {realms[0].name} +
+ {/if} + + + {#if hint}

{hint}

{/if} + {:else} +

Define and compile at least one realm before creating a store.

+ {/if} + + {#if error}{/if} +
+ +
+ {#if stores.length === 0} +

No stores created for this theory

+ {:else} + {#each stores as store, index (store.url)} +
+
+
+

{store.realmName}

+ +
+ {String(stores.length - index).padStart(2, "0")} +
+ {displayUrl(store.url)} + + {store.sourceHeads.length} source {store.sourceHeads.length === 1 ? "head" : "heads"} + +
+ + +
+
+ {/each} + {/if} +
+ + diff --git a/examples/patchwork/src/theory/TheoryTool.svelte b/examples/patchwork/src/theory/TheoryTool.svelte new file mode 100644 index 00000000..ecee747b --- /dev/null +++ b/examples/patchwork/src/theory/TheoryTool.svelte @@ -0,0 +1,364 @@ + + + + + +
+ {#if feedback}{/if} + +
+
+
+
+

Theory source

+

Source for this Coln theory

+
+
+ {statusLabel} + + {sourceLines} {sourceLines === 1 ? "line" : "lines"} + +
+
+
+ + {#if theory.source === ""} + + {/if} +
+
+ +
+
+ {#if error} + + {/if} + + {#if compilation.diagnosticsHtml.length === 0} +

No compilation diagnostics

+ {:else} +
+ {#each compilation.diagnosticsHtml as diagnostic} + +
{@html diagnostic}
+ {/each} +
+ {/if} +
+ + {#if compilation.prettyIr.length === 0} +

No compiled intermediate representation

+ {:else} +
+ {#each compilation.prettyIr as realm}
{realm}
{/each} +
+ {/if} +
+ + {#if compilation.irJson === ""} +

No compiled IR JSON

+ {:else} + + {/if} +
+
+
+ + +
+
+ + diff --git a/examples/patchwork/src/theory/compiled-realms.ts b/examples/patchwork/src/theory/compiled-realms.ts new file mode 100644 index 00000000..b2c8de94 --- /dev/null +++ b/examples/patchwork/src/theory/compiled-realms.ts @@ -0,0 +1,34 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import { isRealmSchema, type RealmSchema } from "./theory-document.ts" + +export type CompiledRealm = { + index: number + name: string + schema: RealmSchema +} + +export function parseCompiledRealms(irJson: string): CompiledRealm[] { + if (irJson === "") return [] + const value: unknown = JSON.parse(irJson) + if (!Array.isArray(value)) throw new TypeError("Compiler IR must be an array of realms") + + return value.map((schema, index) => { + if (!isRealmSchema(schema)) { + throw new TypeError(`Compiler IR realm ${index + 1} is malformed`) + } + return { index, name: realmName(schema, index), schema } + }) +} + +function realmName(schema: RealmSchema, index: number): string { + for (const entry of [...schema.entities, ...schema.rules]) { + if (typeof entry !== "object" || entry === null) continue + const path = (entry as { path?: unknown }).path + if (!Array.isArray(path) || !Array.isArray(path[0])) continue + if (path[0].every(part => typeof part === "string")) return path[0].join(".") + } + return `Realm ${index + 1}` +} diff --git a/examples/patchwork/src/theory/compiler.ts b/examples/patchwork/src/theory/compiler.ts new file mode 100644 index 00000000..39795cbf --- /dev/null +++ b/examples/patchwork/src/theory/compiler.ts @@ -0,0 +1,133 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import * as wasi from "@bjorn3/browser_wasi_shim" +// Staged by scripts/stage-compiler.mjs. Imported as urls rather than resolved +// against `import.meta.url` so vite emits them and rewrites the paths: this +// module is a chunk under dist/assets/, not a file at the root of dist/. +import stagedJsffiUrl from "./compiler-dist/ghc_wasm_jsffi.js?url" +import stagedWasmUrl from "./compiler-dist/coln.wasm?url" + +export type Compilation = { + diagnosticsHtml: string[] + prettyIr: string[] + irJson: string +} + +type CompileResultPointer = unknown + +type CompilerExports = { + compile(source: string): Promise + freeCompileResult(pointer: CompileResultPointer): Promise | void + getDiagnostics(asHtml: boolean, pointer: CompileResultPointer): Promise | string[] + prettyIr(pointer: CompileResultPointer): Promise | string[] + irToJson(pointer: CompileResultPointer): Promise | string +} + +type Jsffi = (exports: Partial) => WebAssembly.ModuleImports + +export type Compiler = { + compile(source: string): Promise +} + +const DIST_KEY = "coln-patchwork:compiler-dist" + +/** + * The compiler's two browser files: `ghc_wasm_jsffi.js`, the JSFFI glue GHC + * generates, and `coln.wasm` itself. They ship with this bundle, so the tool + * needs nothing from the network beyond the module it was loaded from. + * + * Override the pair with + * + * localStorage["coln-patchwork:compiler-dist"] = "https://host/dist/" + * + * to load them from a deployed Coln Lab instead of carrying ~4.4MB of wasm + * through the module — worth doing if pushwork is unhappy holding it. Read once + * per page: changing it takes a reload. + */ +function compilerUrls(): { jsffi: string; wasm: string } { + let override = "" + try { + override = localStorage.getItem(DIST_KEY) ?? "" + } catch { + override = "" + } + if (!override) return { jsffi: stagedJsffiUrl, wasm: stagedWasmUrl } + const base = override.endsWith("/") ? override : `${override}/` + return { jsffi: `${base}ghc_wasm_jsffi.js`, wasm: `${base}coln.wasm` } +} + +let compilerPromise: Promise | undefined + +export function loadCompiler(): Promise { + compilerPromise ??= initializeCompiler().catch(error => { + compilerPromise = undefined + throw error + }) + return compilerPromise +} + +async function initializeCompiler(): Promise { + const urls = compilerUrls() + const jsffiModule = await import(/* @vite-ignore */ urls.jsffi) + const ghcWasmJsffi = jsffiModule.default as Jsffi + const wasmBytes = await fetch(urls.wasm).then(response => { + if (!response.ok) { + throw new Error(`Could not fetch the Coln compiler: ${response.status}`) + } + return response.arrayBuffer() + }) + + /* + * What GHC's own `loadHaskellWasm.js` does, with the WASI shim bundled + * rather than imported from a CDN: that file's one import is a jsdelivr url, + * and a Patchwork tool should not need the public internet — or an escape + * from the page's connect-src — to compile a theory. Instantiated from bytes + * instead of streaming so the wasm loads whatever content type it is served + * with. + */ + const environment = new wasi.WASI( + [], + [], + [ + new wasi.OpenFile(new wasi.File([])), + wasi.ConsoleStdout.lineBuffered(message => + console.log(`[coln] ${message}`), + ), + wasi.ConsoleStdout.lineBuffered(message => + console.error(`[coln] ${message}`), + ), + ], + ) + const exports: Partial = {} + const { instance } = await WebAssembly.instantiate(wasmBytes, { + wasi_snapshot_preview1: environment.wasiImport, + ghc_wasm_jsffi: ghcWasmJsffi(exports), + }) + Object.assign(exports, instance.exports) + // A GHC "reactor" module: initialise it rather than run a main. The shim + // types this against its own instance shape, which is what we hand it. + environment.initialize(instance as unknown as Parameters[0]) + const compiler = exports as CompilerExports + let previousCompilation = Promise.resolve() + + return { + compile(source) { + const run = async () => { + const pointer = await compiler.compile(source) + try { + // GHC reactor exports must not be entered concurrently. + const prettyIr = await compiler.prettyIr(pointer) + const diagnosticsHtml = await compiler.getDiagnostics(true, pointer) + const irJson = await compiler.irToJson(pointer) + return { diagnosticsHtml, prettyIr, irJson } + } finally { + await compiler.freeCompileResult(pointer) + } + } + const compilation = previousCompilation.then(run, run) + previousCompilation = compilation.then(() => undefined, () => undefined) + return compilation + }, + } +} diff --git a/examples/patchwork/src/theory/datatype.ts b/examples/patchwork/src/theory/datatype.ts new file mode 100644 index 00000000..e819906b --- /dev/null +++ b/examples/patchwork/src/theory/datatype.ts @@ -0,0 +1,36 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import type { TheoryDocument } from "./theory-document.ts" + +export const THEORY_TYPE = "coln-theory" + +/** + * An ordinary Patchwork document: `source` is collaborative text, `stores` + * records the Coln stores compiled out of it. Nothing here needs Coln's repo, + * so a theory syncs, forks, versions and comments like any other Patchwork + * document — only the stores it links to live in Coln's world. + */ +export const ColnTheoryDatatype = { + init(doc: TheoryDocument) { + doc["@patchwork"] = { type: THEORY_TYPE } + doc.version = 1 + doc.title = "Untitled theory" + doc.source = "" + doc.stores = [] + }, + + getTitle(doc: TheoryDocument) { + return doc.title || "Untitled theory" + }, + + setTitle(doc: TheoryDocument, title: string) { + doc.title = title + }, + + markCopy(doc: TheoryDocument) { + doc.title = `Copy of ${doc.title || "Untitled theory"}` + }, +} + +export default ColnTheoryDatatype diff --git a/examples/patchwork/src/theory/graph-example.coln b/examples/patchwork/src/theory/graph-example.coln new file mode 100644 index 00000000..04a0be4c --- /dev/null +++ b/examples/patchwork/src/theory/graph-example.coln @@ -0,0 +1,7 @@ +theory Graph := sig + V : Set + E : V -> V -> Set +end + +realm GraphRealm @ Graph +end diff --git a/examples/patchwork/src/theory/stores.ts b/examples/patchwork/src/theory/stores.ts new file mode 100644 index 00000000..5780fbec --- /dev/null +++ b/examples/patchwork/src/theory/stores.ts @@ -0,0 +1,54 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import { colnDocType } from "@coln-project/repo" +import { colnRepo, trackDocument } from "../coln/repo.ts" +import { createPointerDoc } from "../coln/pointer.ts" +import { STORE_TYPE } from "../store/datatype.ts" +import type { CompiledRealm } from "./compiled-realms.ts" +import type { StoreRecord, TheoryHandle } from "./theory-document.ts" + +/** + * Create a Coln store for one compiled realm, then record it on the theory. + * + * Two documents in two repos come out of this: the store itself in Coln's repo + * (flushed to Coln's relay before it is linked, so a collaborator following + * the link finds something there), and a Patchwork pointer document so the + * store can be opened as a Patchwork document at all. + */ +export async function createStore( + theory: TheoryHandle, + realm: CompiledRealm, +): Promise { + const repo = colnRepo() + const createdAt = Date.now() + const sourceHeads = [...theory.heads()] + const store = repo.create(realm.schema, colnDocType) + + trackDocument(store.documentId) + await repo.flush([store.documentId]) + + const docUrl = await createPointerDoc({ + type: STORE_TYPE, + title: `${realm.name} store`, + storeUrl: store.url, + realmName: realm.name, + theoryUrl: theory.url, + }) + + const record: StoreRecord = { + url: store.url, + createdAt, + sourceHeads, + realmIndex: realm.index, + realmName: realm.name, + ir: realm.schema, + docUrl, + } + theory.change(document => { + document.stores.push(record) + }) + + return record +} + diff --git a/examples/patchwork/src/theory/theory-document.ts b/examples/patchwork/src/theory/theory-document.ts new file mode 100644 index 00000000..d3b9f63d --- /dev/null +++ b/examples/patchwork/src/theory/theory-document.ts @@ -0,0 +1,63 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import type { PatchworkHandle, PatchworkMeta } from "../patchwork.ts" + +export type RealmSchema = { + entities: unknown[] + rules: unknown[] +} + +export type StoreRecord = { + /** The `coln:` url of the store itself. */ + url: string + createdAt: number + sourceHeads: string[] + realmIndex: number + realmName: string + ir: RealmSchema + /** + * The Patchwork pointer document opened to browse this store, when one has + * been made. Absent on records written by Coln Lab, which had no Patchwork + * documents to point with — the Store Editor makes one on demand. + */ + docUrl?: string +} + +export type TheoryDocument = { + "@patchwork"?: PatchworkMeta + version: 1 + title: string + source: string + stores: StoreRecord[] +} + +export type TheoryHandle = PatchworkHandle + +export function isTheoryDocument(value: unknown): value is TheoryDocument { + if (typeof value !== "object" || value === null) return false + const document = value as Partial + return document.version === 1 + && typeof document.source === "string" + && Array.isArray(document.stores) + && document.stores.every(isStoreRecord) +} + +export function isRealmSchema(value: unknown): value is RealmSchema { + if (typeof value !== "object" || value === null) return false + const schema = value as Partial + return Array.isArray(schema.entities) && Array.isArray(schema.rules) +} + +function isStoreRecord(value: unknown): value is StoreRecord { + if (typeof value !== "object" || value === null) return false + const store = value as Partial + return typeof store.url === "string" + && typeof store.createdAt === "number" + && Number.isFinite(store.createdAt) + && Array.isArray(store.sourceHeads) + && store.sourceHeads.every(head => typeof head === "string") + && Number.isInteger(store.realmIndex) + && typeof store.realmName === "string" + && isRealmSchema(store.ir) +} diff --git a/examples/patchwork/src/theory/tool.ts b/examples/patchwork/src/theory/tool.ts new file mode 100644 index 00000000..6a5c5c15 --- /dev/null +++ b/examples/patchwork/src/theory/tool.ts @@ -0,0 +1,15 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import { mountTool } from "../lib/mount.ts" +import { injectTheme } from "../lib/theme.ts" +import type { PatchworkTool } from "../patchwork.ts" +import TheoryTool from "./TheoryTool.svelte" +import type { TheoryDocument } from "./theory-document.ts" + +const tool: PatchworkTool = (handle, element) => { + injectTheme() + return mountTool(TheoryTool as never, element, { handle, element }) +} + +export default tool diff --git a/examples/patchwork/tests/datatypes.test.ts b/examples/patchwork/tests/datatypes.test.ts new file mode 100644 index 00000000..6e12f426 --- /dev/null +++ b/examples/patchwork/tests/datatypes.test.ts @@ -0,0 +1,74 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import { describe, expect, it } from "vitest" +import { pointerDatatype, type ColnPointerDoc } from "../src/coln/pointer.ts" +import { ColnTheoryDatatype } from "../src/theory/datatype.ts" +import { + isTheoryDocument, + type TheoryDocument, +} from "../src/theory/theory-document.ts" + +describe("theory datatype", () => { + it("seeds a document Patchwork can title and Coln can compile", () => { + const doc = {} as TheoryDocument + ColnTheoryDatatype.init(doc) + + expect(doc["@patchwork"]).toEqual({ type: "coln-theory" }) + expect(ColnTheoryDatatype.getTitle(doc)).toBe("Untitled theory") + expect(isTheoryDocument(doc)).toBe(true) + }) + + it("renames and copies through the title field", () => { + const doc = {} as TheoryDocument + ColnTheoryDatatype.init(doc) + ColnTheoryDatatype.setTitle(doc, "Graphs") + expect(ColnTheoryDatatype.getTitle(doc)).toBe("Graphs") + ColnTheoryDatatype.markCopy(doc) + expect(ColnTheoryDatatype.getTitle(doc)).toBe("Copy of Graphs") + }) + + it("rejects documents that are not theories", () => { + expect(isTheoryDocument({ version: 1, source: "", stores: [{}] })).toBe(false) + expect(isTheoryDocument({ version: 2, source: "", stores: [] })).toBe(false) + expect(isTheoryDocument(null)).toBe(false) + }) + + it("accepts a theory written by Coln Lab, which has no title", () => { + const lab = { + version: 1, + source: "theory Graph := sig end", + stores: [ + { + url: "coln:abc", + createdAt: 1, + sourceHeads: ["head"], + realmIndex: 0, + realmName: "GraphRealm", + ir: { entities: [], rules: [] }, + }, + ], + } + expect(isTheoryDocument(lab)).toBe(true) + }) +}) + +describe("pointer datatype", () => { + const datatype = pointerDatatype("coln-store", "Coln store") + + it("starts unaimed, so the tool asks for a store url", () => { + const doc = {} as ColnPointerDoc + datatype.init(doc) + expect(doc).toEqual({ + "@patchwork": { type: "coln-store" }, + title: "Coln store", + storeUrl: "", + }) + }) + + it("keeps no undefined fields, which automerge cannot store", () => { + const doc = {} as ColnPointerDoc + datatype.init(doc) + expect(Object.values(doc).every(value => value !== undefined)).toBe(true) + }) +}) diff --git a/examples/patchwork/tests/entry.node.test.ts b/examples/patchwork/tests/entry.node.test.ts new file mode 100644 index 00000000..69194a48 --- /dev/null +++ b/examples/patchwork/tests/entry.node.test.ts @@ -0,0 +1,60 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import { existsSync } from "node:fs" +import { describe, expect, it } from "vitest" + +/** + * Patchwork imports the built entry module inside a Web Worker to list the + * plugins, then structuredClones each entry to the main thread. So the entry + * must load with no document and no importmap, and carry nothing but + * serialisable metadata plus `load`, which the host strips first. + * + * Node with no DOM is a fair stand-in. Skipped when there is no build yet. + */ +const entry = new URL("../dist/index.js", import.meta.url) + +describe.skipIf(!existsSync(entry))("the built entry module", () => { + it("lists every plugin without touching the DOM", async () => { + expect(globalThis.document).toBeUndefined() + const { plugins } = (await import(entry.href)) as { + plugins: Array> + } + + expect(plugins.map(plugin => [plugin.type, plugin.id])).toEqual([ + ["patchwork:datatype", "coln-theory"], + ["patchwork:tool", "coln-theory"], + ["patchwork:datatype", "coln-store"], + ["patchwork:tool", "coln-store"], + ["patchwork:datatype", "coln-graph"], + ["patchwork:tool", "coln-graph"], + ]) + }) + + it("carries only clonable metadata beside load()", async () => { + const { plugins } = (await import(entry.href)) as { + plugins: Array> + } + + for (const plugin of plugins) { + expect(typeof plugin.load).toBe("function") + const { load: _load, ...metadata } = plugin + expect(Object.values(metadata).some(value => typeof value === "function")).toBe(false) + expect(() => structuredClone(metadata)).not.toThrow() + } + }) + + it("pins each tool to a datatype of the same id", async () => { + const { plugins } = (await import(entry.href)) as { + plugins: Array> + } + const datatypes = new Set( + plugins.filter(p => p.type === "patchwork:datatype").map(p => p.id), + ) + + for (const tool of plugins.filter(p => p.type === "patchwork:tool")) { + expect(tool.supportedDatatypes).toEqual([tool.id]) + expect(datatypes.has(tool.id)).toBe(true) + } + }) +}) diff --git a/examples/patchwork/tests/evaluate.test.ts b/examples/patchwork/tests/evaluate.test.ts new file mode 100644 index 00000000..0876bb8b --- /dev/null +++ b/examples/patchwork/tests/evaluate.test.ts @@ -0,0 +1,34 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import type { ColnHandle } from "@coln-project/repo" +import { describe, expect, it } from "vitest" +import { evaluate } from "../src/store/evaluate.ts" + +const handle = { marker: "handle" } as unknown as ColnHandle + +describe("evaluate", () => { + it("awaits programs and captures console output", async () => { + const result = await evaluate( + 'console.log("ready", 2); await Promise.resolve(); return handle.marker', + handle, + ) + expect(result).toMatchObject({ + ok: true, + result: "handle", + console: [{ level: "log", values: ["ready", 2] }], + }) + }) + + it("separates syntax and runtime failures", async () => { + expect(await evaluate("return )", handle)).toMatchObject({ + ok: false, + phase: "syntax", + }) + expect(await evaluate('throw new Error("broken")', handle)).toMatchObject({ + ok: false, + phase: "runtime", + error: { $type: "error", message: "broken" }, + }) + }) +}) diff --git a/examples/patchwork/tests/format.test.ts b/examples/patchwork/tests/format.test.ts new file mode 100644 index 00000000..37f3f1e3 --- /dev/null +++ b/examples/patchwork/tests/format.test.ts @@ -0,0 +1,28 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import { describe, expect, it } from "vitest" +import { displayRowRef, displayValue } from "../src/store/format.ts" + +describe("value formatting", () => { + it("formats primitive values", () => { + expect(displayValue({ tag: "string", value: "Ada" })).toMatchObject({ + compact: "Ada", + kind: "string", + }) + expect(displayValue({ tag: "int", value: 37 })).toMatchObject({ + compact: "37", + kind: "int", + }) + }) + + it("compacts committed references without losing the full ID", () => { + const commit = + "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef" + expect(displayRowRef({ existing: { commit, counter: 4 } })).toEqual({ + compact: "12345678…cdef:4", + full: `${commit}:4`, + kind: "ref", + }) + }) +}) diff --git a/examples/patchwork/tests/graph-store.node.test.ts b/examples/patchwork/tests/graph-store.node.test.ts new file mode 100644 index 00000000..a0eaa545 --- /dev/null +++ b/examples/patchwork/tests/graph-store.node.test.ts @@ -0,0 +1,33 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import { Repo } from "@automerge/automerge-repo" +import { create } from "@coln-project/repo" +import { describe, expect, it } from "vitest" +import * as GraphRealm from "../src/graph/generated/GraphRealm.ts" +import { addEdge, addVertex, readGraph } from "../src/graph/graph.ts" + +/** + * The integration this port rests on: the forked automerge-repo, the Coln + * store runtime and the checked-in GraphRealm bindings, driven by the same + * graph.ts the Graph Demo uses. No storage and no relay — just the store. + */ +describe("graph store", () => { + it("records vertices and directed edges", () => { + const repo = new Repo() + const handle = create(repo, GraphRealm) + + addVertex(handle) + addVertex(handle) + const { vertices } = readGraph(handle.doc()) + expect(vertices.map(vertex => vertex.label)).toEqual(["V1", "V2"]) + + addEdge(handle, vertices[0], vertices[1]) + const graph = readGraph(handle.doc()) + expect(graph.edges).toHaveLength(1) + expect(graph.edges[0]).toMatchObject({ + fromId: vertices[0].id, + toId: vertices[1].id, + }) + }) +}) diff --git a/examples/patchwork/tests/schema.test.ts b/examples/patchwork/tests/schema.test.ts new file mode 100644 index 00000000..f46c7c91 --- /dev/null +++ b/examples/patchwork/tests/schema.test.ts @@ -0,0 +1,62 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import { describe, expect, it } from "vitest" +import { readTables } from "../src/store/schema.ts" + +describe("readTables", () => { + it("extracts paths, types, and primary columns", () => { + const tables = readTables( + JSON.stringify({ + entities: [ + { + path: [["Realm"], ["profile"]], + value: { + entityVariant: { tag: "table" }, + columns: [ + { + path: [["owner"]], + type: { tag: "rowId", path: [["Realm"], ["Person"]] }, + }, + { + path: [["details"], ["name"]], + type: { tag: "builtin", type: "builtinString" }, + }, + ], + primaryKey: [[["owner"]]], + }, + }, + ], + rules: [], + }), + ) + + expect(tables).toEqual([ + { + name: "Realm.profile", + columns: [ + { + name: "owner", + type: "ref Realm.Person", + primary: true, + referenceTable: "Realm.Person", + }, + { name: "details.name", type: "string", primary: false }, + ], + }, + ]) + }) + + it("rejects malformed IR", () => { + expect(() => readTables('{"rules":[]}')).toThrow("entities array") + expect(() => + readTables( + JSON.stringify({ + entities: [ + { path: [["Realm"]], value: { entityVariant: { tag: "view" } } }, + ], + }), + ), + ).toThrow("is not a table") + }) +}) diff --git a/examples/patchwork/tests/setup-wasm.ts b/examples/patchwork/tests/setup-wasm.ts new file mode 100644 index 00000000..9044e719 --- /dev/null +++ b/examples/patchwork/tests/setup-wasm.ts @@ -0,0 +1,24 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +/** + * Initialise the automerge and Subduction wasm modules for the store tests. + * + * Both packages' `/slim` entries — the ones the Coln repo fork imports — leave + * this to the host on purpose. In the browser Patchwork does it before any + * tool loads (core/patchwork/src/repo.ts); here the test runner does it. + */ + +import { readFileSync } from "node:fs" +import { createRequire } from "node:module" +import { initializeWasm } from "@automerge/automerge/slim" +// eslint-disable-next-line +// @ts-expect-error initSync is a wasm-bindgen runtime helper, absent from the types +import { initSync as initSubduction } from "@automerge/automerge-subduction/slim" + +const require = createRequire(import.meta.url) + +await initializeWasm( + readFileSync(require.resolve("@automerge/automerge/automerge.wasm")), +) +initSubduction(readFileSync(require.resolve("@automerge/automerge-subduction/wasm"))) diff --git a/examples/patchwork/tests/snapshot.test.ts b/examples/patchwork/tests/snapshot.test.ts new file mode 100644 index 00000000..3d2a2f62 --- /dev/null +++ b/examples/patchwork/tests/snapshot.test.ts @@ -0,0 +1,41 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import { describe, expect, it } from "vitest" +import { snapshot } from "../src/store/snapshot.ts" + +describe("snapshot", () => { + it("copies plain values and marks cycles", () => { + const value: Record = { answer: 42 } + value.self = value + expect(snapshot(value)).toEqual({ answer: 42, self: { $type: "circular" } }) + }) + + it("does not invoke accessors or retain class instances", () => { + let accessed = false + const value = Object.defineProperty({}, "secret", { + enumerable: true, + get() { + accessed = true + return 1 + }, + }) + expect(snapshot(value)).toEqual({ secret: { $type: "accessor" } }) + expect(accessed).toBe(false) + expect(snapshot(new Date(0))).toEqual({ $type: "instance", value: "Date" }) + }) + + it("contains objects that throw during inspection", () => { + const { proxy, revoke } = Proxy.revocable({}, {}) + revoke() + expect(snapshot(proxy)).toEqual({ $type: "uninspectable" }) + }) + + it("preserves an own __proto__ property", () => { + const value = Object.create(null) as Record + value.__proto__ = "data" + const result = snapshot(value) + expect(Object.prototype.hasOwnProperty.call(result, "__proto__")).toBe(true) + expect((result as Record).__proto__).toBe("data") + }) +}) diff --git a/examples/patchwork/tests/source-storage.test.ts b/examples/patchwork/tests/source-storage.test.ts new file mode 100644 index 00000000..4bce776d --- /dev/null +++ b/examples/patchwork/tests/source-storage.test.ts @@ -0,0 +1,31 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest" +import { + loadSource, + saveSource, + starterSource, +} from "../src/store/source-storage.ts" + +const values = new Map() + +beforeEach(() => { + values.clear() + vi.stubGlobal("localStorage", { + getItem: (key: string) => values.get(key) ?? null, + setItem: (key: string, value: string) => values.set(key, value), + }) +}) + +afterEach(() => vi.unstubAllGlobals()) + +describe("source storage", () => { + it("stores source independently by document URL", () => { + expect(loadSource("coln:one")).toBe(starterSource) + saveSource("coln:one", "return 1") + saveSource("coln:two", "return 2") + expect(loadSource("coln:one")).toBe("return 1") + expect(loadSource("coln:two")).toBe("return 2") + }) +}) diff --git a/examples/sync-demo/tsconfig.json b/examples/patchwork/tsconfig.json similarity index 80% rename from examples/sync-demo/tsconfig.json rename to examples/patchwork/tsconfig.json index 2609a539..3e943926 100644 --- a/examples/sync-demo/tsconfig.json +++ b/examples/patchwork/tsconfig.json @@ -3,7 +3,6 @@ "target": "ES2022", "useDefineForClassFields": true, "lib": ["DOM", "DOM.Iterable", "ES2022"], - "allowJs": false, "skipLibCheck": true, "esModuleInterop": true, "allowSyntheticDefaultImports": true, @@ -15,9 +14,7 @@ "allowImportingTsExtensions": true, "isolatedModules": true, "noEmit": true, - "jsx": "react-jsx", - "types": ["vite/client"] + "types": ["vite/client", "node"] }, - "include": ["src"], - "references": [] + "include": ["src", "tests", "vite.config.ts", "vitest.config.ts"] } diff --git a/examples/patchwork/vite.config.ts b/examples/patchwork/vite.config.ts new file mode 100644 index 00000000..85e32c15 --- /dev/null +++ b/examples/patchwork/vite.config.ts @@ -0,0 +1,83 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import { svelte } from "@sveltejs/vite-plugin-svelte" +import bootloaderExternals from "@inkandswitch/patchwork-bootloader/externals" +import { defineConfig } from "vite" +import wasm from "vite-plugin-wasm" + +/** + * Modules Patchwork's importmap serves to tool code at runtime. Listing them + * here rather than trusting the installed bootloader's `externals` alone: that + * list grows with the host, and an external we bundle by mistake would give us + * a second copy of a wasm module the page has already initialised. + * + * Automerge and Subduction matter most. Patchwork initialises both wasm + * modules on the main thread (`core/patchwork/src/repo.ts`) at exactly the + * versions Coln pins — automerge 3.3.2, subduction 0.16.1 — so the Coln repo + * we build below shares those instances instead of loading its own. + */ +const hostProvided = [ + "@automerge/automerge", + "@automerge/automerge/slim", + "@automerge/automerge-subduction", + "@automerge/automerge-subduction/slim", + "@codemirror/state", + "@codemirror/view", + "@inkandswitch/patchwork-bootloader", + "@inkandswitch/patchwork-elements", + "@inkandswitch/patchwork-filesystem", + "@inkandswitch/patchwork-plugins", +] + +/** + * Bundled on purpose. Coln stores are a custom automerge-repo document type + * (`defineDocumentType`, `urlScheme: "coln"`), and neither exists in any + * published automerge-repo — only in the fork Coln depends on. Patchwork's + * importmap therefore cannot supply it, so this package ships the fork and + * stands up its own `Repo` against Coln's relay. See README. + */ +function bundledAnyway(id: string): boolean { + return ( + id === "@automerge/automerge-repo" || + id.startsWith("@automerge/automerge-repo/") || + // Dependency-free, and bundling it means these tools do not also depend on + // the host importmap carrying a storage adapter. + id.startsWith("@automerge/automerge-repo-storage-indexeddb") + ) +} + +const external = [ + ...new Set([...bootloaderExternals, ...hostProvided]), +].filter(id => !bundledAnyway(id)) + +export default defineConfig({ + // Relative, because Patchwork serves a tool's files from wherever the module + // document lives — not the origin root. With the default base, vite's + // preload helper would point its s at "/assets/…" and every chunk + // would 404. + base: "./", + // No public directory: files copied there land at the root of dist/, while + // the chunk that wants them runs from dist/assets/. The compiler is staged + // into src/theory/compiler-dist/ and imported `?url` instead, so vite emits + // it as an asset and writes the url. + publicDir: false, + // `emitCss: false` keeps every component's styles inside its own JS chunk, + // which is what a Patchwork tool needs: there is no stylesheet link to add, + // and no CSS may land in the entry module — Patchwork reads that inside a + // Web Worker, where there is no document to inject into. The one shared + // stylesheet is imported `?inline` and added by lib/theme.ts. + plugins: [wasm(), svelte({ emitCss: false })], + resolve: { dedupe: ["svelte", "@coln-project/runtime"] }, + build: { + minify: false, + sourcemap: true, + target: "esnext", + rollupOptions: { + external, + input: "./src/index.ts", + output: { format: "es", entryFileNames: "[name].js" }, + preserveEntrySignatures: "strict", + }, + }, +}) diff --git a/examples/patchwork/vitest.config.ts b/examples/patchwork/vitest.config.ts new file mode 100644 index 00000000..33c3988b --- /dev/null +++ b/examples/patchwork/vitest.config.ts @@ -0,0 +1,43 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import { svelte } from "@sveltejs/vite-plugin-svelte" +import { defineConfig } from "vitest/config" + +/** + * Two projects, because the Coln store runtime is wasm with one build per + * environment. + * + * `unit` runs the browser-shaped code (the tools are browser-only) with the + * browser export conditions, like the bundle does. `store` runs the tests that + * actually stand up a Coln store: under Node's conditions automerge, + * Subduction and the Coln runtime each initialise their own wasm, where the + * browser builds expect the host page to have done it (Patchwork does). + */ +export default defineConfig({ + test: { + passWithNoTests: true, + projects: [ + { + plugins: [svelte()], + resolve: { conditions: ["browser"], dedupe: ["svelte"] }, + test: { + name: "unit", + environment: "happy-dom", + globals: true, + include: ["tests/**/*.test.ts"], + exclude: ["tests/**/*.node.test.ts"], + }, + }, + { + test: { + name: "store", + environment: "node", + globals: true, + setupFiles: ["tests/setup-wasm.ts"], + include: ["tests/**/*.node.test.ts"], + }, + }, + ], + }, +}) diff --git a/examples/style.css b/examples/style.css deleted file mode 100644 index 87035491..00000000 --- a/examples/style.css +++ /dev/null @@ -1,115 +0,0 @@ -body { - --hue: 220; - --sat: 32%; - --main-bg: hsl(var(--hue), var(--sat), 16%); - --input-bg: hsl(var(--hue), var(--sat), 8%); - --alt-bg: hsl(var(--hue), var(--sat), 13%); - --border: hsl(var(--hue), var(--sat), 30%); - --accent: hsl(var(--hue), var(--sat), 75%); - --text-hue: 39; - --text-sat: 70%; - --text: hsl(var(--text-hue), var(--text-sat), 88%); - --debug: hsl(290, var(--text-sat), 55%); - --info: hsl(215, var(--text-sat), 55%); - --warning: hsl(45, var(--text-sat), 55%); - --error: hsl(355, var(--text-sat), 55%); - font-family: Helvetica, Arial, sans-serif; - background: var(--main-bg); - color: var(--text); - max-width: 60em; - margin: 2em auto; - > * { - margin: 0 1em; - } -} -textarea, -pre { - padding: 0.8em 1em; -} -a { - color: var(--accent); - &:hover { - color: hsl(var(--hue), var(--sat), 90%); - } -} -textarea, -select { - color: inherit; - &:focus { - border-color: var(--accent); - outline: none; - } -} -textarea { - background: var(--input-bg); - resize: vertical; - border: solid 1px var(--border); -} -pre { - margin: 0; - white-space: pre-wrap; -} -select { - background: var(--input-bg); - border: solid 1px var(--border); - padding: 0.5em 1em; -} -body[data-compiling] { - & .output { - opacity: 0.5; - } -} -.main { - display: grid; - grid-template-columns: auto 1fr; - grid-template-rows: auto auto repeat(3, minmax(2.5em, auto)); - gap: 1em 1em; - > h2 { - text-align: right; - } - > * { - margin: 0; - } -} -.header { - grid-column: 1 / -1; - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 1em; - > h1 { - padding-left: 3em; - margin: 0; - } -} -#input, -#examples, -.output { - border-radius: 0.3em; -} -.output { - border: solid 1px var(--border); - background: var(--alt-bg); - > :not(:last-child) { - border-bottom: solid 1px var(--border); - } -} -#json { - padding: 1em; -} -.diag-debug { - --diag-colour: var(--debug); -} -.diag-info { - --diag-colour: var(--info); -} -.diag-warning { - --diag-colour: var(--warning); -} -.diag-error { - --diag-colour: var(--error); -} -[class^="ann-"] { - color: var(--diag-colour); - font-weight: bold; -} diff --git a/examples/sync-demo/.gitignore b/examples/sync-demo/.gitignore deleted file mode 100644 index f06235c4..00000000 --- a/examples/sync-demo/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -node_modules -dist diff --git a/examples/sync-demo/README.md b/examples/sync-demo/README.md deleted file mode 100644 index 071626c7..00000000 --- a/examples/sync-demo/README.md +++ /dev/null @@ -1,69 +0,0 @@ -# Coln + automerge-repo web demo - -This demo stores a compiler-generated Coln realm in `automerge-repo` using a -custom document type. Custom document types aren't actually a thing in -`automerge-repo` (yet), so we use a vendored version of `automerge-repo` from -the `doctypes` branch which adds support for custom document types. - -We define a function which takes a compiled FFI bindings from compiler -and turns it into an automerge-repo document type. This is the `colnDocType` -function. - -Then, we can create and find handles containing these documents, modify and -render them, and synchronize them using the public subduction sycn server. -That's what most of the code here does. - -## Compiling the schema - -The demo source schema is: - -- `graph.coln` - -The generated FFI/IR artifacts live in: - -- `src/generated/Graph.ts` -- `src/generated/GraphRealm.ts` -- `src/generated/GraphRealm.json` - -`GraphRealm.ts` exports the generated `View`, `Transaction`, and `schema`. The -generic `src/colnDocType.ts` consumes that compiled FFI directly: - -```ts -const coln = colnDocType(GraphRealm) -``` - -`GraphRealm.json` is passed directly to `StoreHandle.fromTheory(...)`; no local -IR-to-FlatTheory translation is needed anymore. - -You can compile them by running `./compile.sh` - -## Run - -From `coln/examples/sync-demo/`: - -```bash -pnpm install -pnpm dev -``` - -Open the Vite URL, then open the hash URL shown in the page in another tab. Each -tab creates its own Repo and syncs via Subduction. - -## Build - -```bash -pnpm build -``` - -The build script runs: - -1. `./compile.sh` to regenerate `src/generated/*` from `graph.coln` -2. `tsc --noEmit` -3. `vite build` - -## Test - -```bash -pnpm exec playwright install chromium # first time only -pnpm test:e2e -``` diff --git a/examples/sync-demo/compile.sh b/examples/sync-demo/compile.sh deleted file mode 100755 index f19b6bd8..00000000 --- a/examples/sync-demo/compile.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -if command -v coln >/dev/null 2>&1; then - coln_cli=(coln) -elif command -v coln-cli >/dev/null 2>&1; then - coln_cli=(coln-cli) -else - coln_cli=(cabal run coln-cli --) -fi - -"${coln_cli[@]}" generate-ts ./graph.coln --output-dir ./src/generated -"${coln_cli[@]}" generate-ir ./graph.coln --output-dir ./src/generated - diff --git a/examples/sync-demo/package.json b/examples/sync-demo/package.json deleted file mode 100644 index eaf37cb6..00000000 --- a/examples/sync-demo/package.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "name": "coln-sync-demo", - "private": true, - "type": "module", - "scripts": { - "dev": "vite --host 0.0.0.0", - "server": "node server.mjs", - "compile": "bash ./compile.sh", - "build": "pnpm run compile && tsc --noEmit && vite build", - "test:e2e": "playwright test" - }, - "dependencies": { - "@automerge/automerge-repo": "file:./vendor/automerge-automerge-repo-2.6.0-subduction.33.tgz", - "@automerge/automerge-subduction": "0.16.0", - "@coln-project/coln-js-runtime": "link:../../packages/coln-js-runtime", - "@coln-project/runtime": "link:../../packages/coln-js-runtime", - "@vitejs/plugin-react": "^6.0.2", - "react": "^19.2.6", - "react-dom": "^19.2.6", - "typescript": "^6.0.3", - "vite": "^8.0.14", - "vite-plugin-wasm": "^3.6.0", - "ws": "^8.21.0" - }, - "devDependencies": { - "@playwright/test": "^1.61.0", - "@types/react": "^19.2.17", - "@types/react-dom": "^19.2.3" - } -} diff --git a/examples/sync-demo/pnpm-lock.yaml b/examples/sync-demo/pnpm-lock.yaml deleted file mode 100644 index b77306e2..00000000 --- a/examples/sync-demo/pnpm-lock.yaml +++ /dev/null @@ -1,847 +0,0 @@ -lockfileVersion: '9.0' - -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false - -importers: - - .: - dependencies: - '@automerge/automerge-repo': - specifier: file:./vendor/automerge-automerge-repo-2.6.0-subduction.33.tgz - version: file:vendor/automerge-automerge-repo-2.6.0-subduction.33.tgz - '@automerge/automerge-subduction': - specifier: 0.16.0 - version: 0.16.0 - '@coln-project/coln-js-runtime': - specifier: link:../../packages/coln-js-runtime - version: link:../../packages/coln-js-runtime - '@coln-project/runtime': - specifier: link:../../packages/coln-js-runtime - version: link:../../packages/coln-js-runtime - '@vitejs/plugin-react': - specifier: ^6.0.2 - version: 6.0.2(vite@8.0.16) - react: - specifier: ^19.2.6 - version: 19.2.7 - react-dom: - specifier: ^19.2.6 - version: 19.2.7(react@19.2.7) - typescript: - specifier: ^6.0.3 - version: 6.0.3 - vite: - specifier: ^8.0.14 - version: 8.0.16 - vite-plugin-wasm: - specifier: ^3.6.0 - version: 3.6.0(vite@8.0.16) - ws: - specifier: ^8.21.0 - version: 8.21.0 - devDependencies: - '@playwright/test': - specifier: ^1.61.0 - version: 1.61.0 - '@types/react': - specifier: ^19.2.17 - version: 19.2.17 - '@types/react-dom': - specifier: ^19.2.3 - version: 19.2.3(@types/react@19.2.17) - -packages: - - '@automerge/automerge-repo@file:vendor/automerge-automerge-repo-2.6.0-subduction.33.tgz': - resolution: {integrity: sha512-zz6O4jJTKHy1bncoGn2pq0OKfW0DxkCJ5ccjoPiaya5v0gDyMnqbEpB5/UU5NJ7TKIBXU97NutNTFJDHWvwdvg==, tarball: file:vendor/automerge-automerge-repo-2.6.0-subduction.33.tgz} - version: 2.6.0-subduction.33 - engines: {node: '>=22.13'} - - '@automerge/automerge-subduction@0.16.0': - resolution: {integrity: sha512-BOOn/4p8cF4LhjDUQdgM3qyZFya+M7ZuZywuC99x/kbhk4UK7dw/QcsifcHG5E1TAW4R/K17ZGFUC5nCVUSSWg==} - - '@automerge/automerge@3.3.0-fragments.1': - resolution: {integrity: sha512-Im2n3qcYSuIsLDSjofTRo7CZyyvFn5pKLSCCO5sjWzESw+9q9G25ZIrL2mczDX43cXtyzi+w/4b9VUaiNdY56Q==} - - '@cbor-extract/cbor-extract-darwin-arm64@2.2.2': - resolution: {integrity: sha512-ZKZ/F8US7JR92J4DMct6cLW/Y66o2K576+zjlEN/MevH70bFIsB10wkZEQPLzl2oNh2SMGy55xpJ9JoBRl5DOA==} - cpu: [arm64] - os: [darwin] - - '@cbor-extract/cbor-extract-darwin-x64@2.2.2': - resolution: {integrity: sha512-32b1mgc+P61Js+KW9VZv/c+xRw5EfmOcPx990JbCBSkYJFY0l25VinvyyWfl+3KjibQmAcYwmyzKF9J4DyKP/Q==} - cpu: [x64] - os: [darwin] - - '@cbor-extract/cbor-extract-linux-arm64@2.2.2': - resolution: {integrity: sha512-wfqgzqCAy/Vn8i6WVIh7qZd0DdBFaWBjPdB6ma+Wihcjv0gHqD/mw3ouVv7kbbUNrab6dKEx/w3xQZEdeXIlzg==} - cpu: [arm64] - os: [linux] - - '@cbor-extract/cbor-extract-linux-arm@2.2.2': - resolution: {integrity: sha512-tNg0za41TpQfkhWjptD+0gSD2fggMiDCSacuIeELyb2xZhr7PrhPe5h66Jc67B/5dmpIhI2QOUtv4SBsricyYQ==} - cpu: [arm] - os: [linux] - - '@cbor-extract/cbor-extract-linux-x64@2.2.2': - resolution: {integrity: sha512-rpiLnVEsqtPJ+mXTdx1rfz4RtUGYIUg2rUAZgd1KjiC1SehYUSkJN7Yh+aVfSjvCGtVP0/bfkQkXpPXKbmSUaA==} - cpu: [x64] - os: [linux] - - '@cbor-extract/cbor-extract-win32-x64@2.2.2': - resolution: {integrity: sha512-dI+9P7cfWxkTQ+oE+7Aa6onEn92PHgfWXZivjNheCRmTBDBf2fx6RyTi0cmgpYLnD1KLZK9ZYrMxaPZ4oiXhGA==} - cpu: [x64] - os: [win32] - - '@emnapi/core@1.10.0': - resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} - - '@emnapi/runtime@1.10.0': - resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} - - '@emnapi/wasi-threads@1.2.1': - resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} - - '@napi-rs/wasm-runtime@1.1.5': - resolution: {integrity: sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q==} - peerDependencies: - '@emnapi/core': ^1.7.1 - '@emnapi/runtime': ^1.7.1 - - '@noble/hashes@1.8.0': - resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==} - engines: {node: ^14.21.3 || >=16} - - '@oxc-project/types@0.133.0': - resolution: {integrity: sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==} - - '@playwright/test@1.61.0': - resolution: {integrity: sha512-cKA5B6lpFEMyMGjxF54QihfYpB4FkEGH+qZhtArDEG+wezQAJY8Pq6C7T1SjWz+FFzt3TbyoXBQYk/0292TdJA==} - engines: {node: '>=18'} - hasBin: true - - '@rolldown/binding-android-arm64@1.0.3': - resolution: {integrity: sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [android] - - '@rolldown/binding-darwin-arm64@1.0.3': - resolution: {integrity: sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [darwin] - - '@rolldown/binding-darwin-x64@1.0.3': - resolution: {integrity: sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [darwin] - - '@rolldown/binding-freebsd-x64@1.0.3': - resolution: {integrity: sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [freebsd] - - '@rolldown/binding-linux-arm-gnueabihf@1.0.3': - resolution: {integrity: sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm] - os: [linux] - - '@rolldown/binding-linux-arm64-gnu@1.0.3': - resolution: {integrity: sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [linux] - libc: [glibc] - - '@rolldown/binding-linux-arm64-musl@1.0.3': - resolution: {integrity: sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [linux] - libc: [musl] - - '@rolldown/binding-linux-ppc64-gnu@1.0.3': - resolution: {integrity: sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [ppc64] - os: [linux] - libc: [glibc] - - '@rolldown/binding-linux-s390x-gnu@1.0.3': - resolution: {integrity: sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [s390x] - os: [linux] - libc: [glibc] - - '@rolldown/binding-linux-x64-gnu@1.0.3': - resolution: {integrity: sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [linux] - libc: [glibc] - - '@rolldown/binding-linux-x64-musl@1.0.3': - resolution: {integrity: sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [linux] - libc: [musl] - - '@rolldown/binding-openharmony-arm64@1.0.3': - resolution: {integrity: sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [openharmony] - - '@rolldown/binding-wasm32-wasi@1.0.3': - resolution: {integrity: sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [wasm32] - - '@rolldown/binding-win32-arm64-msvc@1.0.3': - resolution: {integrity: sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [win32] - - '@rolldown/binding-win32-x64-msvc@1.0.3': - resolution: {integrity: sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [win32] - - '@rolldown/pluginutils@1.0.1': - resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} - - '@tybys/wasm-util@0.10.2': - resolution: {integrity: sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==} - - '@types/react-dom@19.2.3': - resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} - peerDependencies: - '@types/react': ^19.2.0 - - '@types/react@19.2.17': - resolution: {integrity: sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==} - - '@vitejs/plugin-react@6.0.2': - resolution: {integrity: sha512-DlSMqo4WhThw4vB8Mpn0Woe9J+Jfq1geJ61AKW0QEgLzGMNwtIMdxbDUzLxcun8W7NbJO0e2Jg/Nxm3cCSVzzg==} - engines: {node: ^20.19.0 || >=22.12.0} - peerDependencies: - '@rolldown/plugin-babel': ^0.1.7 || ^0.2.0 - babel-plugin-react-compiler: ^1.0.0 - vite: ^8.0.0 - peerDependenciesMeta: - '@rolldown/plugin-babel': - optional: true - babel-plugin-react-compiler: - optional: true - - base-x@5.0.1: - resolution: {integrity: sha512-M7uio8Zt++eg3jPj+rHMfCC+IuygQHHCOU+IYsVtik6FWjuYpVt/+MRKcgsAMHh8mMFAwnB+Bs+mTrFiXjMzKg==} - - bs58@6.0.0: - resolution: {integrity: sha512-PD0wEnEYg6ijszw/u8s+iI3H17cTymlrwkKhDhPZq+Sokl3AU4htyBFTjAeNAlCCmg0f53g6ih3jATyCKftTfw==} - - bs58check@4.0.0: - resolution: {integrity: sha512-FsGDOnFg9aVI9erdriULkd/JjEWONV/lQE5aYziB5PoBsXRind56lh8doIZIc9X4HoxT5x4bLjMWN1/NB8Zp5g==} - - cbor-extract@2.2.2: - resolution: {integrity: sha512-hlSxxI9XO2yQfe9g6msd3g4xCfDqK5T5P0fRMLuaLHhxn4ViPrm+a+MUfhrvH2W962RGxcBwEGzLQyjbDG1gng==} - hasBin: true - - cbor-x@1.6.4: - resolution: {integrity: sha512-UGKHjp6RHC6QuZ2yy5LCKm7MojM4716DwoSaqwQpaH4DvZvbBTGcoDNTiG9Y2lByXZYFEs9WRkS5tLl96IrF1Q==} - - csstype@3.2.3: - resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} - - debug@4.4.3: - resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} - engines: {node: '>=6.0'} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - - detect-libc@2.1.2: - resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} - engines: {node: '>=8'} - - eventemitter3@5.0.4: - resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==} - - fast-sha256@1.3.0: - resolution: {integrity: sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==} - - fdir@6.5.0: - resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} - engines: {node: '>=12.0.0'} - peerDependencies: - picomatch: ^3 || ^4 - peerDependenciesMeta: - picomatch: - optional: true - - fsevents@2.3.2: - resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} - engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} - os: [darwin] - - fsevents@2.3.3: - resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} - engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} - os: [darwin] - - isomorphic-ws@5.0.0: - resolution: {integrity: sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw==} - peerDependencies: - ws: '*' - - lightningcss-android-arm64@1.32.0: - resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} - engines: {node: '>= 12.0.0'} - cpu: [arm64] - os: [android] - - lightningcss-darwin-arm64@1.32.0: - resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} - engines: {node: '>= 12.0.0'} - cpu: [arm64] - os: [darwin] - - lightningcss-darwin-x64@1.32.0: - resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [darwin] - - lightningcss-freebsd-x64@1.32.0: - resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [freebsd] - - lightningcss-linux-arm-gnueabihf@1.32.0: - resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} - engines: {node: '>= 12.0.0'} - cpu: [arm] - os: [linux] - - lightningcss-linux-arm64-gnu@1.32.0: - resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} - engines: {node: '>= 12.0.0'} - cpu: [arm64] - os: [linux] - libc: [glibc] - - lightningcss-linux-arm64-musl@1.32.0: - resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} - engines: {node: '>= 12.0.0'} - cpu: [arm64] - os: [linux] - libc: [musl] - - lightningcss-linux-x64-gnu@1.32.0: - resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [linux] - libc: [glibc] - - lightningcss-linux-x64-musl@1.32.0: - resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [linux] - libc: [musl] - - lightningcss-win32-arm64-msvc@1.32.0: - resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} - engines: {node: '>= 12.0.0'} - cpu: [arm64] - os: [win32] - - lightningcss-win32-x64-msvc@1.32.0: - resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [win32] - - lightningcss@1.32.0: - resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} - engines: {node: '>= 12.0.0'} - - ms@2.1.3: - resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - - nanoid@3.3.12: - resolution: {integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==} - engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} - hasBin: true - - node-gyp-build-optional-packages@5.1.1: - resolution: {integrity: sha512-+P72GAjVAbTxjjwUmwjVrqrdZROD4nf8KgpBoDxqXXTiYZZt/ud60dE5yvCSr9lRO8e8yv6kgJIC0K0PfZFVQw==} - hasBin: true - - picocolors@1.1.1: - resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} - - picomatch@4.0.4: - resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} - engines: {node: '>=12'} - - playwright-core@1.61.0: - resolution: {integrity: sha512-caX7TrY3Ml6egyDX0WUcTHDxodl/b51y5wJOdCEA36QviK/s2g081hvmGs8eaE3DWb6NYZQ6BjO/QkNRPenoPA==} - engines: {node: '>=18'} - hasBin: true - - playwright@1.61.0: - resolution: {integrity: sha512-Z+7BeeqQPRRzklHsVFP4KTGIyMxKUmfeRA4WisM6G3/XW6nwGeX6fX9qYaDa+CiUqpOkb2f6X3nar05R3kSuJQ==} - engines: {node: '>=18'} - hasBin: true - - postcss@8.5.15: - resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==} - engines: {node: ^10 || ^12 || >=14} - - react-dom@19.2.7: - resolution: {integrity: sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==} - peerDependencies: - react: ^19.2.7 - - react@19.2.7: - resolution: {integrity: sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==} - engines: {node: '>=0.10.0'} - - rolldown@1.0.3: - resolution: {integrity: sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==} - engines: {node: ^20.19.0 || >=22.12.0} - hasBin: true - - scheduler@0.27.0: - resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} - - source-map-js@1.2.1: - resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} - engines: {node: '>=0.10.0'} - - tinyglobby@0.2.17: - resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} - engines: {node: '>=12.0.0'} - - tslib@2.8.1: - resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} - - typescript@6.0.3: - resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==} - engines: {node: '>=14.17'} - hasBin: true - - uuid@14.0.0: - resolution: {integrity: sha512-Qo+uWgilfSmAhXCMav1uYFynlQO7fMFiMVZsQqZRMIXp0O7rR7qjkj+cPvBHLgBqi960QCoo/PH2/6ZtVqKvrg==} - hasBin: true - - vite-plugin-wasm@3.6.0: - resolution: {integrity: sha512-mL/QPziiIA4RAA6DkaZZzOstdwbW5jO4Vz7Zenj0wieKWBlNvIvX5L5ljum9lcUX0ShNfBgCNLKTjNkRVVqcsw==} - peerDependencies: - vite: ^2 || ^3 || ^4 || ^5 || ^6 || ^7 || ^8 - - vite@8.0.16: - resolution: {integrity: sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==} - engines: {node: ^20.19.0 || >=22.12.0} - hasBin: true - peerDependencies: - '@types/node': ^20.19.0 || >=22.12.0 - '@vitejs/devtools': ^0.1.18 - esbuild: ^0.27.0 || ^0.28.0 - jiti: '>=1.21.0' - less: ^4.0.0 - sass: ^1.70.0 - sass-embedded: ^1.70.0 - stylus: '>=0.54.8' - sugarss: ^5.0.0 - terser: ^5.16.0 - tsx: ^4.8.1 - yaml: ^2.4.2 - peerDependenciesMeta: - '@types/node': - optional: true - '@vitejs/devtools': - optional: true - esbuild: - optional: true - jiti: - optional: true - less: - optional: true - sass: - optional: true - sass-embedded: - optional: true - stylus: - optional: true - sugarss: - optional: true - terser: - optional: true - tsx: - optional: true - yaml: - optional: true - - ws@8.21.0: - resolution: {integrity: sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==} - engines: {node: '>=10.0.0'} - peerDependencies: - bufferutil: ^4.0.1 - utf-8-validate: '>=5.0.2' - peerDependenciesMeta: - bufferutil: - optional: true - utf-8-validate: - optional: true - - xstate@5.32.1: - resolution: {integrity: sha512-IGX9q5vEOplWjVq79edfgjJfVV/lXCup9p/fQGgUabAveZrlRwWJ/mC2iZEE7wswXbWITBCoS7gmoFfcuWAwsQ==} - -snapshots: - - '@automerge/automerge-repo@file:vendor/automerge-automerge-repo-2.6.0-subduction.33.tgz': - dependencies: - '@automerge/automerge': 3.3.0-fragments.1 - '@automerge/automerge-subduction': 0.16.0 - bs58check: 4.0.0 - cbor-x: 1.6.4 - debug: 4.4.3 - eventemitter3: 5.0.4 - fast-sha256: 1.3.0 - isomorphic-ws: 5.0.0(ws@8.21.0) - uuid: 14.0.0 - ws: 8.21.0 - xstate: 5.32.1 - transitivePeerDependencies: - - bufferutil - - supports-color - - utf-8-validate - - '@automerge/automerge-subduction@0.16.0': {} - - '@automerge/automerge@3.3.0-fragments.1': {} - - '@cbor-extract/cbor-extract-darwin-arm64@2.2.2': - optional: true - - '@cbor-extract/cbor-extract-darwin-x64@2.2.2': - optional: true - - '@cbor-extract/cbor-extract-linux-arm64@2.2.2': - optional: true - - '@cbor-extract/cbor-extract-linux-arm@2.2.2': - optional: true - - '@cbor-extract/cbor-extract-linux-x64@2.2.2': - optional: true - - '@cbor-extract/cbor-extract-win32-x64@2.2.2': - optional: true - - '@emnapi/core@1.10.0': - dependencies: - '@emnapi/wasi-threads': 1.2.1 - tslib: 2.8.1 - optional: true - - '@emnapi/runtime@1.10.0': - dependencies: - tslib: 2.8.1 - optional: true - - '@emnapi/wasi-threads@1.2.1': - dependencies: - tslib: 2.8.1 - optional: true - - '@napi-rs/wasm-runtime@1.1.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': - dependencies: - '@emnapi/core': 1.10.0 - '@emnapi/runtime': 1.10.0 - '@tybys/wasm-util': 0.10.2 - optional: true - - '@noble/hashes@1.8.0': {} - - '@oxc-project/types@0.133.0': {} - - '@playwright/test@1.61.0': - dependencies: - playwright: 1.61.0 - - '@rolldown/binding-android-arm64@1.0.3': - optional: true - - '@rolldown/binding-darwin-arm64@1.0.3': - optional: true - - '@rolldown/binding-darwin-x64@1.0.3': - optional: true - - '@rolldown/binding-freebsd-x64@1.0.3': - optional: true - - '@rolldown/binding-linux-arm-gnueabihf@1.0.3': - optional: true - - '@rolldown/binding-linux-arm64-gnu@1.0.3': - optional: true - - '@rolldown/binding-linux-arm64-musl@1.0.3': - optional: true - - '@rolldown/binding-linux-ppc64-gnu@1.0.3': - optional: true - - '@rolldown/binding-linux-s390x-gnu@1.0.3': - optional: true - - '@rolldown/binding-linux-x64-gnu@1.0.3': - optional: true - - '@rolldown/binding-linux-x64-musl@1.0.3': - optional: true - - '@rolldown/binding-openharmony-arm64@1.0.3': - optional: true - - '@rolldown/binding-wasm32-wasi@1.0.3': - dependencies: - '@emnapi/core': 1.10.0 - '@emnapi/runtime': 1.10.0 - '@napi-rs/wasm-runtime': 1.1.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) - optional: true - - '@rolldown/binding-win32-arm64-msvc@1.0.3': - optional: true - - '@rolldown/binding-win32-x64-msvc@1.0.3': - optional: true - - '@rolldown/pluginutils@1.0.1': {} - - '@tybys/wasm-util@0.10.2': - dependencies: - tslib: 2.8.1 - optional: true - - '@types/react-dom@19.2.3(@types/react@19.2.17)': - dependencies: - '@types/react': 19.2.17 - - '@types/react@19.2.17': - dependencies: - csstype: 3.2.3 - - '@vitejs/plugin-react@6.0.2(vite@8.0.16)': - dependencies: - '@rolldown/pluginutils': 1.0.1 - vite: 8.0.16 - - base-x@5.0.1: {} - - bs58@6.0.0: - dependencies: - base-x: 5.0.1 - - bs58check@4.0.0: - dependencies: - '@noble/hashes': 1.8.0 - bs58: 6.0.0 - - cbor-extract@2.2.2: - dependencies: - node-gyp-build-optional-packages: 5.1.1 - optionalDependencies: - '@cbor-extract/cbor-extract-darwin-arm64': 2.2.2 - '@cbor-extract/cbor-extract-darwin-x64': 2.2.2 - '@cbor-extract/cbor-extract-linux-arm': 2.2.2 - '@cbor-extract/cbor-extract-linux-arm64': 2.2.2 - '@cbor-extract/cbor-extract-linux-x64': 2.2.2 - '@cbor-extract/cbor-extract-win32-x64': 2.2.2 - optional: true - - cbor-x@1.6.4: - optionalDependencies: - cbor-extract: 2.2.2 - - csstype@3.2.3: {} - - debug@4.4.3: - dependencies: - ms: 2.1.3 - - detect-libc@2.1.2: {} - - eventemitter3@5.0.4: {} - - fast-sha256@1.3.0: {} - - fdir@6.5.0(picomatch@4.0.4): - optionalDependencies: - picomatch: 4.0.4 - - fsevents@2.3.2: - optional: true - - fsevents@2.3.3: - optional: true - - isomorphic-ws@5.0.0(ws@8.21.0): - dependencies: - ws: 8.21.0 - - lightningcss-android-arm64@1.32.0: - optional: true - - lightningcss-darwin-arm64@1.32.0: - optional: true - - lightningcss-darwin-x64@1.32.0: - optional: true - - lightningcss-freebsd-x64@1.32.0: - optional: true - - lightningcss-linux-arm-gnueabihf@1.32.0: - optional: true - - lightningcss-linux-arm64-gnu@1.32.0: - optional: true - - lightningcss-linux-arm64-musl@1.32.0: - optional: true - - lightningcss-linux-x64-gnu@1.32.0: - optional: true - - lightningcss-linux-x64-musl@1.32.0: - optional: true - - lightningcss-win32-arm64-msvc@1.32.0: - optional: true - - lightningcss-win32-x64-msvc@1.32.0: - optional: true - - lightningcss@1.32.0: - dependencies: - detect-libc: 2.1.2 - optionalDependencies: - lightningcss-android-arm64: 1.32.0 - lightningcss-darwin-arm64: 1.32.0 - lightningcss-darwin-x64: 1.32.0 - lightningcss-freebsd-x64: 1.32.0 - lightningcss-linux-arm-gnueabihf: 1.32.0 - lightningcss-linux-arm64-gnu: 1.32.0 - lightningcss-linux-arm64-musl: 1.32.0 - lightningcss-linux-x64-gnu: 1.32.0 - lightningcss-linux-x64-musl: 1.32.0 - lightningcss-win32-arm64-msvc: 1.32.0 - lightningcss-win32-x64-msvc: 1.32.0 - - ms@2.1.3: {} - - nanoid@3.3.12: {} - - node-gyp-build-optional-packages@5.1.1: - dependencies: - detect-libc: 2.1.2 - optional: true - - picocolors@1.1.1: {} - - picomatch@4.0.4: {} - - playwright-core@1.61.0: {} - - playwright@1.61.0: - dependencies: - playwright-core: 1.61.0 - optionalDependencies: - fsevents: 2.3.2 - - postcss@8.5.15: - dependencies: - nanoid: 3.3.12 - picocolors: 1.1.1 - source-map-js: 1.2.1 - - react-dom@19.2.7(react@19.2.7): - dependencies: - react: 19.2.7 - scheduler: 0.27.0 - - react@19.2.7: {} - - rolldown@1.0.3: - dependencies: - '@oxc-project/types': 0.133.0 - '@rolldown/pluginutils': 1.0.1 - optionalDependencies: - '@rolldown/binding-android-arm64': 1.0.3 - '@rolldown/binding-darwin-arm64': 1.0.3 - '@rolldown/binding-darwin-x64': 1.0.3 - '@rolldown/binding-freebsd-x64': 1.0.3 - '@rolldown/binding-linux-arm-gnueabihf': 1.0.3 - '@rolldown/binding-linux-arm64-gnu': 1.0.3 - '@rolldown/binding-linux-arm64-musl': 1.0.3 - '@rolldown/binding-linux-ppc64-gnu': 1.0.3 - '@rolldown/binding-linux-s390x-gnu': 1.0.3 - '@rolldown/binding-linux-x64-gnu': 1.0.3 - '@rolldown/binding-linux-x64-musl': 1.0.3 - '@rolldown/binding-openharmony-arm64': 1.0.3 - '@rolldown/binding-wasm32-wasi': 1.0.3 - '@rolldown/binding-win32-arm64-msvc': 1.0.3 - '@rolldown/binding-win32-x64-msvc': 1.0.3 - - scheduler@0.27.0: {} - - source-map-js@1.2.1: {} - - tinyglobby@0.2.17: - dependencies: - fdir: 6.5.0(picomatch@4.0.4) - picomatch: 4.0.4 - - tslib@2.8.1: - optional: true - - typescript@6.0.3: {} - - uuid@14.0.0: {} - - vite-plugin-wasm@3.6.0(vite@8.0.16): - dependencies: - vite: 8.0.16 - - vite@8.0.16: - dependencies: - lightningcss: 1.32.0 - picomatch: 4.0.4 - postcss: 8.5.15 - rolldown: 1.0.3 - tinyglobby: 0.2.17 - optionalDependencies: - fsevents: 2.3.3 - - ws@8.21.0: {} - - xstate@5.32.1: {} diff --git a/examples/sync-demo/pnpm-workspace.yaml b/examples/sync-demo/pnpm-workspace.yaml deleted file mode 100644 index f91e0fa2..00000000 --- a/examples/sync-demo/pnpm-workspace.yaml +++ /dev/null @@ -1,5 +0,0 @@ -allowBuilds: - cbor-extract: false -minimumReleaseAgeExclude: - - '@automerge/automerge-repo@2.6.0-subduction.33' - - '@automerge/automerge-subduction@0.16.0' diff --git a/examples/sync-demo/src/App.tsx b/examples/sync-demo/src/App.tsx deleted file mode 100644 index 9b8faa63..00000000 --- a/examples/sync-demo/src/App.tsx +++ /dev/null @@ -1,320 +0,0 @@ -import { useEffect, useMemo, useState, type ReactNode } from "react" -import type { RowRef, RowView, Value } from "@coln-project/runtime" -import type { CrdtDocHandle } from "@automerge/automerge-repo/slim" -import * as GraphRealm from "./generated/GraphRealm.ts" -import { - refId, - rowValue, - type ColnDocType, - type ColnDocument, -} from "./colnDocType" -import "./style.css" - -type GraphFfi = typeof GraphRealm -type GraphDocType = ColnDocType -type GraphDoc = ColnDocument - -type Props = { - handle: CrdtDocHandle -} - -type UiGraph = { id: string; ref: RowRef | null } -type UiVertex = { - id: string - ref: RowRef - graphId: string - graph: RowRef | null -} -type UiEdge = { - id: string - ref: RowRef - graphId: string - graph: RowRef | null - fromId: string - from: RowRef - toId: string - to: RowRef -} - -type UiGraphProjection = { - graphs: UiGraph[] - vertices: UiVertex[] - edges: UiEdge[] - heads: string[] -} - -const SYNTHETIC_GRAPH_ID = "GraphRealm" - -export function App({ handle }: Props) { - const [doc, setDoc] = useState(() => handle.doc()) - const graph = useMemo(() => projectGraph(doc), [doc]) - const [selectedGraphId, setSelectedGraphId] = useState("") - const [fromId, setFromId] = useState("") - const [toId, setToId] = useState("") - const [error, setError] = useState("") - - useEffect(() => { - const update = () => setDoc(handle.doc()) - handle.on("change", update) - handle.on("heads-changed", update) - return () => { - handle.off("change", update) - handle.off("heads-changed", update) - } - }, [handle]) - - const selectedGraph = useMemo( - () => graph.graphs.find(graph => graph.id === selectedGraphId) ?? graph.graphs[0], - [graph.graphs, selectedGraphId] - ) - - useEffect(() => { - if (!selectedGraph) { - setSelectedGraphId("") - return - } - if (selectedGraph.id !== selectedGraphId) setSelectedGraphId(selectedGraph.id) - }, [selectedGraph, selectedGraphId]) - - const vertices = useMemo( - () => graph.vertices.filter(vertex => vertex.graphId === selectedGraph?.id), - [graph.vertices, selectedGraph] - ) - const edges = useMemo( - () => graph.edges.filter(edge => edge.graphId === selectedGraph?.id), - [graph.edges, selectedGraph] - ) - - useEffect(() => { - if (!vertices.some(vertex => vertex.id === fromId)) setFromId(vertices[0]?.id ?? "") - if (!vertices.some(vertex => vertex.id === toId)) setToId(vertices[1]?.id ?? vertices[0]?.id ?? "") - }, [vertices, fromId, toId]) - - const run = (fn: () => void) => { - setError("") - try { - fn() - } catch (e) { - setError(e instanceof Error ? e.message : String(e)) - } - } - - const addVertex = (graph: UiGraph | undefined) => - run(() => { - if (!graph) throw new Error("Create/select a graph first") - handle.change(tx => { - tx.root.V.add() - }) - }) - - const addEdge = () => - run(() => { - if (!selectedGraph) throw new Error("Create/select a graph first") - const from = vertices.find(vertex => vertex.id === fromId) - const to = vertices.find(vertex => vertex.id === toId) - if (!from || !to) throw new Error("Select two vertices first") - handle.change(tx => { - tx.root.E(rowValue(from.ref))(rowValue(to.ref)).add() - }) - }) - - return ( -
-
-

Coln + automerge-repo

-

- Open this URL in another tab, then edit from either side. -

- e.currentTarget.select()} /> -
- - {error &&
{error}
} - -
- - -
- -
- - - -
- -
- - {graph.graphs.length === 0 ? ( -

No visible graphs yet. The document may still be syncing.

- ) : ( -
    - {graph.graphs.map((graph, index) => ( -
  • Graph {index + 1}: {shortId(graph.id)}
  • - ))} -
- )} -
- - - {vertices.length === 0 ? ( -

No vertices in this graph.

- ) : ( -
    - {vertices.map((vertex, index) => ( -
  • {vertexLabel(vertex, vertices, index)}
  • - ))} -
- )} -
- - - {edges.length === 0 ? ( -

No edges in this graph.

- ) : ( -
    - {edges.map(edge => ( -
  • - {vertexName(edge.fromId, vertices)} → {vertexName(edge.toId, vertices)} -
  • - ))} -
- )} -
-
- -
- {graph.heads.length} head{graph.heads.length === 1 ? "" : "s"} - {graph.vertices.length} visible vertices - {graph.edges.length} visible edges -
-
- ) -} - -function projectGraph(doc: GraphDoc): UiGraphProjection { - const vertexRows = iteratorToArray(doc.realm.root.V.values()) - const vertices = vertexRows.map(row => { - const ref = valueRef(row.rowId) - if (!ref) throw new Error("vertex row is missing a row id") - return { - id: refId(ref), - ref, - graphId: SYNTHETIC_GRAPH_ID, - graph: null, - } - }) - const visibleVertexIds = new Set(vertices.map(vertex => vertex.id)) - - return { - graphs: [{ id: SYNTHETIC_GRAPH_ID, ref: null }], - vertices, - edges: doc.store - .scanTable("GraphRealm.E") - .flatMap(row => projectEdge(row, visibleVertexIds)), - heads: doc.heads, - } -} - -function projectEdge(row: RowView, visibleVertexIds: Set): UiEdge[] { - const from = valueRef(row.values[0]) - const to = valueRef(row.values[1]) - if (!from || !to) return [] - - const fromId = refId(from) - const toId = refId(to) - if (!visibleVertexIds.has(fromId) || !visibleVertexIds.has(toId)) return [] - - const ref = valueRef(row.rowId) - if (!ref) return [] - - return [ - { - id: refId(ref), - ref, - graphId: SYNTHETIC_GRAPH_ID, - graph: null, - fromId, - from, - toId, - to, - }, - ] -} - -function iteratorToArray(iterator: Iterator): T[] { - const values: T[] = [] - for (let next = iterator.next(); !next.done; next = iterator.next()) { - values.push(next.value) - } - return values -} - -function valueRef(value: Value | undefined): RowRef | undefined { - return value?.tag === "row_id" ? value.value : undefined -} - -function VertexSelect({ - testId, - vertices, - value, - onChange, -}: { - testId: string - vertices: UiVertex[] - value: string - onChange(value: string): void -}) { - return ( - - ) -} - -function Panel({ title, children }: { title: string; children: ReactNode }) { - return ( -
-

{title}

- {children} -
- ) -} - -function vertexLabel(vertex: UiVertex, vertices: UiVertex[], fallbackIndex: number) { - return `${vertexName(vertex.id, vertices, fallbackIndex)} · ${shortId(vertex.id)}` -} - -function vertexName(id: string, vertices: UiVertex[], fallbackIndex = -1) { - const index = vertices.findIndex(vertex => vertex.id === id) - return `V${(index >= 0 ? index : fallbackIndex) + 1}` -} - -function shortId(id: string) { - return id.slice(0, 8) -} diff --git a/examples/sync-demo/src/colnDocType.ts b/examples/sync-demo/src/colnDocType.ts deleted file mode 100644 index a27f520a..00000000 --- a/examples/sync-demo/src/colnDocType.ts +++ /dev/null @@ -1,161 +0,0 @@ -// SPDX-FileCopyrightText: 2026 Coln contributors -// -// SPDX-License-Identifier: Apache-2.0 OR MIT - -import { - StoreHandle, - type CommitChunk, - type RowRef, - type TransactionHandle, - type Value, -} from "@coln-project/runtime" -import { - defineDocumentType, - type DocumentType, - type DocumentTypeContext, - type SedimentreeMeta, -} from "@automerge/automerge-repo/slim" - -export type RowIdValue = Extract - -export interface ColnState { - store: StoreHandle - actor: string -} - -export type ColnFfi = { - schema: unknown - View: new (store: StoreHandle) => View - Transaction: new (store: StoreHandle, transaction: TransactionHandle) => Transaction -} - -export type AnyColnFfi = ColnFfi -export type ColnFfiView = Ffi extends ColnFfi ? View : never -export type ColnFfiTransaction = Ffi extends ColnFfi ? Transaction : never - -export type ColnDocument = { - realm: ColnFfiView - store: StoreHandle - heads: string[] -} - -export type ColnChange = (tx: ColnFfiTransaction) => void - -export type ColnDocType< - Ffi extends AnyColnFfi, - View = ColnDocument, - Change = ColnChange, - Init = undefined, -> = DocumentType - -export type ColnDocTypeOptions< - Ffi extends AnyColnFfi, - View = ColnDocument, - Change = ColnChange, - Init = undefined, -> = { - name?: string - view?: (doc: ColnDocument, state: ColnState) => View - change?: (tx: ColnFfiTransaction, change: Change, state: ColnState) => void - init?: (init: Init, tx: ColnFfiTransaction, state: ColnState) => void - hasData?: (state: ColnState) => boolean -} - -export function colnDocType< - Ffi extends AnyColnFfi, - View = ColnDocument, - Change = ColnChange, - Init = undefined, ->( - ffi: Ffi, - options: ColnDocTypeOptions = {} -): ColnDocType { - const schemaJson = JSON.stringify(ffi.schema) - const projectView = options.view ?? ((doc: ColnDocument) => doc as unknown as View) - const applyChange = - options.change ?? - ((tx: ColnFfiTransaction, change: Change) => { - ;(change as unknown as ColnChange)(tx) - }) - - const makeState = (ctx: DocumentTypeContext): ColnState => ({ - store: StoreHandle.fromTheory(schemaJson), - actor: ctx.peerId, - }) - - const makeDocument = (state: ColnState): ColnDocument => ({ - realm: new ffi.View(state.store), - store: state.store, - heads: state.store.heads(), - }) - - const runTransaction = ( - state: ColnState, - body: (tx: ColnFfiTransaction) => void - ): ColnState => { - const tx = state.store.beginTransaction() - try { - const typedTx = new ffi.Transaction(state.store, tx) - body(typedTx) - const result = tx.commit() - return { ...state, store: result.takeStore() } - } catch (error) { - try { - state.store = tx.takeStore() - } catch { - // ignore; preserve the original error - } - throw error - } - } - - return defineDocumentType({ - name: options.name ?? "coln", - empty: ctx => makeState(ctx), - init: (init, ctx) => { - const state = makeState(ctx) - return options.init - ? runTransaction(state, tx => options.init?.(init, tx, state)) - : state - }, - view: state => projectView(makeDocument(state), state), - change: (state, change) => - runTransaction(state, tx => applyChange(tx, change, state)), - heads: state => state.store.heads(), - hasData: state => options.hasData?.(state) ?? state.store.heads().length > 0, - sedimentree: { - metadata: state => chunks(state).map(chunkToMeta), - materialize: (state, metas) => { - const wanted = new Set(metas.map(meta => meta.head)) - return chunks(state) - .filter(chunk => wanted.has(chunk.hash)) - .map(chunk => ({ ...chunkToMeta(chunk), bytes: new Uint8Array(chunk.bytes) })) - }, - apply: (state, blobs) => { - if (blobs.length > 0) { - state.store.applyChunkBytes(blobs.map(blob => Array.from(blob))) - } - return state - }, - liveHashes: state => chunks(state).map(chunk => chunk.hash), - }, - }) -} - -export function rowValue(ref: RowRef | null): RowIdValue { - if (!ref) throw new Error("cannot convert a synthetic row to a row_id value") - return { tag: "row_id", value: ref } -} - -export function refId(ref: RowRef): string { - if ("existing" in ref) return `${ref.existing.commit}:${ref.existing.counter}` - return `pending:${ref.pending.txId}:${ref.pending.counter}` -} - -function chunks(state: ColnState): CommitChunk[] { - return state.store.commitChunksAfter([]) -} - -function chunkToMeta(chunk: CommitChunk): SedimentreeMeta { - return { kind: "commit", head: chunk.hash, parents: chunk.parents } -} diff --git a/examples/sync-demo/src/generated/GraphRealm.json b/examples/sync-demo/src/generated/GraphRealm.json deleted file mode 100644 index 7ca1beae..00000000 --- a/examples/sync-demo/src/generated/GraphRealm.json +++ /dev/null @@ -1 +0,0 @@ -{"entities":[{"path":[["GraphRealm"],["E"]],"value":{"entityVariant":{"tag":"table"},"columns":[{"path":[["a"]],"type":{"tag":"rowId","path":[["GraphRealm"],["V"]]}},{"path":[["b"]],"type":{"tag":"rowId","path":[["GraphRealm"],["V"]]}}],"primaryKey":null}},{"path":[["GraphRealm"],["V"]],"value":{"entityVariant":{"tag":"table"},"columns":[],"primaryKey":null}}],"rules":[{"path":[["GraphRealm"],["E"],["foreignKey"]],"value":{"ruleVariant":"enforced","varNames":[[["a"]],[["b"]]],"varTypes":[{"tag":"rowId","path":[["GraphRealm"],["V"]]},{"tag":"rowId","path":[["GraphRealm"],["V"]]}],"antecedents":[{"tag":"atom","atom":{"entity":[["GraphRealm"],["E"]],"rowId":null,"values":[{"column":0,"term":{"tag":"var","index":0}},{"column":1,"term":{"tag":"var","index":1}}]}}],"consequents":[{"tag":"atom","atom":{"entity":[["GraphRealm"],["V"]],"rowId":{"tag":"var","index":0},"values":[]}},{"tag":"atom","atom":{"entity":[["GraphRealm"],["V"]],"rowId":{"tag":"var","index":1},"values":[]}}]}},{"path":[["GraphRealm"],["V"],["foreignKey"]],"value":{"ruleVariant":"enforced","varNames":[],"varTypes":[],"antecedents":[{"tag":"atom","atom":{"entity":[["GraphRealm"],["V"]],"rowId":null,"values":[]}}],"consequents":[]}}]} \ No newline at end of file diff --git a/examples/sync-demo/src/main.tsx b/examples/sync-demo/src/main.tsx deleted file mode 100644 index 16931209..00000000 --- a/examples/sync-demo/src/main.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import React from "react" -import ReactDOM from "react-dom/client" -import { - Repo, - isValidAutomergeUrl, - type CrdtDocHandle, -} from "@automerge/automerge-repo" -// @ts-ignore initSync is exported at runtime but may be missing from declarations -import { initSync } from "@automerge/automerge-subduction/slim" -// @ts-ignore wasm-base64 may not have declarations -import { wasmBase64 } from "@automerge/automerge-subduction/wasm-base64" -import { App } from "./App" -import { colnDocType, type ColnDocType } from "./colnDocType" -import * as GraphRealm from "./generated/GraphRealm.ts" - -initSync({ module: Uint8Array.from(atob(wasmBase64), c => c.charCodeAt(0)) }) - -type GraphDocType = ColnDocType - -const coln = colnDocType(GraphRealm) -const repo = new Repo({ - subductionWebsocketEndpoints: ["wss://subduction.sync.inkandswitch.com"], -}) - -const hashUrl = location.hash.slice(1) -let handle: CrdtDocHandle - -if (isValidAutomergeUrl(hashUrl)) { - handle = await repo.find(hashUrl, coln) -} else { - handle = repo.create(undefined, coln) -} - -location.hash = handle.url - -Object.assign(window, { repo, handle }) - -ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( - - - -) diff --git a/examples/sync-demo/src/style.css b/examples/sync-demo/src/style.css deleted file mode 100644 index a1591adb..00000000 --- a/examples/sync-demo/src/style.css +++ /dev/null @@ -1,158 +0,0 @@ -:root { - color-scheme: light dark; - font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; - line-height: 1.5; - font-weight: 400; - background: #111827; - color: #f9fafb; -} - -* { - box-sizing: border-box; -} - -body { - margin: 0; - min-width: 320px; - min-height: 100vh; -} - -button, -select, -input { - font: inherit; -} - -button { - border: 0; - border-radius: 10px; - padding: 0.6rem 0.9rem; - color: #08111f; - background: #67e8f9; - font-weight: 700; - cursor: pointer; -} - -button:hover { - background: #22d3ee; -} - -button:disabled { - cursor: not-allowed; - opacity: 0.5; -} - -select, -input { - border: 1px solid #334155; - border-radius: 10px; - padding: 0.55rem 0.7rem; - background: #020617; - color: #f9fafb; -} - -main { - width: min(1100px, calc(100vw - 32px)); - margin: 0 auto; - padding: 32px 0; -} - -header { - margin-bottom: 24px; -} - -h1 { - margin: 0 0 8px; - font-size: clamp(2rem, 4vw, 3.5rem); - line-height: 1; -} - -h2 { - margin: 0 0 12px; -} - -p { - color: #cbd5e1; -} - -code { - color: #a7f3d0; -} - -.url { - width: 100%; - margin-top: 8px; - font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; -} - -.error { - margin: 16px 0; - padding: 12px 14px; - border: 1px solid #fca5a5; - border-radius: 12px; - color: #fecaca; - background: #450a0a; -} - -.toolbar { - display: flex; - flex-wrap: wrap; - align-items: end; - gap: 12px; - margin: 14px 0; - padding: 14px; - border: 1px solid #1f2937; - border-radius: 16px; - background: #0f172a; -} - -label { - display: grid; - gap: 4px; - color: #cbd5e1; - font-size: 0.9rem; -} - -.grid { - display: grid; - grid-template-columns: repeat(3, minmax(0, 1fr)); - gap: 16px; - margin-top: 18px; -} - -.panel { - min-height: 180px; - padding: 18px; - border: 1px solid #1f2937; - border-radius: 18px; - background: #0f172a; -} - -ul { - margin: 0; - padding-left: 1.25rem; -} - -li { - margin: 0.35rem 0; - overflow-wrap: anywhere; -} - -.muted { - color: #94a3b8; -} - -footer { - display: flex; - gap: 14px; - flex-wrap: wrap; - margin-top: 18px; - color: #94a3b8; - font-size: 0.9rem; -} - -@media (max-width: 800px) { - .grid { - grid-template-columns: 1fr; - } -} diff --git a/examples/sync-demo/test-results/.last-run.json b/examples/sync-demo/test-results/.last-run.json deleted file mode 100644 index cbcc1fba..00000000 --- a/examples/sync-demo/test-results/.last-run.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "status": "passed", - "failedTests": [] -} \ No newline at end of file diff --git a/examples/sync-demo/tests/sync.spec.ts b/examples/sync-demo/tests/sync.spec.ts deleted file mode 100644 index c9efc9f6..00000000 --- a/examples/sync-demo/tests/sync.spec.ts +++ /dev/null @@ -1,68 +0,0 @@ -// SPDX-FileCopyrightText: 2026 Coln contributors -// -// SPDX-License-Identifier: Apache-2.0 OR MIT - -import { expect, test, type Page } from "@playwright/test" - -test("a tab opened after edits loads the coln graph", async ({ browser }) => { - const context = await browser.newContext() - const alice = await context.newPage() - - await alice.goto("/") - await expect(alice.getByTestId("doc-url")).toHaveValue(/^automerge:/) - await expect(alice.getByTestId("graph-item")).toHaveCount(1) - - await addTwoVerticesAndEdge(alice) - - const url = await alice.getByTestId("doc-url").inputValue() - const bob = await context.newPage() - await bob.goto(`/#${url}`) - - await expect(bob.getByTestId("doc-url")).toHaveValue(url) - await expect(bob.getByTestId("graph-item")).toHaveCount(1) - await expect(bob.getByTestId("vertex-item")).toHaveCount(2) - await expect(bob.getByTestId("edge-item")).toHaveCount(1) - - await bob.getByTestId("add-vertex").click() - await expect(bob.getByTestId("vertex-item")).toHaveCount(3) - await expect(alice.getByTestId("vertex-item")).toHaveCount(3) -}) - -test("a tab already open on the doc receives later coln edits", async ({ browser }) => { - const context = await browser.newContext() - const alice = await context.newPage() - - await alice.goto("/") - await expect(alice.getByTestId("doc-url")).toHaveValue(/^automerge:/) - await expect(alice.getByTestId("graph-item")).toHaveCount(1) - - const url = await alice.getByTestId("doc-url").inputValue() - const bob = await context.newPage() - await bob.goto(`/#${url}`) - await expect(bob.getByTestId("doc-url")).toHaveValue(url) - await expect(bob.getByTestId("graph-item")).toHaveCount(1) - - await addTwoVerticesAndEdge(alice) - - await expect(bob.getByTestId("vertex-item")).toHaveCount(2) - await expect(bob.getByTestId("edge-item")).toHaveCount(1) -}) - -async function addTwoVerticesAndEdge(page: Page) { - await page.getByTestId("add-vertex").click() - await page.getByTestId("add-vertex").click() - await expect(page.getByTestId("vertex-item")).toHaveCount(2) - - await selectByIndex(page, "from-select", 0) - await selectByIndex(page, "to-select", 1) - await page.getByTestId("add-edge").click() - await expect(page.getByTestId("edge-item")).toHaveCount(1) -} - -async function selectByIndex(page: Page, testId: string, index: number) { - const values = await page.getByTestId(testId).locator("option").evaluateAll(options => - options.map(option => (option as HTMLOptionElement).value) - ) - expect(values.length).toBeGreaterThan(index) - await page.getByTestId(testId).selectOption(values[index]) -} diff --git a/examples/sync-demo/vendor/automerge-automerge-repo-2.6.0-subduction.33.tgz b/examples/sync-demo/vendor/automerge-automerge-repo-2.6.0-subduction.33.tgz deleted file mode 100644 index 325ccbee..00000000 Binary files a/examples/sync-demo/vendor/automerge-automerge-repo-2.6.0-subduction.33.tgz and /dev/null differ diff --git a/flake.lock b/flake.lock index 145fa01a..9767fced 100644 --- a/flake.lock +++ b/flake.lock @@ -75,11 +75,11 @@ }, "nixpkgs_2": { "locked": { - "lastModified": 1780145889, - "narHash": "sha256-md0zn0RnwNvPyASas1yG5YUuwQ4ALA6ucL50l0DvqCo=", + "lastModified": 1788584326, + "narHash": "sha256-Fd3OB8J9JhgliQwOKcqx4M672CInxi1I5VnwsaXeSQo=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "8c50a710ddca43d7a530fb805ad55bde8d0141c5", + "rev": "6713828a351efa628b025a1adf7f43cbf8597513", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index 860d0be0..68c7683c 100644 --- a/flake.nix +++ b/flake.nix @@ -12,7 +12,7 @@ rust-overlay, ... }: - inputs.flake-utils.lib.eachSystem [ "x86_64-linux" "aarch64-darwin" ] ( + inputs.flake-utils.lib.eachSystem [ "x86_64-linux" "aarch64-darwin" "x86_64-darwin" ] ( system: let pkgs = import nixpkgs { @@ -107,35 +107,6 @@ doCheck = false; }; - build-sync-demo = pkgs.writeShellApplication { - name = "build-sync-demo"; - runtimeInputs = [ - coln-cli - pkgs.binaryen - pkgs.esbuild - pkgs.nodejs_24 - pkgs.pnpm - rustToolchain - wasm-bindgen-cli - wasm-bodge - ]; - text = '' - repo_root="''${1:-$PWD}" - cd "$repo_root" - - export CI="''${CI:-1}" - pnpm_store_dir="''${PNPM_STORE_DIR:-$repo_root/.pnpm-store}" - - npm ci --prefix packages/coln-js-runtime - npm run --prefix packages/coln-js-runtime build - - pnpm --dir examples/sync-demo install --frozen-lockfile --store-dir "$pnpm_store_dir" - pnpm --dir examples/sync-demo build - - echo "Built sync demo at $repo_root/examples/sync-demo/dist" - ''; - }; - format-hs = nuShellCheck [pkgs.fourmolu] ./nix/checks/format-hs.nu; format-cabal = nuShellCheck [pkgs.haskellPackages.cabal-gild] ./nix/checks/format-cabal.nu; @@ -202,14 +173,27 @@ in { inherit packages; - apps = let - buildSyncDemo = { - type = "app"; - program = "${pkgs.lib.getExe packages.build-sync-demo}"; - }; - in { - build-sync-demo = buildSyncDemo; - sync-demo = buildSyncDemo; + devShells.web = pkgs.mkShell { + name = "coln-web"; + buildInputs = with pkgs; [ + binaryen + esbuild + haskell-wasm.wasm32-wasi-ghc-9_14 + haskell-wasm.wasm32-wasi-cabal-9_14 + just + nodejs_24 + openssl + packages.wasm-bindgen-cli + packages.wasm-bodge + pkg-config + pnpm + rustToolchain + zlib + zlib.dev + ]; + shellHook = '' + export CFLAGS="''${CFLAGS:+$CFLAGS }-std=gnu17" + ''; }; devShells.default = pkgs.mkShell { name = "coln"; diff --git a/justfile b/justfile index 3e8b1a12..fe7e9471 100644 --- a/justfile +++ b/justfile @@ -3,7 +3,7 @@ # just # list available recipes # just check # run all package checks -generated_file_pathspecs := "':(exclude,glob)packages/coln-compiler/test/golden/basic-ir/*.ts.output/**' ':(exclude,glob)packages/coln-compiler/test/golden/*.ts.output/**' ':(exclude,glob)examples/sync-demo/src/generated/**'" +generated_file_pathspecs := "':(exclude,glob)packages/coln-compiler/test/golden/basic-ir/*.ts.output/**' ':(exclude,glob)packages/coln-compiler/test/golden/*.ts.output/**' ':(exclude,glob)examples/lab/src/tools/graph/generated/**' ':(exclude,glob)examples/patchwork/src/graph/generated/**'" default: @just --list diff --git a/packages/coln-compiler-wasm/README.md b/packages/coln-compiler-wasm/README.md index b949daa2..33a941c3 100644 --- a/packages/coln-compiler-wasm/README.md +++ b/packages/coln-compiler-wasm/README.md @@ -1,3 +1,3 @@ # Coln compiler Wasm library -This package can be compiled with the [GHC WebAssembly backend](https://downloads.haskell.org/ghc/latest/docs/users_guide/wasm.html) to produce a [WASI reactor module](https://github.com/WebAssembly/WASI/blob/wasi-0.1/application-abi.md#current-unstable-abi). See [this example](../../examples/compiler-demo/index.html) for how to call it from JavaScript. The example app can be launched with `just examples/serve-web-compiler` from the project root. All necessary tools, including a Wasm-targeting GHC and Cabal, are provided by this repository's top-level Nix shell. +This package can be compiled with the [GHC WebAssembly backend](https://downloads.haskell.org/ghc/latest/docs/users_guide/wasm.html) to produce a [WASI reactor module](https://github.com/WebAssembly/WASI/blob/wasi-0.1/application-abi.md#current-unstable-abi). The [Lab](../../examples/lab/README.md) uses it from JavaScript and can be launched with `just examples/dev-web-lab` from the project root. All necessary tools, including a Wasm-targeting GHC and Cabal, are provided by this repository's top-level Nix shell. diff --git a/packages/coln-repo/.gitignore b/packages/coln-repo/.gitignore new file mode 100644 index 00000000..fe30a0cf --- /dev/null +++ b/packages/coln-repo/.gitignore @@ -0,0 +1,4 @@ +dist/ +playwright-report/ +test-results/ +tests/browser/dist/ diff --git a/packages/coln-repo/.oxfmtrc.json b/packages/coln-repo/.oxfmtrc.json new file mode 100644 index 00000000..2764424d --- /dev/null +++ b/packages/coln-repo/.oxfmtrc.json @@ -0,0 +1,5 @@ +{ + "$schema": "./node_modules/oxfmt/configuration_schema.json", + "printWidth": 80, + "semi": false +} diff --git a/packages/coln-repo/README.md b/packages/coln-repo/README.md new file mode 100644 index 00000000..90682a09 --- /dev/null +++ b/packages/coln-repo/README.md @@ -0,0 +1,77 @@ +# `@coln-project/repo` + +Coln Repo exposes Coln stores as [Automerge Repo](https://github.com/automerge/automerge-repo) document handles. Use them to create or find stores, read the current typed view, make atomic changes, and subscribe to changes to update application state or UI. Automerge Repo provides persistence and synchronization. + +Coln Repo currently requires an experimental Automerge Repo branch with document types and custom URL schemes. + +## Create + +Create a Coln store using generated bindings. + +```ts +import * as Bindings from "./ExampleRealm.js" +import { create } from "@coln-project/repo" + +const handle = create(repo, Bindings) +``` + +## Find + +Find a Coln store by its `coln:` URL. Bindings are optional and must match the document schema when supplied. + +```ts +import * as Bindings from "./ExampleRealm.js" +import { find } from "@coln-project/repo" + +const handle = await find(repo, url, Bindings) +``` + +Apply bindings to a raw handle later if needed: + +```ts +import * as Bindings from "./ExampleRealm.js" +import { applyBindings, find } from "@coln-project/repo" + +const rawHandle = await find(repo, url) +const handle = applyBindings(rawHandle, Bindings) +``` + +> [!IMPORTANT] +> Once bindings are applied, later raw lookups in the same Repo may return that bound handle. It retains the raw Coln operations and adds `root` to documents and change transactions. + +## Read + +Read the current document synchronously or subscribe to changes. + +```ts +const doc = handle.doc() +doc.scanTable("Example.Items") +doc.root.Items.values() + +handle.on("change", ({ doc }) => { + doc?.scanTable("Example.Items") + doc?.root.Items.values() +}) +``` + +Documents expose `heads()`, `jsonIR()`, `rowById()`, and `scanTable()`. + +## Write + +Changes are synchronous and atomic. A thrown error aborts the transaction. + +```ts +handle.change((tx) => { + tx.root.Items.add() +}) +``` + +Transactions expose `add(path, values)`; bound transactions also expose `root`. + +The underlying `StoreHandle` is available through `handle.fullDoc().store` for advanced use. Its lifecycle operations bypass Coln Repo changes, events, and synchronization; prefer `doc()` and `change()`. + +## Current Limitations + +Values returned by `handle.doc()` are current documents, not durable snapshots. + +Generated read methods such as `has()` and `values()` are not supported inside `handle.change()`. Typed transactions currently support writes only. diff --git a/packages/coln-repo/package.json b/packages/coln-repo/package.json new file mode 100644 index 00000000..cd2f1fad --- /dev/null +++ b/packages/coln-repo/package.json @@ -0,0 +1,43 @@ +{ + "name": "@coln-project/repo", + "version": "0.0.1-alpha.0", + "private": true, + "license": "(Apache-2.0 OR MIT)", + "files": [ + "dist" + ], + "type": "module", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "scripts": { + "build": "tsc -p tsconfig.build.json", + "format": "oxfmt --write .", + "format:check": "oxfmt --check .", + "typecheck": "tsc --noEmit", + "test": "pnpm run format:check && pnpm run typecheck && pnpm run test:unit && pnpm run test:e2e", + "test:unit": "tsx --test \"tests/**/*.test.ts\"", + "test:package": "pnpm run build && tsc -p tsconfig.package.json && node --input-type=module --eval \"const pkg = await import('@coln-project/repo'); if (typeof pkg.find !== 'function') throw new Error('missing find export')\"", + "test:e2e": "pnpm run test:package && playwright test" + }, + "dependencies": { + "@automerge/automerge-repo": "github:onsetsoftware/automerge-repo#test/custom-url-scemes&path:packages/automerge-repo", + "@coln-project/runtime": "link:../coln-js-runtime" + }, + "devDependencies": { + "@automerge/automerge-subduction": "0.16.1", + "@playwright/test": "^1.63.0", + "@types/node": "^25.9.5", + "@types/ws": "^8.18.1", + "oxfmt": "0.66.0", + "tsx": "^4.23.13", + "typescript": "^6.0.3", + "vite": "^8.2.2", + "vite-plugin-wasm": "^3.6.0", + "ws": "^8.21.3" + } +} diff --git a/examples/sync-demo/playwright.config.ts b/packages/coln-repo/playwright.config.ts similarity index 60% rename from examples/sync-demo/playwright.config.ts rename to packages/coln-repo/playwright.config.ts index 9374d023..9a741d42 100644 --- a/examples/sync-demo/playwright.config.ts +++ b/packages/coln-repo/playwright.config.ts @@ -4,28 +4,31 @@ import { defineConfig, devices } from "@playwright/test" +const webPort = Number(process.env.COLN_REPO_TEST_WEB_PORT ?? 5174) + export default defineConfig({ testDir: "./tests", + testMatch: "**/*.spec.ts", timeout: 30_000, expect: { timeout: 10_000 }, fullyParallel: false, retries: 0, use: { - baseURL: "http://127.0.0.1:5173", + baseURL: `http://127.0.0.1:${webPort}`, trace: "retain-on-failure", }, webServer: [ { - command: "pnpm server", - port: 3030, - reuseExistingServer: !process.env.CI, + command: "node tests/server.mjs", + port: 3031, + reuseExistingServer: false, stdout: "pipe", stderr: "pipe", }, { - command: "pnpm dev -- --port 5173", - url: "http://127.0.0.1:5173", - reuseExistingServer: !process.env.CI, + command: `./node_modules/.bin/vite --host 127.0.0.1 --port ${webPort}`, + url: `http://127.0.0.1:${webPort}`, + reuseExistingServer: false, stdout: "pipe", stderr: "pipe", }, diff --git a/packages/coln-repo/pnpm-lock.yaml b/packages/coln-repo/pnpm-lock.yaml new file mode 100644 index 00000000..877da2cf --- /dev/null +++ b/packages/coln-repo/pnpm-lock.yaml @@ -0,0 +1,1244 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +overrides: + '@automerge/automerge': 3.3.2 + '@automerge/automerge-repo': github:onsetsoftware/automerge-repo#test/custom-url-scemes&path:packages/automerge-repo + '@automerge/automerge-subduction': 0.16.1 + +importers: + + .: + dependencies: + '@automerge/automerge-repo': + specifier: github:onsetsoftware/automerge-repo#test/custom-url-scemes&path:packages/automerge-repo + version: https://codeload.github.com/onsetsoftware/automerge-repo/tar.gz/b39d94c5624a3497150e08ba18546f29222289d8#path:packages/automerge-repo + '@coln-project/runtime': + specifier: link:../coln-js-runtime + version: link:../coln-js-runtime + devDependencies: + '@automerge/automerge-subduction': + specifier: 0.16.1 + version: 0.16.1 + '@playwright/test': + specifier: ^1.63.0 + version: 1.63.0 + '@types/node': + specifier: ^25.9.5 + version: 25.9.5 + '@types/ws': + specifier: ^8.18.1 + version: 8.18.1 + oxfmt: + specifier: 0.66.0 + version: 0.66.0 + tsx: + specifier: ^4.23.13 + version: 4.23.13 + typescript: + specifier: ^6.0.3 + version: 6.0.3 + vite: + specifier: ^8.2.2 + version: 8.2.2(@types/node@25.9.5)(esbuild@0.28.2)(tsx@4.23.13) + vite-plugin-wasm: + specifier: ^3.6.0 + version: 3.6.0(vite@8.2.2(@types/node@25.9.5)(esbuild@0.28.2)(tsx@4.23.13)) + ws: + specifier: ^8.21.3 + version: 8.21.3 + +packages: + + '@automerge/automerge-repo@https://codeload.github.com/onsetsoftware/automerge-repo/tar.gz/b39d94c5624a3497150e08ba18546f29222289d8#path:packages/automerge-repo': + resolution: {gitHosted: true, integrity: sha512-6OS6f5Jo1ZuZLy2G43c74SHh1/a0fIO8QB7OsX4jJZSu7fMYjCoDWTZ2rX/MMzOU2Ywjg9CLCjy9YV2w5gvmxg==, path: packages/automerge-repo, tarball: https://codeload.github.com/onsetsoftware/automerge-repo/tar.gz/b39d94c5624a3497150e08ba18546f29222289d8} + version: 2.6.0-subduction.47 + engines: {node: '>=22.13'} + + '@automerge/automerge-subduction@0.16.1': + resolution: {integrity: sha512-alH7U4eYn0O7sT6hNLv7CJhNODJi+QD6qfRQTNfZCxL3Q5JZdJ8lFpdvXUkBf+I3bu0GLvVlTaYPWGrBihjpIw==} + + '@automerge/automerge@3.3.2': + resolution: {integrity: sha512-9vCdCL7pdQwUra66SBxPVHr+/t9epXKni9KDeak2rNBFMzABVh2u6gSpcwxi3jkR5qr047jVqZv9DlrOfVxLFw==} + + '@cbor-extract/cbor-extract-darwin-arm64@2.2.2': + resolution: {integrity: sha512-ZKZ/F8US7JR92J4DMct6cLW/Y66o2K576+zjlEN/MevH70bFIsB10wkZEQPLzl2oNh2SMGy55xpJ9JoBRl5DOA==} + cpu: [arm64] + os: [darwin] + + '@cbor-extract/cbor-extract-darwin-x64@2.2.2': + resolution: {integrity: sha512-32b1mgc+P61Js+KW9VZv/c+xRw5EfmOcPx990JbCBSkYJFY0l25VinvyyWfl+3KjibQmAcYwmyzKF9J4DyKP/Q==} + cpu: [x64] + os: [darwin] + + '@cbor-extract/cbor-extract-linux-arm64@2.2.2': + resolution: {integrity: sha512-wfqgzqCAy/Vn8i6WVIh7qZd0DdBFaWBjPdB6ma+Wihcjv0gHqD/mw3ouVv7kbbUNrab6dKEx/w3xQZEdeXIlzg==} + cpu: [arm64] + os: [linux] + + '@cbor-extract/cbor-extract-linux-arm@2.2.2': + resolution: {integrity: sha512-tNg0za41TpQfkhWjptD+0gSD2fggMiDCSacuIeELyb2xZhr7PrhPe5h66Jc67B/5dmpIhI2QOUtv4SBsricyYQ==} + cpu: [arm] + os: [linux] + + '@cbor-extract/cbor-extract-linux-x64@2.2.2': + resolution: {integrity: sha512-rpiLnVEsqtPJ+mXTdx1rfz4RtUGYIUg2rUAZgd1KjiC1SehYUSkJN7Yh+aVfSjvCGtVP0/bfkQkXpPXKbmSUaA==} + cpu: [x64] + os: [linux] + + '@cbor-extract/cbor-extract-win32-x64@2.2.2': + resolution: {integrity: sha512-dI+9P7cfWxkTQ+oE+7Aa6onEn92PHgfWXZivjNheCRmTBDBf2fx6RyTi0cmgpYLnD1KLZK9ZYrMxaPZ4oiXhGA==} + cpu: [x64] + os: [win32] + + '@esbuild/aix-ppc64@0.28.2': + resolution: {integrity: sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.28.2': + resolution: {integrity: sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.28.2': + resolution: {integrity: sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.28.2': + resolution: {integrity: sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.28.2': + resolution: {integrity: sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.28.2': + resolution: {integrity: sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.28.2': + resolution: {integrity: sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.28.2': + resolution: {integrity: sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.28.2': + resolution: {integrity: sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.28.2': + resolution: {integrity: sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.28.2': + resolution: {integrity: sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.28.2': + resolution: {integrity: sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.28.2': + resolution: {integrity: sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.28.2': + resolution: {integrity: sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.28.2': + resolution: {integrity: sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.28.2': + resolution: {integrity: sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.28.2': + resolution: {integrity: sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.28.2': + resolution: {integrity: sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.28.2': + resolution: {integrity: sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.28.2': + resolution: {integrity: sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.28.2': + resolution: {integrity: sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.28.2': + resolution: {integrity: sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.28.2': + resolution: {integrity: sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.28.2': + resolution: {integrity: sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.28.2': + resolution: {integrity: sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.28.2': + resolution: {integrity: sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@noble/hashes@1.8.0': + resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==} + engines: {node: ^14.21.3 || >=16} + + '@oxc-project/types@0.148.0': + resolution: {integrity: sha512-Nm4s/jB+4FpFsPhWGEC4h7rzksesmtnMXomo6rCMcg/b8zLQuOziRgkCS1fxDCXOlJB/6Q8oABOZ/OP6RIPj9A==} + + '@oxfmt/binding-android-arm-eabi@0.66.0': + resolution: {integrity: sha512-2Me9eoptv6ERdEuI2P8AOlYdHHraXebJaM6SC0kc2Dfb+mLrep2db+fedBPKaYn673h/vBgvP4tkOdAbaudX6w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] + + '@oxfmt/binding-android-arm64@0.66.0': + resolution: {integrity: sha512-u7O+bSSF0HGsDKkQQxBqvLGVepu93RA+JKu+ONqvfh4sCnCEbj31wZj4iG5gk3XfRwrmYj0/8catkO2LcblQKQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@oxfmt/binding-darwin-arm64@0.66.0': + resolution: {integrity: sha512-/ikyMIVjX/sdo7KtjxoEsSUosfPzveVhT9RWMx9yGqFDKFJ89JAEKuEeLBmurDjrkb4w8tOnAdSO3SBaplY3bw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@oxfmt/binding-darwin-x64@0.66.0': + resolution: {integrity: sha512-q5xUsKeFqawa9NXa6ZGXWimFV19m8MogKPdTaSVDAAk2EQKBmBZRDeluwcl1p8ty/OFc9s9888OKEh3xfPVH0g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@oxfmt/binding-freebsd-x64@0.66.0': + resolution: {integrity: sha512-CR+x4VzMY0pRXLK/xFQ/RzsSFkP5t2Z2mef0QY6OP/rTRcMUoMLCOM62/3Fp/t0K+UDoBKxvMyeb6D0zPMjleA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@oxfmt/binding-linux-arm-gnueabihf@0.66.0': + resolution: {integrity: sha512-ZEYmO/LbH9tTQCADILHGZE4GeOXOAj2VzedHkASNwjmwlwtutJCLpCJbIs37wRGTFgWRoEcD72jpMX+IBJUGjQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxfmt/binding-linux-arm-musleabihf@0.66.0': + resolution: {integrity: sha512-hNtR9/oU0CeTkq7JnRkmBQwqe17v2ZaAMLC4VcN7IIOWeRyWDk0knSPWS9iiLmtbZ2RRBBtsG01jQgkZmKCJeQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxfmt/binding-linux-arm64-gnu@0.66.0': + resolution: {integrity: sha512-uwOVQ8i6I1LT/+eDzfsgrrcZp8Fn6NPVUPn8fF5gdFGekFf0PddF+LEuwsD0/pbNUcKZhDj2rQ5UpITh9gF4iQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@oxfmt/binding-linux-arm64-musl@0.66.0': + resolution: {integrity: sha512-tTkF2Dmx4nGAjmBlZb+UtTGqR/EK4ZrW9qBfzte07a9XWqzoGGKzpFFlyNDhQe+Uwql94+ReCTeNbhOXscw1Dg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@oxfmt/binding-linux-ppc64-gnu@0.66.0': + resolution: {integrity: sha512-F3cKHUav4yXOHn6GFnwpBhSYsJOYKKf9eqO/9jlEuqPxNw9zb98E9ZFct79gcg8pibUGkbveEu9WDlmXJpDzKw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@oxfmt/binding-linux-riscv64-gnu@0.66.0': + resolution: {integrity: sha512-K5fDaNZfDyQMYA/3qL21bqyN0X9T15LLwwbFPt2aHc94+ZG7bh0vZEsy2y7NlRnjjHFSwN+Hzg6ldJtbOriH4Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@oxfmt/binding-linux-riscv64-musl@0.66.0': + resolution: {integrity: sha512-44Yc+I+qOmTElRcEhm5hUKIUJEQIOugymz4ua4tB0Wox7tGAfIbjzmXz/HDAtw1Ij6gmBwZlzh4hc9679RhWeA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@oxfmt/binding-linux-s390x-gnu@0.66.0': + resolution: {integrity: sha512-1e29Eg9hEj2kRBB19M0seIehPbbXHCk35GvImjDvb79rjjYjXCRmtbUNHJcgoktZAMIzXrTbxDBKmTc1V4bg3A==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@oxfmt/binding-linux-x64-gnu@0.66.0': + resolution: {integrity: sha512-vODY1UQo10gngn0+D4xHKU84F1Twm1LqrzV4SqPXvmQKSd87paehvZ6jqA5wKs6XQrlWul9clYMDVHcoW9CPMA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@oxfmt/binding-linux-x64-musl@0.66.0': + resolution: {integrity: sha512-YDzXx2JsT4+HL4MdkVrYjO55NS5lUKNm8rLC4ZPou8+seu0v0jhecSh+ufoO6+xEa8gccEezMlI2WHJi4ApUgw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@oxfmt/binding-openharmony-arm64@0.66.0': + resolution: {integrity: sha512-mJjUYd8lj0+j4JkYyEM+5qKBf1Rnrpgjn/SVYKJhicVDqLz566ooa7Fs8zflPqt+dnZDV7X054rVIQX6ZcQNlQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@oxfmt/binding-win32-arm64-msvc@0.66.0': + resolution: {integrity: sha512-soV+0vESv7e5ntCHWC61x4gg8OSak6IHHnWsZmHrJFlvMj2AK+kmldErCNkVkrvc1Ts2/++rJXn+IuAb2WMXhw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@oxfmt/binding-win32-ia32-msvc@0.66.0': + resolution: {integrity: sha512-YCPi23uRIEYuIKTZohAkKbPFpujQ5QBuUM5iDv+UqbCmTPAkaFsxjsSuB8xlBpRT0G7eP/4HMF+cPDSqHtOD9A==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ia32] + os: [win32] + + '@oxfmt/binding-win32-x64-msvc@0.66.0': + resolution: {integrity: sha512-bwTQcv/JVRPkOqQtMF0X7vpvpncDQiBcXHxZ9S2hR12Hlo8bvBdUR5x5XnxzDZ3kM0qoZw1rv7KaD66Ly+pFWA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@playwright/test@1.63.0': + resolution: {integrity: sha512-oxMK4vllB9RK5NQ2l1pq1IfOf2AvnEuj/vYGDj0H2nMtmtZpKtCwt/l00GEO6xjGfpBNAvjovvYdCm50dRQkpQ==} + engines: {node: '>=20'} + hasBin: true + + '@rolldown/binding-android-arm-eabi@1.2.7': + resolution: {integrity: sha512-EypzgnYCwyVY4NDHKzGmNJT5b+XaQEBniHxsMdeIQLB/tcCzZnhqrzHpZFbX9iaxx+5RiB8caATBtfvZP7zVxQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] + + '@rolldown/binding-android-arm64@1.2.7': + resolution: {integrity: sha512-l17HE9EweWaqJZhuUuNBN/FzM62xw+DECVnJyvMsxn8vJFAGLy5QfLDoYAcronkAN8VxKZHezDpulHDPx95vFw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.2.7': + resolution: {integrity: sha512-8ED8ELFvHXc6OCETIn4gXObPiaR6bckM/ipXtbzlPVDRMBfEGjCKgO90F9YtfdpDatVx/ZQw7aZ1vUMf/+T3Mw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.2.7': + resolution: {integrity: sha512-/WPripjtiAIZ2tWY7ddijORT0Ujg87wxWW/qcoFVCKAWVDPhtY0xr7Dj0M3GyNGz60jGwTElhro/mkF9dT7dDQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.2.7': + resolution: {integrity: sha512-14DI4NcqpvbICxSnGLx3PmtDaWqRP/KGSGb6C+JLLVPeZRl6dKdHba3pGsqT3vpdTqhEYIPG0MMQ8c0xYqoJxA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.2.7': + resolution: {integrity: sha512-bxrWIRvHWQvbJwi+VIie/kDJmQxcNE6xxWwZdqF/ExVAigtHkv54WTLQPb+QsZdnFy18fg7JPfWGL0RH6vwIlQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.2.7': + resolution: {integrity: sha512-toOY2BChBZyuxU7OYX6Tn389di4IzAqPTycVcci0O7FSfBqzRB3RZn+K5Is6ANf4tmgRd/K1yZTsNTXbkXsnLg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-arm64-musl@1.2.7': + resolution: {integrity: sha512-lAIXTH/aiLRLxsTgQvfhjo4K1ydWIp00+V0voOr9beb/9ZmkUFrSIb03dXNFRgMNvkE6oGsF10ioQ6UsI+vS5Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rolldown/binding-linux-ppc64-gnu@1.2.7': + resolution: {integrity: sha512-kdnwS28Pkenp/mZMRwjXXXwxQ7pIsm+bF919LUK93BOyhcLsrVKdP2p9fxpiPNPAbNuch8ypQt0pm2P2LYCAGg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-s390x-gnu@1.2.7': + resolution: {integrity: sha512-516OdsyLdr5E65paF3yBF55t8mfm9+gmtCsK3xI7XKXIT7EfRlHhxL8K/NR6Hu8BWSgF5+1w74lTL0+nxcc8Qw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-gnu@1.2.7': + resolution: {integrity: sha512-r8/z8n7GFaYRln3xmP1Cxy0HH/HLM0uBUPkEuSVEfKGDA89M0FsZRZJRSwe/tJjRx+fpH/gjorfhB8tmEbSFLA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-musl@1.2.7': + resolution: {integrity: sha512-pAsE8iiDxUg1xBqdhrTfg45AVDVpirjz00sblEYClGNNcMnDb+e8beQgqIAw6LvauX/APvgxUnwrgun/YYGBhw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rolldown/binding-openharmony-arm64@1.2.7': + resolution: {integrity: sha512-lTcIYmmnQQA8Or/2DatS6oSqcdLHvendjS+zLu+FwgToynWMRSmQdpM65fTANJgIS4mjbMOo5KT2lnT9SAb96w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-win32-arm64-msvc@1.2.7': + resolution: {integrity: sha512-e3Gu3WxbNk/UqQhxqU7YIYO+9ZBvWNz3U+h/qRFosscMFzdRPbXYSaSWgSnklv2fz1TgzBTcti2z35c/7irsHw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.2.7': + resolution: {integrity: sha512-W/jg5qoRSqjsEv0+dZi4e687mcHqmVuU0P4fK6qS/xjetW2Gmc1W8j//z5nAeNcC8Ttm0hV46IjcYeuVwYhuiw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@rolldown/pluginutils@1.0.1': + resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} + + '@types/node@25.9.5': + resolution: {integrity: sha512-OScDchr2fwuUmWdf4kZ9h7PcJiYDVInhJizG/biAq3cAvqwYktuy/TYGGdZNMtNTFUP7rnb0NU4TUdm82kt4Rg==} + + '@types/ws@8.18.1': + resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} + + base-x@5.0.1: + resolution: {integrity: sha512-M7uio8Zt++eg3jPj+rHMfCC+IuygQHHCOU+IYsVtik6FWjuYpVt/+MRKcgsAMHh8mMFAwnB+Bs+mTrFiXjMzKg==} + + bs58@6.0.0: + resolution: {integrity: sha512-PD0wEnEYg6ijszw/u8s+iI3H17cTymlrwkKhDhPZq+Sokl3AU4htyBFTjAeNAlCCmg0f53g6ih3jATyCKftTfw==} + + bs58check@4.0.0: + resolution: {integrity: sha512-FsGDOnFg9aVI9erdriULkd/JjEWONV/lQE5aYziB5PoBsXRind56lh8doIZIc9X4HoxT5x4bLjMWN1/NB8Zp5g==} + + cbor-extract@2.2.2: + resolution: {integrity: sha512-hlSxxI9XO2yQfe9g6msd3g4xCfDqK5T5P0fRMLuaLHhxn4ViPrm+a+MUfhrvH2W962RGxcBwEGzLQyjbDG1gng==} + hasBin: true + + cbor-x@1.6.6: + resolution: {integrity: sha512-8QiD9PGOxyQHo7s2pzwTBH6lTjqekxPdl9Aq6fXvZgCuCJHOht1puDEA/fTr6mciB76c+M+Gi0qT2i1a4pm4Wg==} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + esbuild@0.28.2: + resolution: {integrity: sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==} + engines: {node: '>=18'} + hasBin: true + + eventemitter3@5.0.4: + resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==} + + fast-sha256@1.3.0: + resolution: {integrity: sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + isomorphic-ws@5.0.0: + resolution: {integrity: sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw==} + peerDependencies: + ws: '*' + + lightningcss-android-arm64@1.33.0: + resolution: {integrity: sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.33.0: + resolution: {integrity: sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.33.0: + resolution: {integrity: sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.33.0: + resolution: {integrity: sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.33.0: + resolution: {integrity: sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.33.0: + resolution: {integrity: sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + lightningcss-linux-arm64-musl@1.33.0: + resolution: {integrity: sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + lightningcss-linux-x64-gnu@1.33.0: + resolution: {integrity: sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + lightningcss-linux-x64-musl@1.33.0: + resolution: {integrity: sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + lightningcss-win32-arm64-msvc@1.33.0: + resolution: {integrity: sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.33.0: + resolution: {integrity: sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.33.0: + resolution: {integrity: sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==} + engines: {node: '>= 12.0.0'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.18: + resolution: {integrity: sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + node-gyp-build-optional-packages@5.1.1: + resolution: {integrity: sha512-+P72GAjVAbTxjjwUmwjVrqrdZROD4nf8KgpBoDxqXXTiYZZt/ud60dE5yvCSr9lRO8e8yv6kgJIC0K0PfZFVQw==} + hasBin: true + + oxfmt@0.66.0: + resolution: {integrity: sha512-FfvqR8RFtV6JJpRrpkfqyVCQ7HDvZ/VriWFx7veftCgL1B5ZO9qNr+1rvPieycMQnNfVG0PWyJQiy7p0hq1I5w==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + svelte: ^5.0.0 + vite-plus: '*' + peerDependenciesMeta: + svelte: + optional: true + vite-plus: + optional: true + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.7: + resolution: {integrity: sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==} + engines: {node: '>=12'} + + playwright-core@1.63.0: + resolution: {integrity: sha512-rYCsBF/M5HjUch52bbtVONEFjv6Xu8sm8h72dNlR5bzIE1fvC/bxgspzkjSfU+MweEMmPM8KJebG6nnyxo5mCg==} + engines: {node: '>=20'} + hasBin: true + + playwright@1.63.0: + resolution: {integrity: sha512-+7ziBLidS4NaNCdt57SUDT+wYmmd5fmiQejUic/kb+YsYSCPyOOE9sebzMjNmQrsnNpDJqd4WHvV/8lfKfUDUg==} + engines: {node: '>=20'} + hasBin: true + + postcss@8.5.28: + resolution: {integrity: sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==} + engines: {node: ^10 || ^12 || >=14} + + rolldown@1.2.7: + resolution: {integrity: sha512-g0EtLvBjTUB7jhyV0S/TCup3v/XSVl45vUIGbOGU4QPiyjTenCe4mKuFvW9fEgYmS2Fo42AUssRmNuMziXdrig==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + + tinypool@2.1.0: + resolution: {integrity: sha512-Pugqs6M0m7Lv1I7FtxN4aoyToKg1C4tu+/381vH35y8oENM/Ai7f7C4StcoK4/+BSw9ebcS8jRiVrORFKCALLw==} + engines: {node: ^20.0.0 || >=22.0.0} + + tsx@4.23.13: + resolution: {integrity: sha512-BL5MGkRln6aDYhb0xbQlEAGw743BaZYWdbWtdJOBriYJboKgUUYCadFp2/FpBBZquBC/ezNBn7wMMPx7FDZUDw==} + engines: {node: '>=18.0.0'} + hasBin: true + + typescript@6.0.3: + resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@7.24.6: + resolution: {integrity: sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==} + + uuid@14.0.2: + resolution: {integrity: sha512-xZe/16rV4aa+HGSOCiY2YeLT1OybRLrrkL/Rqaq7p7GMVXjFh+6wN4oMYgjFmnSnhY8t6Xpdl2l9qmnHYuMHwQ==} + hasBin: true + + vite-plugin-wasm@3.6.0: + resolution: {integrity: sha512-mL/QPziiIA4RAA6DkaZZzOstdwbW5jO4Vz7Zenj0wieKWBlNvIvX5L5ljum9lcUX0ShNfBgCNLKTjNkRVVqcsw==} + peerDependencies: + vite: ^2 || ^3 || ^4 || ^5 || ^6 || ^7 || ^8 + + vite@8.2.2: + resolution: {integrity: sha512-cFKLV/PRgAUlIRm5WjMjJ86jrftzpqcgH+Us+DS8mI3CDNiH30Whrz8uHL3+MOLPAgqbMBAqWdAHAphOAM+z/Q==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + '@vitejs/devtools': ^0.4.0 || ^0.5.0 + esbuild: ^0.27.0 || ^0.28.0 + jiti: '>=1.21.0' + less: ^4.0.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + '@vitejs/devtools': + optional: true + esbuild: + optional: true + jiti: + optional: true + less: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + ws@8.21.3: + resolution: {integrity: sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + xstate@5.32.6: + resolution: {integrity: sha512-WfA8WNrh6r9osuGwVm+aIPM4jmMW2LKHV1Yv9thYpOtL4HVF/kobh95K/O8v2jDCpDmP2EoY+6uvuqHXCZ6ZLw==} + +snapshots: + + '@automerge/automerge-repo@https://codeload.github.com/onsetsoftware/automerge-repo/tar.gz/b39d94c5624a3497150e08ba18546f29222289d8#path:packages/automerge-repo': + dependencies: + '@automerge/automerge': 3.3.2 + '@automerge/automerge-subduction': 0.16.1 + bs58check: 4.0.0 + cbor-x: 1.6.6 + debug: 4.4.3 + eventemitter3: 5.0.4 + fast-sha256: 1.3.0 + isomorphic-ws: 5.0.0(ws@8.21.3) + uuid: 14.0.2 + ws: 8.21.3 + xstate: 5.32.6 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + '@automerge/automerge-subduction@0.16.1': {} + + '@automerge/automerge@3.3.2': {} + + '@cbor-extract/cbor-extract-darwin-arm64@2.2.2': + optional: true + + '@cbor-extract/cbor-extract-darwin-x64@2.2.2': + optional: true + + '@cbor-extract/cbor-extract-linux-arm64@2.2.2': + optional: true + + '@cbor-extract/cbor-extract-linux-arm@2.2.2': + optional: true + + '@cbor-extract/cbor-extract-linux-x64@2.2.2': + optional: true + + '@cbor-extract/cbor-extract-win32-x64@2.2.2': + optional: true + + '@esbuild/aix-ppc64@0.28.2': + optional: true + + '@esbuild/android-arm64@0.28.2': + optional: true + + '@esbuild/android-arm@0.28.2': + optional: true + + '@esbuild/android-x64@0.28.2': + optional: true + + '@esbuild/darwin-arm64@0.28.2': + optional: true + + '@esbuild/darwin-x64@0.28.2': + optional: true + + '@esbuild/freebsd-arm64@0.28.2': + optional: true + + '@esbuild/freebsd-x64@0.28.2': + optional: true + + '@esbuild/linux-arm64@0.28.2': + optional: true + + '@esbuild/linux-arm@0.28.2': + optional: true + + '@esbuild/linux-ia32@0.28.2': + optional: true + + '@esbuild/linux-loong64@0.28.2': + optional: true + + '@esbuild/linux-mips64el@0.28.2': + optional: true + + '@esbuild/linux-ppc64@0.28.2': + optional: true + + '@esbuild/linux-riscv64@0.28.2': + optional: true + + '@esbuild/linux-s390x@0.28.2': + optional: true + + '@esbuild/linux-x64@0.28.2': + optional: true + + '@esbuild/netbsd-arm64@0.28.2': + optional: true + + '@esbuild/netbsd-x64@0.28.2': + optional: true + + '@esbuild/openbsd-arm64@0.28.2': + optional: true + + '@esbuild/openbsd-x64@0.28.2': + optional: true + + '@esbuild/openharmony-arm64@0.28.2': + optional: true + + '@esbuild/sunos-x64@0.28.2': + optional: true + + '@esbuild/win32-arm64@0.28.2': + optional: true + + '@esbuild/win32-ia32@0.28.2': + optional: true + + '@esbuild/win32-x64@0.28.2': + optional: true + + '@noble/hashes@1.8.0': {} + + '@oxc-project/types@0.148.0': {} + + '@oxfmt/binding-android-arm-eabi@0.66.0': + optional: true + + '@oxfmt/binding-android-arm64@0.66.0': + optional: true + + '@oxfmt/binding-darwin-arm64@0.66.0': + optional: true + + '@oxfmt/binding-darwin-x64@0.66.0': + optional: true + + '@oxfmt/binding-freebsd-x64@0.66.0': + optional: true + + '@oxfmt/binding-linux-arm-gnueabihf@0.66.0': + optional: true + + '@oxfmt/binding-linux-arm-musleabihf@0.66.0': + optional: true + + '@oxfmt/binding-linux-arm64-gnu@0.66.0': + optional: true + + '@oxfmt/binding-linux-arm64-musl@0.66.0': + optional: true + + '@oxfmt/binding-linux-ppc64-gnu@0.66.0': + optional: true + + '@oxfmt/binding-linux-riscv64-gnu@0.66.0': + optional: true + + '@oxfmt/binding-linux-riscv64-musl@0.66.0': + optional: true + + '@oxfmt/binding-linux-s390x-gnu@0.66.0': + optional: true + + '@oxfmt/binding-linux-x64-gnu@0.66.0': + optional: true + + '@oxfmt/binding-linux-x64-musl@0.66.0': + optional: true + + '@oxfmt/binding-openharmony-arm64@0.66.0': + optional: true + + '@oxfmt/binding-win32-arm64-msvc@0.66.0': + optional: true + + '@oxfmt/binding-win32-ia32-msvc@0.66.0': + optional: true + + '@oxfmt/binding-win32-x64-msvc@0.66.0': + optional: true + + '@playwright/test@1.63.0': + dependencies: + playwright: 1.63.0 + + '@rolldown/binding-android-arm-eabi@1.2.7': + optional: true + + '@rolldown/binding-android-arm64@1.2.7': + optional: true + + '@rolldown/binding-darwin-arm64@1.2.7': + optional: true + + '@rolldown/binding-darwin-x64@1.2.7': + optional: true + + '@rolldown/binding-freebsd-x64@1.2.7': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.2.7': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.2.7': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.2.7': + optional: true + + '@rolldown/binding-linux-ppc64-gnu@1.2.7': + optional: true + + '@rolldown/binding-linux-s390x-gnu@1.2.7': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.2.7': + optional: true + + '@rolldown/binding-linux-x64-musl@1.2.7': + optional: true + + '@rolldown/binding-openharmony-arm64@1.2.7': + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.2.7': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.2.7': + optional: true + + '@rolldown/pluginutils@1.0.1': {} + + '@types/node@25.9.5': + dependencies: + undici-types: 7.24.6 + + '@types/ws@8.18.1': + dependencies: + '@types/node': 25.9.5 + + base-x@5.0.1: {} + + bs58@6.0.0: + dependencies: + base-x: 5.0.1 + + bs58check@4.0.0: + dependencies: + '@noble/hashes': 1.8.0 + bs58: 6.0.0 + + cbor-extract@2.2.2: + dependencies: + node-gyp-build-optional-packages: 5.1.1 + optionalDependencies: + '@cbor-extract/cbor-extract-darwin-arm64': 2.2.2 + '@cbor-extract/cbor-extract-darwin-x64': 2.2.2 + '@cbor-extract/cbor-extract-linux-arm': 2.2.2 + '@cbor-extract/cbor-extract-linux-arm64': 2.2.2 + '@cbor-extract/cbor-extract-linux-x64': 2.2.2 + '@cbor-extract/cbor-extract-win32-x64': 2.2.2 + optional: true + + cbor-x@1.6.6: + optionalDependencies: + cbor-extract: 2.2.2 + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + detect-libc@2.1.2: {} + + esbuild@0.28.2: + optionalDependencies: + '@esbuild/aix-ppc64': 0.28.2 + '@esbuild/android-arm': 0.28.2 + '@esbuild/android-arm64': 0.28.2 + '@esbuild/android-x64': 0.28.2 + '@esbuild/darwin-arm64': 0.28.2 + '@esbuild/darwin-x64': 0.28.2 + '@esbuild/freebsd-arm64': 0.28.2 + '@esbuild/freebsd-x64': 0.28.2 + '@esbuild/linux-arm': 0.28.2 + '@esbuild/linux-arm64': 0.28.2 + '@esbuild/linux-ia32': 0.28.2 + '@esbuild/linux-loong64': 0.28.2 + '@esbuild/linux-mips64el': 0.28.2 + '@esbuild/linux-ppc64': 0.28.2 + '@esbuild/linux-riscv64': 0.28.2 + '@esbuild/linux-s390x': 0.28.2 + '@esbuild/linux-x64': 0.28.2 + '@esbuild/netbsd-arm64': 0.28.2 + '@esbuild/netbsd-x64': 0.28.2 + '@esbuild/openbsd-arm64': 0.28.2 + '@esbuild/openbsd-x64': 0.28.2 + '@esbuild/openharmony-arm64': 0.28.2 + '@esbuild/sunos-x64': 0.28.2 + '@esbuild/win32-arm64': 0.28.2 + '@esbuild/win32-ia32': 0.28.2 + '@esbuild/win32-x64': 0.28.2 + + eventemitter3@5.0.4: {} + + fast-sha256@1.3.0: {} + + fdir@6.5.0(picomatch@4.0.7): + optionalDependencies: + picomatch: 4.0.7 + + fsevents@2.3.3: + optional: true + + isomorphic-ws@5.0.0(ws@8.21.3): + dependencies: + ws: 8.21.3 + + lightningcss-android-arm64@1.33.0: + optional: true + + lightningcss-darwin-arm64@1.33.0: + optional: true + + lightningcss-darwin-x64@1.33.0: + optional: true + + lightningcss-freebsd-x64@1.33.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.33.0: + optional: true + + lightningcss-linux-arm64-gnu@1.33.0: + optional: true + + lightningcss-linux-arm64-musl@1.33.0: + optional: true + + lightningcss-linux-x64-gnu@1.33.0: + optional: true + + lightningcss-linux-x64-musl@1.33.0: + optional: true + + lightningcss-win32-arm64-msvc@1.33.0: + optional: true + + lightningcss-win32-x64-msvc@1.33.0: + optional: true + + lightningcss@1.33.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.33.0 + lightningcss-darwin-arm64: 1.33.0 + lightningcss-darwin-x64: 1.33.0 + lightningcss-freebsd-x64: 1.33.0 + lightningcss-linux-arm-gnueabihf: 1.33.0 + lightningcss-linux-arm64-gnu: 1.33.0 + lightningcss-linux-arm64-musl: 1.33.0 + lightningcss-linux-x64-gnu: 1.33.0 + lightningcss-linux-x64-musl: 1.33.0 + lightningcss-win32-arm64-msvc: 1.33.0 + lightningcss-win32-x64-msvc: 1.33.0 + + ms@2.1.3: {} + + nanoid@3.3.18: {} + + node-gyp-build-optional-packages@5.1.1: + dependencies: + detect-libc: 2.1.2 + optional: true + + oxfmt@0.66.0: + dependencies: + tinypool: 2.1.0 + optionalDependencies: + '@oxfmt/binding-android-arm-eabi': 0.66.0 + '@oxfmt/binding-android-arm64': 0.66.0 + '@oxfmt/binding-darwin-arm64': 0.66.0 + '@oxfmt/binding-darwin-x64': 0.66.0 + '@oxfmt/binding-freebsd-x64': 0.66.0 + '@oxfmt/binding-linux-arm-gnueabihf': 0.66.0 + '@oxfmt/binding-linux-arm-musleabihf': 0.66.0 + '@oxfmt/binding-linux-arm64-gnu': 0.66.0 + '@oxfmt/binding-linux-arm64-musl': 0.66.0 + '@oxfmt/binding-linux-ppc64-gnu': 0.66.0 + '@oxfmt/binding-linux-riscv64-gnu': 0.66.0 + '@oxfmt/binding-linux-riscv64-musl': 0.66.0 + '@oxfmt/binding-linux-s390x-gnu': 0.66.0 + '@oxfmt/binding-linux-x64-gnu': 0.66.0 + '@oxfmt/binding-linux-x64-musl': 0.66.0 + '@oxfmt/binding-openharmony-arm64': 0.66.0 + '@oxfmt/binding-win32-arm64-msvc': 0.66.0 + '@oxfmt/binding-win32-ia32-msvc': 0.66.0 + '@oxfmt/binding-win32-x64-msvc': 0.66.0 + + picocolors@1.1.1: {} + + picomatch@4.0.7: {} + + playwright-core@1.63.0: {} + + playwright@1.63.0: + dependencies: + playwright-core: 1.63.0 + + postcss@8.5.28: + dependencies: + nanoid: 3.3.18 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + rolldown@1.2.7: + dependencies: + '@oxc-project/types': 0.148.0 + '@rolldown/pluginutils': 1.0.1 + optionalDependencies: + '@rolldown/binding-android-arm-eabi': 1.2.7 + '@rolldown/binding-android-arm64': 1.2.7 + '@rolldown/binding-darwin-arm64': 1.2.7 + '@rolldown/binding-darwin-x64': 1.2.7 + '@rolldown/binding-freebsd-x64': 1.2.7 + '@rolldown/binding-linux-arm-gnueabihf': 1.2.7 + '@rolldown/binding-linux-arm64-gnu': 1.2.7 + '@rolldown/binding-linux-arm64-musl': 1.2.7 + '@rolldown/binding-linux-ppc64-gnu': 1.2.7 + '@rolldown/binding-linux-s390x-gnu': 1.2.7 + '@rolldown/binding-linux-x64-gnu': 1.2.7 + '@rolldown/binding-linux-x64-musl': 1.2.7 + '@rolldown/binding-openharmony-arm64': 1.2.7 + '@rolldown/binding-win32-arm64-msvc': 1.2.7 + '@rolldown/binding-win32-x64-msvc': 1.2.7 + + source-map-js@1.2.1: {} + + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.7) + picomatch: 4.0.7 + + tinypool@2.1.0: {} + + tsx@4.23.13: + dependencies: + esbuild: 0.28.2 + optionalDependencies: + fsevents: 2.3.3 + + typescript@6.0.3: {} + + undici-types@7.24.6: {} + + uuid@14.0.2: {} + + vite-plugin-wasm@3.6.0(vite@8.2.2(@types/node@25.9.5)(esbuild@0.28.2)(tsx@4.23.13)): + dependencies: + vite: 8.2.2(@types/node@25.9.5)(esbuild@0.28.2)(tsx@4.23.13) + + vite@8.2.2(@types/node@25.9.5)(esbuild@0.28.2)(tsx@4.23.13): + dependencies: + lightningcss: 1.33.0 + picomatch: 4.0.7 + postcss: 8.5.28 + rolldown: 1.2.7 + tinyglobby: 0.2.17 + optionalDependencies: + '@types/node': 25.9.5 + esbuild: 0.28.2 + fsevents: 2.3.3 + tsx: 4.23.13 + + ws@8.21.3: {} + + xstate@5.32.6: {} diff --git a/packages/coln-repo/pnpm-workspace.yaml b/packages/coln-repo/pnpm-workspace.yaml new file mode 100644 index 00000000..e29d98df --- /dev/null +++ b/packages/coln-repo/pnpm-workspace.yaml @@ -0,0 +1,8 @@ +allowBuilds: + cbor-extract: false + "@automerge/automerge-repo@https://codeload.github.com/onsetsoftware/automerge-repo/tar.gz/b39d94c5624a3497150e08ba18546f29222289d8#path:packages/automerge-repo": true + esbuild: true +overrides: + "@automerge/automerge": 3.3.2 + "@automerge/automerge-repo": github:onsetsoftware/automerge-repo#test/custom-url-scemes&path:packages/automerge-repo + "@automerge/automerge-subduction": 0.16.1 diff --git a/packages/coln-repo/src/colnDocType.ts b/packages/coln-repo/src/colnDocType.ts new file mode 100644 index 00000000..65b18c2d --- /dev/null +++ b/packages/coln-repo/src/colnDocType.ts @@ -0,0 +1,146 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import { + StoreHandle, + type CommitChunk, + type ColnSchema, + type RealmBindings, + type TransactionHandle, +} from "@coln-project/runtime" +import { + defineDocumentType, + type SedimentreeMeta, +} from "@automerge/automerge-repo/slim" + +export type { ColnSchema, RealmBindings } from "@coln-project/runtime" + +export type ColnState = { + store: StoreHandle + bindings?: RealmBindings +} + +type ColnDocumentBase = Pick< + StoreHandle, + "heads" | "jsonIR" | "rowById" | "scanTable" +> +type ColnTransactionBase = Pick + +export type ColnDocument< + Bindings extends RealmBindings | undefined = undefined, +> = Bindings extends RealmBindings + ? ColnDocumentBase & Pick, "root"> + : ColnDocumentBase + +export type ColnTransaction< + Bindings extends RealmBindings | undefined = undefined, +> = Bindings extends RealmBindings + ? ColnTransactionBase & Pick, "root"> + : ColnTransactionBase + +export type ColnChange = + (transaction: ColnTransaction) => void + +export const colnDocType = defineDocumentType< + ColnState, + ColnDocumentBase, + ColnChange, + ColnSchema, + "coln" +>({ + name: "coln", + urlScheme: "coln", + empty: () => ({ store: StoreHandle.empty() }), + init: (schema) => ({ + store: StoreHandle.fromTheory(serializeSchema(schema)), + }), + view: createDocument, + change: (state, change) => runTransaction(state, change), + heads: (state) => state.store.heads(), + hasData: (state) => state.store.heads().length > 0, + sedimentree: { + metadata: (state) => commitChunks(state).map(commitMetadata), + materialize: (state, metadata) => { + const wantedHeads = new Set(metadata.map((entry) => entry.head)) + // TODO: Add a runtime commitChunksByHash API if full-history scans become costly. + return commitChunks(state) + .filter((chunk) => wantedHeads.has(chunk.hash)) + .map((chunk) => ({ + ...commitMetadata(chunk), + bytes: new Uint8Array(chunk.bytes), + })) + }, + apply: (state, blobs) => { + if (blobs.length > 0) { + // TODO: "Add independent StoreHandle snapshots for Repo document states": + // apply blobs to a snapshot and return a new document. + state.store.applyChunkBytes(blobs) + } + return state + }, + liveHashes: (state) => commitChunks(state).map((chunk) => chunk.hash), + }, +}) + +function createDocument(state: ColnState): ColnDocumentBase { + const { store, bindings } = state + const document: ColnDocumentBase & { root?: unknown } = { + heads: () => store.heads(), + jsonIR: () => store.jsonIR(), + rowById: (path, rowId) => store.rowById(path, rowId), + scanTable: (path) => store.scanTable(path), + } + if (bindings) document.root = new bindings.View(store).root + return Object.freeze(document) +} + +function createTransaction( + state: ColnState, + transaction: TransactionHandle, +): ColnTransactionBase { + const { store, bindings } = state + const exposed: ColnTransactionBase & { root?: unknown } = { + // TODO: Validate values before WASM; invalid argument conversion can prevent + // transaction recovery. Avoid duplicating the runtime schema without a clear design. + add: (path, values) => transaction.add(path, values), + } + if (bindings) exposed.root = new bindings.Transaction(store, transaction).root + return Object.freeze(exposed) +} + +function runTransaction(document: ColnState, change: ColnChange): ColnState { + // TODO: "Add independent StoreHandle snapshots for Repo document states": + // start the transaction from a snapshot and leave this document unchanged. + const { store, bindings } = document + const transaction = store.beginTransaction() + try { + change(createTransaction(document, transaction)) + const commit = transaction.commit() + return { store: commit.takeStore(), bindings } + } catch (error) { + // beginTransaction() moved ownership of the store into the transaction, and on + // error we cannot return a fresh document. Recover the (aborted) store back onto + // the existing state object, otherwise it would hold a dead StoreHandle. + document.store = transaction.takeStore() + throw error + } +} + +function serializeSchema(schema: ColnSchema): string { + const serialized = JSON.stringify(schema) + if (serialized === undefined) + throw new Error("creating a Coln store requires a schema") + return serialized +} + +// TODO: Every call pays to serialize chunk bytes as number[] across the wasm +// boundary, even when only hashes/parents are needed (metadata, liveHashes). +// Switch to Uint8Array output once CommitChunk uses serde_bytes in the runtime. +function commitChunks(document: ColnState): CommitChunk[] { + return document.store.commitChunksAfter([]) +} + +function commitMetadata(chunk: CommitChunk): SedimentreeMeta { + return { kind: "commit", head: chunk.hash, parents: chunk.parents } +} diff --git a/packages/coln-repo/src/colnHandle.ts b/packages/coln-repo/src/colnHandle.ts new file mode 100644 index 00000000..658c6e09 --- /dev/null +++ b/packages/coln-repo/src/colnHandle.ts @@ -0,0 +1,103 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import type { + CrdtDocHandle, + DocumentUrl, + DocumentType, + Repo, +} from "@automerge/automerge-repo" +import type { RealmBindings } from "@coln-project/runtime" +import { + colnDocType, + type ColnChange, + type ColnDocument, + type ColnSchema, + type ColnState, +} from "./colnDocType.js" + +type FoundColnHandle = + Bindings extends RealmBindings ? ColnHandle : ColnHandle + +export type ColnUrl = DocumentUrl<"coln"> + +export type ColnHandle = + CrdtDocHandle< + DocumentType< + ColnState, + ColnDocument, + ColnChange, + ColnSchema, + "coln" + > + > + +export function applyBindings( + handle: ColnHandle | ColnHandle, + bindings: Bindings, +): ColnHandle { + const state = handle.fullDoc() + const existingBindings = state.bindings + + if (existingBindings) { + if (existingBindings !== bindings) { + throw new TypeError("Coln handle already uses different realm bindings") + } + return handle as unknown as ColnHandle + } + + const actualSchema = canonicalJson(JSON.parse(handle.doc().jsonIR())) + const bindingsSchema = canonicalJson(bindings.schema) + if (actualSchema !== bindingsSchema) { + throw new TypeError( + "Realm bindings schema does not match Coln document schema", + ) + } + + state.bindings = bindings + + return handle as unknown as ColnHandle +} + +export function create( + repo: Repo, + bindings: Bindings, +): ColnHandle { + const handle = repo.create(bindings.schema, colnDocType) + + return applyBindings(handle, bindings) +} + +export async function find< + Bindings extends RealmBindings | undefined = undefined, +>( + repo: Repo, + url: ColnUrl, + // a rest tuple (rather than an optional parameter) keeps `undefined` in the + // inferred Bindings, so maybe-undefined arguments produce a union return type + ...rest: [bindings: Bindings] | [] +): Promise> { + const [bindings] = rest + const handle = await repo.find(url, colnDocType) + + return ( + bindings === undefined ? handle : applyBindings(handle, bindings) + ) as FoundColnHandle +} + +function canonicalJson(value: unknown): string | undefined { + return JSON.stringify(value, (_key, current: unknown) => { + if ( + current === null || + Array.isArray(current) || + typeof current !== "object" + ) + return current + return Object.fromEntries( + Object.entries(current).sort(([left], [right]) => + left.localeCompare(right), + ), + ) + }) +} diff --git a/packages/coln-repo/src/index.ts b/packages/coln-repo/src/index.ts new file mode 100644 index 00000000..7c8aafe9 --- /dev/null +++ b/packages/coln-repo/src/index.ts @@ -0,0 +1,20 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +export { + colnDocType, + type ColnChange, + type ColnDocument, + type RealmBindings, + type ColnSchema, + type ColnTransaction, +} from "./colnDocType.js" + +export { + applyBindings, + create, + find, + type ColnHandle, + type ColnUrl, +} from "./colnHandle.js" diff --git a/examples/sync-demo/index.html b/packages/coln-repo/tests/browser/index.html similarity index 59% rename from examples/sync-demo/index.html rename to packages/coln-repo/tests/browser/index.html index 7f7f6654..37bb5c8a 100644 --- a/examples/sync-demo/index.html +++ b/packages/coln-repo/tests/browser/index.html @@ -3,10 +3,9 @@ - Coln automerge-repo demo + Coln Repo Test -
- + diff --git a/packages/coln-repo/tests/browser/main.ts b/packages/coln-repo/tests/browser/main.ts new file mode 100644 index 00000000..da64204a --- /dev/null +++ b/packages/coln-repo/tests/browser/main.ts @@ -0,0 +1,109 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import { Repo, type CrdtDocHandle } from "@automerge/automerge-repo" +import { initSync } from "@automerge/automerge-subduction/slim" +import { wasmBase64 } from "@automerge/automerge-subduction/wasm-base64" +import type { RowView, Value } from "@coln-project/runtime" +import { + colnDocType, + applyBindings, + type ColnHandle, + type ColnSchema, + type ColnUrl, +} from "../../src/index.js" +import * as itemFfi from "../fixtures/itemFfi" + +initSync({ + module: Uint8Array.from(atob(wasmBase64), (char) => char.charCodeAt(0)), +}) + +type RawHandle = CrdtDocHandle +type ItemHandle = ColnHandle + +const repo = new Repo({ + subductionWebsocketEndpoints: ["ws://127.0.0.1:3031"], +}) +let handle: RawHandle | undefined +let typedHandle: ItemHandle | undefined + +const api = { + create(schema: ColnSchema): ColnUrl { + handle = repo.create(schema, colnDocType) + return handle.url + }, + + async find(url: ColnUrl): Promise { + handle = await repo.find(url, colnDocType) + }, + + createTyped(): ColnUrl { + const rawHandle = repo.create(itemFfi.schema, colnDocType) + handle = rawHandle + typedHandle = applyBindings(rawHandle, itemFfi) + return typedHandle.url + }, + + applyTypedBindings(): void { + typedHandle = applyBindings(currentHandle(), itemFfi) + }, + + add(path: string, values: Value[]): void { + currentHandle().change((transaction) => transaction.add(path, values)) + }, + + rows(path: string): RowView[] { + return currentHandle().doc().scanTable(path) + }, + + addTyped(value: string): void { + currentTypedHandle().change((transaction) => { + transaction.root.Items(stringValue(value)).add() + }) + }, + + typedCount(value: string): number { + const rows = currentTypedHandle() + .doc() + .root.Items(stringValue(value)) + .values() + let count = 0 + for (let next = rows.next(); !next.done; next = rows.next()) count += 1 + return count + }, + + heads(): string[] { + return currentHandle().doc().heads() + }, + + async flush(): Promise { + await repo.flush() + }, + + async shutdown(): Promise { + await repo.shutdown() + }, +} + +function currentHandle(): RawHandle { + if (!handle) throw new Error("no Coln store is open") + return handle +} + +function currentTypedHandle(): ItemHandle { + if (!typedHandle) throw new Error("no typed Coln store is open") + return typedHandle +} + +function stringValue(value: string): Value { + return { tag: "string", value } +} + +declare global { + interface Window { + colnTest: typeof api + } +} + +window.colnTest = api diff --git a/packages/coln-repo/tests/browser/subduction.d.ts b/packages/coln-repo/tests/browser/subduction.d.ts new file mode 100644 index 00000000..ee91318e --- /dev/null +++ b/packages/coln-repo/tests/browser/subduction.d.ts @@ -0,0 +1,12 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +declare module "@automerge/automerge-subduction/slim" { + export * from "@automerge/automerge-subduction" + export function initSync(input: { module: Uint8Array }): void +} + +declare module "@automerge/automerge-subduction/wasm-base64" { + export const wasmBase64: string +} diff --git a/packages/coln-repo/tests/fixtures/itemFfi.ts b/packages/coln-repo/tests/fixtures/itemFfi.ts new file mode 100644 index 00000000..20085080 --- /dev/null +++ b/packages/coln-repo/tests/fixtures/itemFfi.ts @@ -0,0 +1,37 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import { + RowIdSet, + type StoreHandle, + type TransactionHandle, + type Value, +} from "@coln-project/runtime" +export { itemSchema as schema } from "./schema" + +interface Root { + Items: (value: Value) => T +} + +export class View { + root: Root + + constructor(store: StoreHandle) { + this.root = { + Items: (value) => new RowIdSet.View(store, "Test.Items", [value]), + } + } +} + +export class Transaction extends View { + declare root: Root + + constructor(store: StoreHandle, transaction: TransactionHandle) { + super(store) + this.root = { + Items: (value) => + new RowIdSet.Transaction(store, "Test.Items", [value], transaction), + } + } +} diff --git a/packages/coln-repo/tests/fixtures/schema.ts b/packages/coln-repo/tests/fixtures/schema.ts new file mode 100644 index 00000000..d8a45f9d --- /dev/null +++ b/packages/coln-repo/tests/fixtures/schema.ts @@ -0,0 +1,24 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import type { ColnSchema } from "../../src/index.js" + +export const itemSchema = { + entities: [ + { + path: [["Test"], ["Items"]], + value: { + entityVariant: { tag: "table" }, + columns: [ + { + path: [["value"]], + type: { tag: "builtin", type: "builtinString" }, + }, + ], + primaryKey: null, + }, + }, + ], + rules: [], +} satisfies ColnSchema diff --git a/packages/coln-repo/tests/generated/Graph.ts b/packages/coln-repo/tests/generated/Graph.ts new file mode 100644 index 00000000..52151b3e --- /dev/null +++ b/packages/coln-repo/tests/generated/Graph.ts @@ -0,0 +1,15 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import * as runtime from "@coln-project/runtime" + +export interface View { + V: runtime.ColnSet.View + E: (x: runtime.Value) => (x: runtime.Value) => runtime.ColnSet.View +} + +export interface Transaction extends View { + V: runtime.ColnSet.Transaction + E: (x: runtime.Value) => (x: runtime.Value) => runtime.ColnSet.Transaction +} diff --git a/packages/coln-repo/tests/generated/GraphRealm.json b/packages/coln-repo/tests/generated/GraphRealm.json new file mode 100644 index 00000000..d7f212c4 --- /dev/null +++ b/packages/coln-repo/tests/generated/GraphRealm.json @@ -0,0 +1,92 @@ +{ + "entities": [ + { + "path": [["GraphRealm"], ["V"]], + "value": { + "entityVariant": { "tag": "table" }, + "columns": [], + "primaryKey": null + } + }, + { + "path": [["GraphRealm"], ["E"]], + "value": { + "entityVariant": { "tag": "table" }, + "columns": [ + { + "path": [["a"]], + "type": { "tag": "rowId", "path": [["GraphRealm"], ["V"]] } + }, + { + "path": [["b"]], + "type": { "tag": "rowId", "path": [["GraphRealm"], ["V"]] } + } + ], + "primaryKey": null + } + } + ], + "rules": [ + { + "path": [["GraphRealm"], ["V"], ["foreignKey"]], + "value": { + "ruleVariant": "enforced", + "varNames": [], + "varTypes": [], + "antecedents": [ + { + "tag": "atom", + "atom": { + "entity": [["GraphRealm"], ["V"]], + "rowId": null, + "values": [] + } + } + ], + "consequents": [] + } + }, + { + "path": [["GraphRealm"], ["E"], ["foreignKey"]], + "value": { + "ruleVariant": "enforced", + "varNames": [[["a"]], [["b"]]], + "varTypes": [ + { "tag": "rowId", "path": [["GraphRealm"], ["V"]] }, + { "tag": "rowId", "path": [["GraphRealm"], ["V"]] } + ], + "antecedents": [ + { + "tag": "atom", + "atom": { + "entity": [["GraphRealm"], ["E"]], + "rowId": null, + "values": [ + { "column": 0, "term": { "tag": "var", "index": 0 } }, + { "column": 1, "term": { "tag": "var", "index": 1 } } + ] + } + } + ], + "consequents": [ + { + "tag": "atom", + "atom": { + "entity": [["GraphRealm"], ["V"]], + "rowId": { "tag": "var", "index": 0 }, + "values": [] + } + }, + { + "tag": "atom", + "atom": { + "entity": [["GraphRealm"], ["V"]], + "rowId": { "tag": "var", "index": 1 }, + "values": [] + } + } + ] + } + } + ] +} diff --git a/packages/coln-repo/tests/generated/GraphRealm.pretty b/packages/coln-repo/tests/generated/GraphRealm.pretty new file mode 100644 index 00000000..b4a7d502 --- /dev/null +++ b/packages/coln-repo/tests/generated/GraphRealm.pretty @@ -0,0 +1,13 @@ +flatrealm + entities + table ℜ.V := [] + table ℜ.E := [.a : ℜ.V, .b : ℜ.V] + end + rules + enforced ℜ.V.foreignKey := ℜ.V [] ⊢ ⊤ + enforced ℜ.E.foreignKey a b := ℜ.E [ + .a ↦ a, + .b ↦ b + ] ⊢ a ∈ ℜ.V [] ∧ b ∈ ℜ.V [] + end +end \ No newline at end of file diff --git a/packages/coln-repo/tests/generated/GraphRealm.ts b/packages/coln-repo/tests/generated/GraphRealm.ts new file mode 100644 index 00000000..b0b861a4 --- /dev/null +++ b/packages/coln-repo/tests/generated/GraphRealm.ts @@ -0,0 +1,52 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import schema from "./GraphRealm.json" with { type: "json" } +export { schema } +import * as runtime from "@coln-project/runtime" +import * as Graph from "./Graph" + +export class View { + root: Graph.View + + constructor(store: runtime.StoreHandle) { + this.root = { + V: new runtime.RowIdSet.View(store, "GraphRealm.V", []), + E: (a: runtime.Value) => { + return (b: runtime.Value) => { + return new runtime.RowIdSet.View(store, "GraphRealm.E", [a, b]) + } + }, + } + } +} + +export class Transaction extends View { + root: Graph.Transaction + + constructor( + store: runtime.StoreHandle, + transaction: runtime.TransactionHandle, + ) { + super(store) + this.root = { + V: new runtime.RowIdSet.Transaction( + store, + "GraphRealm.V", + [], + transaction, + ), + E: (a: runtime.Value) => { + return (b: runtime.Value) => { + return new runtime.RowIdSet.Transaction( + store, + "GraphRealm.E", + [a, b], + transaction, + ) + } + }, + } + } +} diff --git a/packages/coln-repo/tests/handle.test.ts b/packages/coln-repo/tests/handle.test.ts new file mode 100644 index 00000000..53e7fa32 --- /dev/null +++ b/packages/coln-repo/tests/handle.test.ts @@ -0,0 +1,202 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import assert from "node:assert/strict" +import { afterEach, beforeEach, test } from "node:test" +import { + isValidAutomergeUrl, + isValidDocumentUrl, +} from "@automerge/automerge-repo" +import type { Value } from "@coln-project/runtime" +import { + applyBindings, + colnDocType, + create, + find, + type ColnDocument, +} from "../src/index" +import * as itemFfi from "./fixtures/itemFfi" +import schema from "./generated/GraphRealm.json" with { type: "json" } +import * as GraphRealm from "./generated/GraphRealm" +import { + createTestRepoPair, + waitForChange, + type TestRepoPair, +} from "./helpers/TestRepoPair" + +let repos: TestRepoPair + +beforeEach(async () => { + repos = await createTestRepoPair() +}) + +afterEach(async () => { + await repos.shutdown() +}) + +test("create returns a bound, initialized handle", () => { + const handle = create(repos.source, GraphRealm) + const document = handle.doc() + + assert.match(handle.url, /^coln:/) + assert(isValidDocumentUrl(handle.url, "coln")) + assert.equal(isValidAutomergeUrl(handle.url), false) + assert.deepEqual(Object.keys(document).sort(), [ + "heads", + "jsonIR", + "root", + "rowById", + "scanTable", + ]) + assert.deepEqual(JSON.parse(document.jsonIR()), schema) + assert.deepEqual(document.heads(), handle.rawHeads()) + assert.deepEqual(document.scanTable("GraphRealm.V"), []) +}) + +test("document can be updated and changes are reflected", () => { + const handle = create(repos.source, GraphRealm) + + let v1!: Value, v2!: Value, e1!: Value + + handle.change((tx) => { + v1 = tx.root.V.add() + v2 = tx.root.V.add() + e1 = tx.root.E(v1)(v2).add() + }) + + const doc = handle.doc() + + assert(doc.root.V.has(v1)) + assert(doc.root.V.has(v2)) + assert(doc.root.E(v1)(v2).has(e1)) + assert.equal(v1.tag, "row_id") + assert.deepEqual(doc.rowById("GraphRealm.V", v1.value)?.rowId, v1) +}) + +test("changes expose only safe Coln operations", () => { + const handle = create(repos.source, GraphRealm) + + handle.change((transaction) => { + assert.deepEqual(Object.keys(transaction).sort(), ["add", "root"]) + transaction.add("GraphRealm.V", []) + }) + + assert.equal(handle.doc().scanTable("GraphRealm.V").length, 1) +}) + +test("failed changes are aborted and leave the handle usable", () => { + const handle = create(repos.source, GraphRealm) + let committed!: Value + + assert.throws( + () => + handle.change((transaction) => { + transaction.root.V.add() + throw new Error("abort") + }), + new Error("abort"), + ) + handle.change((transaction) => { + committed = transaction.root.V.add() + }) + + assert.deepEqual( + handle + .doc() + .scanTable("GraphRealm.V") + .map((row) => row.rowId), + [committed], + ) +}) + +test("typed handles sync changes", async () => { + const handle = create(repos.source, GraphRealm) + const replica = await find(repos.replica, handle.url, GraphRealm) + let vertex!: Value + + const changed = new Promise>((resolve) => { + replica.once("change", (payload) => { + if (payload.doc) resolve(payload.doc) + }) + }) + handle.change((tx) => { + vertex = tx.root.V.add() + }) + const changedDocument = await changed + + assert(changedDocument.root.V.has(vertex)) + assert.deepEqual( + changedDocument.scanTable("GraphRealm.V").map((row) => row.rowId), + [vertex], + ) + assert(replica.doc().root.V.has(vertex)) +}) + +test("raw changes sync into a typed handle", async () => { + const handle = create(repos.source, GraphRealm) + const replica = await repos.replica.find(handle.url, colnDocType) + let vertex!: Value + + const changed = waitForChange(handle) + replica.change((transaction) => { + vertex = transaction.add("GraphRealm.V", []) + }) + await changed + + assert(handle.doc().root.V.has(vertex)) +}) + +test("find returns a raw handle when bindings are omitted", async () => { + const handle = create(repos.source, GraphRealm) + const replica = await find(repos.replica, handle.url) + + assert.deepEqual(Object.keys(replica.doc()).sort(), [ + "heads", + "jsonIR", + "rowById", + "scanTable", + ]) +}) + +test("bindings can be applied to a raw found handle later", async () => { + const handle = create(repos.source, GraphRealm) + const rawReplica = await find(repos.replica, handle.url) + const replica = applyBindings(rawReplica, GraphRealm) + + assert.strictEqual(replica, rawReplica) + assert.strictEqual(applyBindings(replica, GraphRealm), replica) + + let vertex!: Value + replica.change((transaction) => { + vertex = transaction.root.V.add() + }) + + assert(replica.doc().root.V.has(vertex)) + assert.deepEqual( + replica + .doc() + .scanTable("GraphRealm.V") + .map((row) => row.rowId), + [vertex], + ) +}) + +test("applying different bindings to a bound handle is rejected", () => { + const handle = create(repos.source, GraphRealm) + const otherBindings = { ...GraphRealm } + + assert.throws( + () => applyBindings(handle, otherBindings), + new TypeError("Coln handle already uses different realm bindings"), + ) +}) + +test("applying bindings for a different schema is rejected", () => { + const handle = repos.source.create(schema, colnDocType) + + assert.throws( + () => applyBindings(handle, itemFfi), + new TypeError("Realm bindings schema does not match Coln document schema"), + ) +}) diff --git a/packages/coln-repo/tests/helpers/TestNetworkAdapter.ts b/packages/coln-repo/tests/helpers/TestNetworkAdapter.ts new file mode 100644 index 00000000..e25621d3 --- /dev/null +++ b/packages/coln-repo/tests/helpers/TestNetworkAdapter.ts @@ -0,0 +1,50 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import { + NetworkAdapter, + type Message, + type PeerId, +} from "@automerge/automerge-repo" + +export class TestNetworkAdapter extends NetworkAdapter { + #connected = false + #peer?: TestNetworkAdapter + + static createConnectedPair(): [TestNetworkAdapter, TestNetworkAdapter] { + const first = new TestNetworkAdapter() + const second = new TestNetworkAdapter() + first.#peer = second + second.#peer = first + return [first, second] + } + + isReady(): boolean { + return true + } + + async whenReady(): Promise {} + + connect(peerId: PeerId): void { + this.peerId = peerId + this.#connected = true + } + + disconnect(): void { + this.#connected = false + } + + announce(peerId: PeerId): void { + this.emit("peer-candidate", { peerId, peerMetadata: {} }) + } + + send(message: Message): void { + if (!this.#connected) return + queueMicrotask(() => this.#peer?.receive(message)) + } + + private receive(message: Message): void { + if (this.#connected) this.emit("message", message) + } +} diff --git a/packages/coln-repo/tests/helpers/TestRepoPair.ts b/packages/coln-repo/tests/helpers/TestRepoPair.ts new file mode 100644 index 00000000..324330c9 --- /dev/null +++ b/packages/coln-repo/tests/helpers/TestRepoPair.ts @@ -0,0 +1,83 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import { initSubduction, Repo } from "@automerge/automerge-repo" +import { setTimeout as delay } from "node:timers/promises" +import { TestNetworkAdapter } from "./TestNetworkAdapter" + +export interface TestRepoPair { + source: Repo + replica: Repo + shutdown(): Promise +} + +export async function createTestRepoPair(): Promise { + await initSubduction() + const [sourceAdapter, replicaAdapter] = + TestNetworkAdapter.createConnectedPair() + const source = new Repo({ + subductionAdapters: [ + { adapter: sourceAdapter, serviceName: "test", role: "connect" }, + ], + subductionTimeouts: { healMaxAttempts: 0, syncMs: 1_000 }, + }) + const replica = new Repo({ + subductionAdapters: [ + { adapter: replicaAdapter, serviceName: "test", role: "accept" }, + ], + subductionTimeouts: { healMaxAttempts: 0, syncMs: 1_000 }, + }) + + const sourceConnected = waitForConnection(source) + const replicaConnected = waitForConnection(replica) + sourceAdapter.announce(replica.peerId) + replicaAdapter.announce(source.peerId) + await Promise.all([sourceConnected, replicaConnected]) + + return { + source, + replica, + async shutdown() { + await Promise.all([source.flush(), replica.flush()]) + await Promise.all([ + source.subduction.then((subduction) => subduction.disconnectAll()), + replica.subduction.then((subduction) => subduction.disconnectAll()), + ]) + await Promise.all([source.shutdown(), replica.shutdown()]) + }, + } +} + +export function waitForChange(handle: { + once(event: "change", listener: () => void): unknown +}): Promise { + return withTimeout( + new Promise((resolve) => handle.once("change", () => resolve())), + "handle change", + ) +} + +function waitForConnection(repo: Repo): Promise { + if (repo.isSubductionConnected()) return Promise.resolve() + return withTimeout( + new Promise((resolve) => { + const onConnection = ({ connected }: { connected: boolean }) => { + if (!connected) return + repo.off("subduction-connection", onConnection) + resolve() + } + repo.on("subduction-connection", onConnection) + }), + "Subduction connection", + ) +} + +function withTimeout(promise: Promise, operation: string): Promise { + return Promise.race([ + promise, + delay(5_000, undefined, { ref: false }).then(() => { + throw new Error(`${operation} timed out`) + }), + ]) +} diff --git a/packages/coln-repo/tests/package/consumer.ts b/packages/coln-repo/tests/package/consumer.ts new file mode 100644 index 00000000..6c9144a2 --- /dev/null +++ b/packages/coln-repo/tests/package/consumer.ts @@ -0,0 +1,94 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import type { DocumentUrl, Repo } from "@automerge/automerge-repo" +import type { Value } from "@coln-project/runtime" +import { + colnDocType, + find, + type ColnChange, + type ColnDocument, + type ColnHandle, + type ColnTransaction, + type ColnUrl, + type RealmBindings, +} from "@coln-project/repo" + +type Equal = + (() => Type extends Left ? 1 : 2) extends () => Type extends Right + ? 1 + : 2 + ? true + : false +type Expect = Type + +declare const repo: Repo +declare const url: ColnUrl +declare const bindings: RealmBindings +declare const optionalBindings: RealmBindings | undefined + +const raw = find(repo, url) +const explicitUndefined = find(repo, url, undefined) +const typed = find(repo, url, bindings) +const optional = find(repo, url, optionalBindings) + +void raw.then((handle) => { + handle.doc().scanTable("Example.Items") + handle.fullDoc().store.heads() +}) + +void typed.then((handle) => { + handle.doc().root + handle.on("change", (payload) => { + payload.doc?.root + payload.handle.doc().root + }) + handle.addListener("change", (payload) => payload.doc?.root) + handle.listeners("change").forEach((listener) => listener) + handle.on("heads-changed", (payload) => { + payload.doc.store + payload.handle.doc().root + }) + handle.change((transaction) => { + const value: Value = transaction.add("Example.Items", []) + value + transaction.root + }) +}) + +type DocumentOperation = "heads" | "jsonIR" | "rowById" | "scanTable" + +export type TypeChecks = [ + Expect>>, + Expect>>, + Expect>>, + Expect>>>, + Expect< + Equal>> + >, + Expect< + Equal< + ReturnType["doc"]>, + ColnDocument + > + >, + Expect< + Equal< + Parameters["change"]>[0], + ColnChange + > + >, + Expect< + Equal< + Parameters>[0], + ColnTransaction + > + >, + Expect>, + Expect< + Equal, DocumentOperation | "root"> + >, + Expect>, + Expect, "add" | "root">>, +] diff --git a/packages/coln-repo/tests/server.mjs b/packages/coln-repo/tests/server.mjs new file mode 100644 index 00000000..a8cef1f9 --- /dev/null +++ b/packages/coln-repo/tests/server.mjs @@ -0,0 +1,119 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import { WebSocketServer } from "ws" +import { + MemorySigner, + MemoryStorage, + Subduction, +} from "@automerge/automerge-subduction" + +class WebSocketTransport { + #socket + #queue = [] + #waiters = [] + #errorWaiters = [] + #closed = false + #closedPromise + #resolveClosed + #disconnectCallback = null + + constructor(socket) { + this.#socket = socket + this.#closedPromise = new Promise((resolve) => { + this.#resolveClosed = resolve + }) + + socket.on("message", (data) => { + const bytes = toUint8Array(data) + const waiter = this.#waiters.shift() + if (waiter) { + this.#errorWaiters.shift() + waiter(bytes) + } else { + this.#queue.push(bytes) + } + }) + socket.on("close", () => this.#markClosed(new Error("WebSocket closed"))) + socket.on("error", (error) => this.#markClosed(error)) + } + + onDisconnect(callback) { + this.#disconnectCallback = callback + } + + async sendBytes(bytes) { + if (this.#closed) throw new Error("WebSocket closed") + await new Promise((resolve, reject) => { + this.#socket.send(Buffer.from(bytes), (error) => + error ? reject(error) : resolve(), + ) + }) + } + + recvBytes() { + const queued = this.#queue.shift() + if (queued) return Promise.resolve(queued) + if (this.#closed) return Promise.reject(new Error("WebSocket closed")) + return new Promise((resolve, reject) => { + this.#waiters.push(resolve) + this.#errorWaiters.push(reject) + }) + } + + async disconnect() { + this.#markClosed(new Error("WebSocket closed"), false) + this.#socket.close() + } + + closed() { + return this.#closedPromise + } + + #markClosed(error, notify = true) { + if (this.#closed) return + this.#closed = true + this.#resolveClosed() + for (const reject of this.#errorWaiters) reject(error) + this.#waiters = [] + this.#errorWaiters = [] + if (notify) this.#disconnectCallback?.() + } +} + +function toUint8Array(data) { + if (data instanceof Uint8Array) { + return new Uint8Array(data.buffer, data.byteOffset, data.byteLength) + } + if (data instanceof ArrayBuffer) return new Uint8Array(data) + if (Array.isArray(data)) return toUint8Array(Buffer.concat(data)) + return new Uint8Array(Buffer.from(data)) +} + +const port = 3031 +const serviceName = `127.0.0.1:${port}` +const subduction = new Subduction({ + signer: new MemorySigner(), + storage: new MemoryStorage(), +}) +const server = new WebSocketServer({ port }) + +server.on("connection", (socket) => { + subduction + .acceptTransport(new WebSocketTransport(socket), serviceName) + .catch((error) => { + if (!String(error?.message ?? error).includes("closed")) + console.error(error) + }) +}) + +async function shutdown() { + await subduction.disconnectAll().catch(() => {}) + server.close(() => process.exit(0)) +} + +process.on("SIGINT", shutdown) +process.on("SIGTERM", shutdown) + +console.log(`Subduction test relay listening on ws://127.0.0.1:${port}`) diff --git a/packages/coln-repo/tests/sync.spec.ts b/packages/coln-repo/tests/sync.spec.ts new file mode 100644 index 00000000..d2e84902 --- /dev/null +++ b/packages/coln-repo/tests/sync.spec.ts @@ -0,0 +1,202 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import { expect, test, type Page } from "@playwright/test" +import { + initSubduction, + Repo, + type CrdtDocHandle, +} from "@automerge/automerge-repo" +import { colnDocType, type ColnUrl } from "../src/index.js" +import { itemSchema } from "./fixtures/schema" + +const table = "Test.Items" +type RawHandle = CrdtDocHandle + +test("browser and Node repos create, find, and update each other's stores", async ({ + browser, +}) => { + await initSubduction() + const context = await browser.newContext() + const pages: Page[] = [] + let nodeRepo: Repo | undefined + + try { + nodeRepo = new Repo({ + subductionWebsocketEndpoints: ["ws://127.0.0.1:3031"], + }) + const browserCreator = await openPage(context.newPage()) + pages.push(browserCreator) + const browserUrl = await browserCreator.evaluate( + (schema) => window.colnTest.create(schema), + itemSchema, + ) + await add(browserCreator, "browser-one") + await browserCreator.evaluate(() => window.colnTest.flush()) + + const nodeFinder = await nodeRepo.find(browserUrl, colnDocType) + await expectNodeRows(nodeFinder, ["browser-one"]) + + addInNode(nodeFinder, "node-two") + await nodeRepo.flush() + await expectRows(browserCreator, ["browser-one", "node-two"]) + await expectSameHeads(browserCreator, nodeFinder) + + const nodeCreator = nodeRepo.create(itemSchema, colnDocType) + addInNode(nodeCreator, "node-one") + await nodeRepo.flush() + + const browserFinder = await openPage(context.newPage()) + pages.push(browserFinder) + await browserFinder.evaluate( + (url) => window.colnTest.find(url), + nodeCreator.url as ColnUrl, + ) + await expectRows(browserFinder, ["node-one"]) + + await add(browserFinder, "browser-two") + await browserFinder.evaluate(() => window.colnTest.flush()) + await expectNodeRows(nodeCreator, ["browser-two", "node-one"]) + await expectSameHeads(browserFinder, nodeCreator) + } finally { + try { + await shutdownPages(pages) + } finally { + try { + await nodeRepo?.shutdown() + } finally { + await context.close() + } + } + } +}) + +test("reloads without schema and applies typed bindings", async ({ + browser, +}) => { + const context = await browser.newContext() + const pages: Page[] = [] + + try { + const creator = await openPage(context.newPage()) + pages.push(creator) + const url = await creator.evaluate(() => window.colnTest.createTyped()) + + await creator.evaluate(() => window.colnTest.addTyped("one")) + await creator.evaluate(() => window.colnTest.flush()) + + const finder = await openPage(context.newPage()) + pages.push(finder) + await finder.evaluate((url) => window.colnTest.find(url), url) + await expectRows(finder, ["one"]) + await finder.evaluate(() => window.colnTest.applyTypedBindings()) + await expectTypedCount(finder, "one", 1) + + await finder.evaluate(() => window.colnTest.addTyped("two")) + await finder.evaluate(() => window.colnTest.flush()) + await expectTypedCount(creator, "two", 1) + + await shutdownPages([creator, finder]) + await creator.close() + await finder.close() + + const reloaded = await openPage(context.newPage()) + pages.push(reloaded) + await reloaded.evaluate((url) => window.colnTest.find(url), url) + await expectRows(reloaded, ["one", "two"]) + await reloaded.evaluate(() => window.colnTest.applyTypedBindings()) + await expectTypedCount(reloaded, "one", 1) + await expectTypedCount(reloaded, "two", 1) + } finally { + try { + await shutdownPages(pages) + } finally { + await context.close() + } + } +}) + +async function openPage(pagePromise: Promise): Promise { + const page = await pagePromise + await page.goto("/") + await page.waitForFunction(() => window.colnTest !== undefined) + return page +} + +async function shutdownPages(pages: Page[]): Promise { + await Promise.all( + pages + .filter((page) => !page.isClosed()) + .map((page) => + page.evaluate(() => window.colnTest.shutdown()).catch(() => undefined), + ), + ) +} + +async function add(page: Page, value: string) { + await page.evaluate( + ({ table, value }) => + window.colnTest.add(table, [{ tag: "string", value }]), + { table, value }, + ) +} + +async function expectRows(page: Page, values: string[]) { + await expect + .poll(() => + page.evaluate( + (table) => + window.colnTest + .rows(table) + .map((row) => row.values[0]) + .filter((value) => value?.tag === "string") + .map((value) => value.value) + .sort(), + table, + ), + ) + .toEqual([...values].sort()) +} + +function addInNode(handle: RawHandle, value: string) { + handle.change((transaction) => + transaction.add(table, [{ tag: "string", value }]), + ) +} + +async function expectNodeRows(handle: RawHandle, values: string[]) { + await expect + .poll(() => + handle + .doc() + .scanTable(table) + .map((row) => row.values[0]) + .filter((value) => value?.tag === "string") + .map((value) => value.value) + .sort(), + ) + .toEqual([...values].sort()) +} + +async function expectSameHeads(page: Page, handle: RawHandle) { + await expect + .poll(async () => { + const browserHeads = ( + await page.evaluate(() => window.colnTest.heads()) + ).sort() + const nodeHeads = handle.doc().heads().sort() + return browserHeads.join(",") === nodeHeads.join(",") + ? "equal" + : `browser: ${browserHeads.join(",")}; Node: ${nodeHeads.join(",")}` + }) + .toBe("equal") +} + +async function expectTypedCount(page: Page, value: string, count: number) { + await expect + .poll(() => + page.evaluate((value) => window.colnTest.typedCount(value), value), + ) + .toBe(count) +} diff --git a/packages/coln-repo/tsconfig.build.json b/packages/coln-repo/tsconfig.build.json new file mode 100644 index 00000000..274a9026 --- /dev/null +++ b/packages/coln-repo/tsconfig.build.json @@ -0,0 +1,12 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "noEmit": false, + "declaration": true, + "outDir": "dist", + "rootDir": "src", + "types": ["node"] + }, + "include": ["src/**/*.ts"], + "exclude": ["tests"] +} diff --git a/packages/coln-repo/tsconfig.json b/packages/coln-repo/tsconfig.json new file mode 100644 index 00000000..6d028484 --- /dev/null +++ b/packages/coln-repo/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "Bundler", + "lib": ["DOM", "ES2022"], + "strict": true, + "noEmit": true, + "types": ["node", "vite/client"] + }, + "include": ["src/**/*.ts", "tests/**/*.ts", "*.config.ts"], + "exclude": ["tests/package"] +} diff --git a/packages/coln-repo/tsconfig.package.json b/packages/coln-repo/tsconfig.package.json new file mode 100644 index 00000000..d8dc1cd9 --- /dev/null +++ b/packages/coln-repo/tsconfig.package.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "types": [] + }, + "include": ["tests/package/**/*.ts"], + "exclude": [] +} diff --git a/examples/sync-demo/vite.config.ts b/packages/coln-repo/vite.config.ts similarity index 68% rename from examples/sync-demo/vite.config.ts rename to packages/coln-repo/vite.config.ts index d704cedc..06488873 100644 --- a/examples/sync-demo/vite.config.ts +++ b/packages/coln-repo/vite.config.ts @@ -3,11 +3,10 @@ // SPDX-License-Identifier: Apache-2.0 OR MIT import { defineConfig } from "vite" -import react from "@vitejs/plugin-react" import wasm from "vite-plugin-wasm" export default defineConfig({ - base: process.env.VITE_BASE || "/", - plugins: [wasm(), react()], + root: "tests/browser", + plugins: [wasm()], build: { target: "esnext" }, })