diff --git a/.gitignore b/.gitignore index 63e61a1..156f063 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,9 @@ playwright-report/ test-results/ .wrangler/ .bithub/ +vrt/test-results/ +vrt/baselines/ +vrt/snapshots/ +vrt/output/ +vrt/vrt-report.json +vrt/node_modules/ diff --git a/README.md b/README.md index fa412d4..9364bbd 100644 --- a/README.md +++ b/README.md @@ -92,3 +92,25 @@ bit core /Users/mz/ghq/github.com/bit-vcs/bit ## Cloudflare Entrypoint `src/cmd/main/main.mbt` exports `fetch(request, env, exec_ctx)` and delegates to `@mars.Server::to_handler_with_env`. Page rendering lives in `src/adapters/mars_http/server.mbt`, using `@sol.render_page` + `mizchi/luna/x/components` for SSR. + +### Cloudflare Artifacts browse adapter + +`worker-entry.js` binds the test namespace `bit-vcs-playground` as `ARTIFACTS`. The read-only +adapter keeps Git credentials in the Worker and exposes raw Artifacts metadata instead. The binding +uses `remote = true`, so `wrangler dev` accesses the real test namespace: + +- `GET /api/artifacts/repos` +- `GET /api/artifacts/repos//log?ref=main&limit=20&offset=0` +- `GET /api/artifacts/repos//commits/` +- `GET /api/artifacts/repos//trees/` + +These endpoints remain disabled until a dedicated secret is configured. This prevents the default +public read UI from accidentally exposing a private Artifact repository: + +```bash +pnpm wrangler secret put BITHUB_ARTIFACTS_READ_TOKEN +``` + +Call them with `Authorization: Bearer $BITHUB_ARTIFACTS_READ_TOKEN`. The adapter only calls +`list`, `log`, `readCommit`, and `readTree`; it never creates Git tokens, writes repository data, or +replaces bithub's R2-backed issue/PR/CI state. diff --git a/TODO.md b/TODO.md index 9f35a88..17f9a5f 100644 --- a/TODO.md +++ b/TODO.md @@ -61,6 +61,24 @@ Last updated: 2026-03-17 - [ ] Add `moon_ide` tool wrapping `moon ide peek-def` and `moon ide outline` - More accurate than grep for MoonBit symbol lookup +### VRT + Semantic Verification + +- [ ] Multi-step goal runner: 大きなゴールを分解して複数コミットで達成するシナリオテスト + - Goal → sub-tasks に分解 → 各 sub-task で expectation 生成 → 逐次実行 → ゴール達成を判定 + - 例: "ダークモード対応" → テーマ変数追加 → コンポーネント適用 → VRT 全ページ verify + - ランナー設計: `GoalRunner { goal, steps: Step[], currentStep, accumScore }` + - 各 step: expectation.json 自動生成 → subagent 実行 → verify → score → 次の step へ + - 失敗時: 修正ループ (max 3 retry) or ゴール縮小 + - 最終スコア: step 成功率 × 品質スコア × (1 / トークン消費) +- [ ] Playwright テストヘルパ: テスト失敗時のみ AI アサーション発火 + - `nlAssert(page, "ナビに5つ以上リンクがある", { onlyOnFailure: true, dependsOn: ["src/Header.tsx"] })` + - 失敗時: Vision LLM でスクリーンショット分析 → 修正ヒント → エージェントに投げる + - dep graph 連携で影響のないアサーションをスキップ +- [ ] introspect CLI コマンド: `vrt introspect` → spec.json 自動生成 +- [ ] spec verify CLI コマンド: `vrt spec-verify` → long-cycle 不変条件の検証 +- [ ] form/region ランドマークの spec invariant 自動生成 (現状 banner/main/nav のみ) +- [ ] role-changed の spec 検出 (現状は a11y diff でのみ検出) + ### Remaining features - [ ] Enable Container orchestration in wrangler.toml diff --git a/docs/vrt-research.md b/docs/vrt-research.md new file mode 100644 index 0000000..b2042da --- /dev/null +++ b/docs/vrt-research.md @@ -0,0 +1,128 @@ +# VRT (Visual Regression Testing) + AI 調査メモ + +## 動機 + +VRT で検出した差分を AI でリーズニングし、自己修復を行うアーキテクチャを検討する。 +ただしコストが高いため、以下で最適化したい: + +1. **依存ツリー解析** — 変更の影響範囲を絞り込み、スナップショット対象を最小化 +2. **意図とのすり合わせ** — commit/PR の変更意図と視覚差分を照合し、期待通りの変更を自動承認 + +--- + +## 1. 既存ツール・プラグインの状況 + +### AI-Powered VRT (商用) + +| ツール | アプローチ | 特徴 | +|--------|-----------|------| +| **Applitools Eyes** | 独自 Visual AI (CV ベース) | UI をセマンティックに理解。レンダリングノイズを無視。最も成熟 | +| **Percy Visual Review Agent** (BrowserStack) | スマートハイライト + 説明文生成 | 「ヘッダーが4px左にずれた」等の自然言語差分。偽陽性40%削減。人間承認は必要 | +| **Meticulous AI** | セッションリプレイ | ユーザ操作を記録→再生。静的スクリーンショットではなくフロー単位 | +| **TestMu SmartUI** | ピクセル + ヒューリスティクス | ルールベースの Smart Ignore (フォントレンダリング等) | + +### 依存ツリー対応 VRT + +| ツール | アプローチ | 特徴 | +|--------|-----------|------| +| **Chromatic TurboSnap** | Webpack/Vite Stats → モジュール依存グラフ | 変更ファイル → 影響 Story をマッピング。60-90% のスナップショット削減。**唯一の実装** | + +### OSS VRT ツール (ピクセルベース) + +- **Playwright** built-in VRT (pixelmatch) +- **Lost Pixel** — Storybook/Ladle 対応 +- **BackstopJS** — ビューポート/シナリオ設定 +- **reg-suit / reg-cli** — CI 統合・レポート生成 +- **Visual-Regression-Tracker** — セルフホスト。実験的 VLM サポートあり + +### ギャップ (未開拓領域) + +1. **LLM ベース VRT リーズニング**: スクリーンショット差分を Vision LLM に送り判定する OSS ツールは**存在しない** +2. **依存ツリー対応 VRT**: Chromatic TurboSnap の独占。Vite 単独や任意フレームワークで使える OSS 代替なし +3. **Intent-aware VRT**: コミットメッセージ/PR 記述から変更意図を抽出し、視覚差分と照合する仕組みは**完全に未開拓** + +--- + +## 2. 関連論文 + +### VRT 方法論・最適化 + +- Moradi et al. (2024) — **"AI for Context-Aware Visual Change Detection"** [arXiv:2405.00874](https://arxiv.org/abs/2405.00874) + - YOLOv5 で UI コントロールを検出 → グラフ構築 → 空間的文脈から意味のある変更を判定。ピクセル/リージョン比較より優秀 +- Web Application Testing Survey (2024) [arXiv:2412.10476](https://arxiv.org/abs/2412.10476) + - 2014-2023 の Web テスト研究サーベイ + +### 依存対応テスト選択 + +- **DIRTS** (ICST 2023) — DI フレームワーク対応の Regression Test Selection +- **Lam et al.** (ISSTA 2020) — 順序依存テストの優先付け・選択・並列化。依存対応アルゴリズムで失敗 80% 削減 +- **CORTS/C2RTS** (2025, J. Systems Architecture) — コンポーネントベース RTS。モジュールレベル依存グラフ + +### Self-Healing テスト自動化 + +- Chede & Tijare (2025, IJRASET) — **"AI-Driven Self-Healing UI Testing with Visual Proof"** + - DOM ベースのヒーリングとセマンティック視覚検証の統合 +- Self-Healing Test Automation with AI/ML (2024) — RL + 画像認識 + 動的ロケータ +- **SHML** (NeurIPS 2024) [arXiv:2411.00186](https://arxiv.org/abs/2411.00186) — 自律的診断・修復フレームワーク + +### LLM/Vision モデル × テスト + +- **Wang et al.** (TSE 2024) — LLM × ソフトウェアテスト 102 研究のサーベイ [arXiv:2307.07221](https://arxiv.org/abs/2307.07221) +- **VisionDroid** (2024) [arXiv:2407.03037](https://arxiv.org/abs/2407.03037) — マルチモーダル LLM で GUI 探索 + 非クラッシュバグ検出 +- **Make LLM a Testing Expert** (ICSE 2024) — 機能認識に基づくモバイル GUI テスト +- **RepairAgent** (ICSE 2025) — LLM ベースの自律プログラム修復エージェント +- Yu et al. (ACM Computing Surveys, 2025) — Vision-Based Mobile GUI Testing サーベイ [arXiv:2310.13518](https://arxiv.org/abs/2310.13518) + +### 参考リポジトリ + +- [LLM4SoftwareTesting](https://github.com/LLM-Testing/LLM4SoftwareTesting) — LLM × テスト論文キュレーション +- [GUI-Agents-Paper-List](https://github.com/OSU-NLP-Group/GUI-Agents-Paper-List) — GUI エージェント論文一覧 + +--- + +## 3. 考察: 本プロジェクトへの適用可能性 + +### 構想: 依存ツリー + Intent-aware + AI リーズニングの統合 + +``` +Code Change + │ + ├─ 1. 依存ツリー解析 (低コスト) + │ Vite モジュールグラフ / コンポーネント import 解析 + │ → 影響を受けるコンポーネント/ページを特定 + │ → VRT スナップショット対象を最小化 (TurboSnap 相当) + │ + ├─ 2. Intent 抽出 (低〜中コスト) + │ commit message / PR description を LLM でパース + │ → 「ボタンの色を青→緑に変更」等の期待変更を構造化 + │ + ├─ 3. VRT 実行 (中コスト) + │ 最小化されたスナップショット対象のみ撮影・比較 + │ + └─ 4. AI リーズニング (高コスト、条件付き) + Intent と一致する差分 → 自動承認 + Intent と不一致 or 予期しない差分 → Vision LLM で分析 + → 修復提案 or 人間レビューにエスカレート +``` + +### コスト最適化のポイント + +1. **段階的フィルタリング**: 依存ツリー → Intent 照合 → AI 判定 の順で、安価なフィルタから先に適用 +2. **AI 呼び出しの最小化**: 全差分を LLM に送るのではなく、自動承認できないものだけを AI に渡す +3. **キャッシュ**: 同一コンポーネントの類似差分パターンをキャッシュし、再判定を回避 + +### 技術的課題 + +- Vite モジュールグラフの取得方法 (TurboSnap は Webpack Stats API 依存) +- Intent 抽出の精度 (自然言語→構造化された期待変更の変換) +- Vision LLM のコスト vs 精度のトレードオフ +- self-healing の信頼性 (自動修復が意図しない変更を入れるリスク) + +--- + +## 4. 次のステップ + +- [ ] Vite プラグインとしての依存ツリー取得 PoC +- [ ] Playwright VRT + LLM リーズニングの最小プロトタイプ +- [ ] Intent 抽出プロンプトの設計 +- [ ] コスト試算 (スナップショット数 × LLM API コスト) diff --git a/e2e/commits-branches.spec.ts b/e2e/commits-branches.spec.ts index b9dc88c..206191a 100644 --- a/e2e/commits-branches.spec.ts +++ b/e2e/commits-branches.spec.ts @@ -191,18 +191,15 @@ test.describe('blame view', () => { }); }); -test.describe('navigation', () => { - test('nav bar has commits and branches links', async ({ page }) => { +test.describe('workspace landing page', () => { + test('prompts to open a repository before showing repository navigation', async ({ page }) => { await page.goto('/'); - const nav = page.getByRole('navigation', { name: 'primary navigation' }); - await expect(nav.getByRole('link', { name: 'commits' })).toBeVisible(); - await expect(nav.getByRole('link', { name: 'branches' })).toBeVisible(); - }); - - test('home page has commits and branches in feature list', async ({ page }) => { - await page.goto('/'); - const main = page.getByRole('main'); - await expect(main.getByRole('link', { name: 'commits' })).toBeVisible(); - await expect(main.getByRole('link', { name: 'branches' })).toBeVisible(); + await expect( + page.getByRole('heading', { level: 2, name: 'Open Repository' }), + ).toBeVisible(); + await expect(page.locator('#ws-repo')).toBeVisible(); + await expect( + page.getByRole('button', { name: 'Clone & Open' }), + ).toBeVisible(); }); }); diff --git a/e2e/features.spec.ts b/e2e/features.spec.ts index 3e841e0..9ea148c 100644 --- a/e2e/features.spec.ts +++ b/e2e/features.spec.ts @@ -181,17 +181,18 @@ test.describe('compare', () => { }); // --------------------------------------------------------------------------- -// Navigation includes new links +// Workspace landing page // --------------------------------------------------------------------------- -test.describe('navigation', () => { - test('nav bar includes issues, activity, search', async ({ page }) => { +test.describe('workspace landing page', () => { + test('prompts to open a repository before showing repository navigation', async ({ page }) => { await page.goto('/'); - const nav = page.getByRole('navigation', { - name: 'primary navigation', - }); - await expect(nav.getByRole('link', { name: 'issues' })).toBeVisible(); - await expect(nav.getByRole('link', { name: 'activity' })).toBeVisible(); - await expect(nav.getByRole('link', { name: 'search' })).toBeVisible(); + await expect( + page.getByRole('heading', { level: 2, name: 'Open Repository' }), + ).toBeVisible(); + await expect(page.locator('#ws-repo')).toBeVisible(); + await expect( + page.getByRole('button', { name: 'Clone & Open' }), + ).toBeVisible(); }); }); diff --git a/e2e/mars-http.spec.ts b/e2e/mars-http.spec.ts index f3d0d42..3feb9fa 100644 --- a/e2e/mars-http.spec.ts +++ b/e2e/mars-http.spec.ts @@ -6,8 +6,9 @@ test('home route is available', async ({ page }) => { expect(response).not.toBeNull(); expect(response!.status()).toBe(200); await expect(page.getByRole('heading', { level: 1, name: 'bithub' })).toBeVisible(); - await expect(page.getByRole('link', { name: 'README.md' })).toBeVisible(); - await expect(page.getByRole('link', { name: 'files' })).toBeVisible(); + await expect(page.getByRole('heading', { level: 2, name: 'Open Repository' })).toBeVisible(); + await expect(page.locator('#ws-repo')).toBeVisible(); + await expect(page.getByRole('button', { name: 'Clone & Open' })).toBeVisible(); }); test('healthz returns ok', async ({ request }) => { diff --git a/e2e/pulls-webhooks.spec.ts b/e2e/pulls-webhooks.spec.ts index b6d788c..3ada02d 100644 --- a/e2e/pulls-webhooks.spec.ts +++ b/e2e/pulls-webhooks.spec.ts @@ -187,10 +187,15 @@ test.describe('webhook log', () => { }); // --------------------------------------------------------------------------- -// Navigation +// Workspace landing page // --------------------------------------------------------------------------- -test('nav includes pulls link', async ({ page }) => { +test('workspace landing page prompts to open a repository', async ({ page }) => { await page.goto('/'); - const nav = page.getByRole('navigation', { name: 'primary navigation' }); - await expect(nav.getByRole('link', { name: 'pulls' })).toBeVisible(); + await expect( + page.getByRole('heading', { level: 2, name: 'Open Repository' }), + ).toBeVisible(); + await expect(page.locator('#ws-repo')).toBeVisible(); + await expect( + page.getByRole('button', { name: 'Clone & Open' }), + ).toBeVisible(); }); diff --git a/justfile b/justfile index b827ac7..1e459f0 100644 --- a/justfile +++ b/justfile @@ -9,7 +9,11 @@ check: moon check --target js test: - moon test --target js + moon test --target js --package bithub + just test-artifacts + +test-artifacts: + node --test tests/artifacts_api.test.mjs build: moon build --target js @@ -70,6 +74,69 @@ agent-parallel: # ---- Workflow: Issue → Agent → Done ---- +# ---- VRT (Visual Regression + Semantic Testing) ---- + +# Initialize VRT baselines (requires running server) +vrt-init: + npx tsx vrt/src/vrt-cli.ts init + +# Capture current VRT snapshots +vrt-capture: + npx tsx vrt/src/vrt-cli.ts capture + +# Verify VRT snapshots against baselines +vrt-verify: + npx tsx vrt/src/vrt-cli.ts verify + +# Accept current snapshots as new baselines +vrt-approve: + npx tsx vrt/src/vrt-cli.ts approve + +# Show VRT report +vrt-report: + npx tsx vrt/src/vrt-cli.ts report + +# Show affected components +vrt-affected: + npx tsx vrt/src/vrt-cli.ts affected + +# Generate spec.json from current state +vrt-introspect: + npx tsx vrt/src/vrt-cli.ts introspect + +# Verify spec.json invariants +vrt-spec-verify: + npx tsx vrt/src/vrt-cli.ts spec-verify + +# Auto-generate expectation.json from diff +vrt-expect: + npx tsx vrt/src/vrt-cli.ts expect + +# Run VRT demo (kitty graphics) +vrt-demo: + npx tsx vrt/src/demo.ts + +# Run fix loop demo (detect → reason → fix → verify) +vrt-demo-fix: + npx tsx vrt/src/demo-fix-loop.ts + +# Run multi-scenario demo (3 complex scenarios) +vrt-demo-multi: + npx tsx vrt/src/demo-scenarios.ts + +# Run 6-step dashboard rebuild demo +vrt-demo-multistep: + npx tsx vrt/src/demo-multistep.ts + +# Run VRT unit tests +vrt-test: + cd vrt && node --test --experimental-strip-types src/**/*.test.ts + +# Full VRT cycle: capture + verify +vrt: vrt-capture vrt-verify + +# ---- Workflow: Issue → Agent → Done ---- + # Full workflow: create issue, run daemon once auto title body="": #!/usr/bin/env bash diff --git a/moon.mod.json b/moon.mod.json index 07ce20f..3075ac5 100644 --- a/moon.mod.json +++ b/moon.mod.json @@ -1,15 +1,21 @@ { "name": "bit-vcs/bithub", - "version": "0.1.1", + "version": "0.1.2", "deps": { - "moonbitlang/async": "0.16.8", - "mizchi/mars": "0.3.9", - "mizchi/bit": "0.37.0", - "mizchi/markdown": "0.4.7", - "mizchi/luna": "0.16.0", - "moonbitlang/x": "0.4.41", - "mizchi/js": "0.10.14", - "mizchi/sol": "0.12.0" + "moonbitlang/async": "0.20.3", + "mizchi/mars": "0.3.11", + "mizchi/bit": "0.45.1", + "mizchi/markdown": "0.6.2", + "mizchi/luna": "0.23.3", + "moonbitlang/x": "0.4.47", + "mizchi/js": "0.12.1", + "mizchi/sol": "0.23.2", + "mizchi/bit_lib": "0.45.1", + "mizchi/bit_vfs": "0.45.1", + "mizchi/bitx_hub": "0.45.1", + "mizchi/bit_core": "0.45.1", + "mizchi/bit_types": "0.45.1", + "mizchi/bit_object": "0.45.1" }, "readme": "README.md", "repository": "https://github.com/bit-vcs/bithub", @@ -23,4 +29,4 @@ "description": "GitHub-like UI backend for bit", "source": "src", "preferred-target": "js" -} \ No newline at end of file +} diff --git a/package.json b/package.json index 9ae3e47..ab59b77 100644 --- a/package.json +++ b/package.json @@ -24,11 +24,11 @@ "test:e2e:ui": "playwright test --ui" }, "devDependencies": { - "@playwright/test": "^1.52.0", - "tsx": "^4.21.0", - "wrangler": "^4.74.0" + "@playwright/test": "^1.61.1", + "tsx": "^4.23.1", + "wrangler": "^4.114.0" }, "dependencies": { - "@hono/node-server": "^1.19.9" + "@hono/node-server": "^2.0.11" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b4df3c0..3831f12 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,60 +9,60 @@ importers: .: dependencies: '@hono/node-server': - specifier: ^1.19.9 - version: 1.19.9(hono@4.11.10) + specifier: ^2.0.11 + version: 2.0.11(hono@4.11.10) devDependencies: '@playwright/test': - specifier: ^1.52.0 - version: 1.58.2 + specifier: ^1.61.1 + version: 1.61.1 tsx: - specifier: ^4.21.0 - version: 4.21.0 + specifier: ^4.23.1 + version: 4.23.1 wrangler: - specifier: ^4.74.0 - version: 4.74.0 + specifier: ^4.114.0 + version: 4.114.0 packages: - '@cloudflare/kv-asset-handler@0.4.2': - resolution: {integrity: sha512-SIOD2DxrRRwQ+jgzlXCqoEFiKOFqaPjhnNTGKXSRLvp1HiOvapLaFG2kEr9dYQTYe8rKrd9uvDUzmAITeNyaHQ==} - engines: {node: '>=18.0.0'} + '@cloudflare/kv-asset-handler@0.5.0': + resolution: {integrity: sha512-jxQYkj8dSIzc0cD6cMMNdOc1UVjqSqu8BZdor5s8cGjW2I8BjODt/kWPVdY+u9zj3ms75Q5qaZgnxUad83+eAg==} + engines: {node: '>=22.0.0'} - '@cloudflare/unenv-preset@2.15.0': - resolution: {integrity: sha512-EGYmJaGZKWl+X8tXxcnx4v2bOZSjQeNI5dWFeXivgX9+YCT69AkzHHwlNbVpqtEUTbew8eQurpyOpeN8fg00nw==} + '@cloudflare/unenv-preset@2.16.1': + resolution: {integrity: sha512-ECxObrMfyTl5bhQf/lZCXwo5G6xX9IAUo+nDMKK4SZ8m4Jvvxp52vilxyySSWh2YTZz8+HQ07qGH/2rEom1vDw==} peerDependencies: unenv: 2.0.0-rc.24 - workerd: 1.20260301.1 || ~1.20260302.1 || ~1.20260303.1 || ~1.20260304.1 || >1.20260305.0 <2.0.0-0 + workerd: '>1.20260305.0 <2.0.0-0' peerDependenciesMeta: workerd: optional: true - '@cloudflare/workerd-darwin-64@1.20260312.1': - resolution: {integrity: sha512-HUAtDWaqUduS6yasV6+NgsK7qBpP1qGU49ow/Wb117IHjYp+PZPUGReDYocpB4GOMRoQlvdd4L487iFxzdARpw==} + '@cloudflare/workerd-darwin-64@1.20260722.1': + resolution: {integrity: sha512-vZOP8vIS3NwnuaO+gz0FZ7kIGeiO3bZmxV35Ph9zOXKSREhDFlH7wQ7mkCdhW3O4jnXsew+XT7b+DNEI2CcJGQ==} engines: {node: '>=16'} cpu: [x64] os: [darwin] - '@cloudflare/workerd-darwin-arm64@1.20260312.1': - resolution: {integrity: sha512-DOn7TPTHSxJYfi4m4NYga/j32wOTqvJf/pY4Txz5SDKWIZHSTXFyGz2K4B+thoPWLop/KZxGoyTv7db0mk/qyw==} + '@cloudflare/workerd-darwin-arm64@1.20260722.1': + resolution: {integrity: sha512-EmIQymihDq6WNdER4+LF8Qn80yqayBUpJ+tkOO7wmY8pmgfyXjIUFNXotl21AHovTeu2seR7HdVUgeN/BilCWw==} engines: {node: '>=16'} cpu: [arm64] os: [darwin] - '@cloudflare/workerd-linux-64@1.20260312.1': - resolution: {integrity: sha512-TdkIh3WzPXYHuvz7phAtFEEvAxvFd30tHrm4gsgpw0R0F5b8PtoM3hfL2uY7EcBBWVYUBtkY2ahDYFfufnXw/g==} + '@cloudflare/workerd-linux-64@1.20260722.1': + resolution: {integrity: sha512-jvZ3k9fxcnEn04s80CgIYxQfpOyAiz/8qC42DP8EBa9tR27qWyg9wmm31zIobVlrgBZn/+8NfdP73avRGcQOjQ==} engines: {node: '>=16'} cpu: [x64] os: [linux] - '@cloudflare/workerd-linux-arm64@1.20260312.1': - resolution: {integrity: sha512-kNauZhL569Iy94t844OMwa1zP6zKFiL3xiJ4tGLS+TFTEfZ3pZsRH6lWWOtkXkjTyCmBEOog0HSEKjIV4oAffw==} + '@cloudflare/workerd-linux-arm64@1.20260722.1': + resolution: {integrity: sha512-BOSB55SMNdy+DA5uj2WirgiNanpHGis5PVvXH1wSfvjRKr4JGgWK+EZzxz0RFUo6QjjQQC/NimEzNZ7va7jmKg==} engines: {node: '>=16'} cpu: [arm64] os: [linux] - '@cloudflare/workerd-windows-64@1.20260312.1': - resolution: {integrity: sha512-5dBrlSK+nMsZy5bYQpj8t9iiQNvCRlkm9GGvswJa9vVU/1BNO4BhJMlqOLWT24EmFyApZ+kaBiPJMV8847NDTg==} + '@cloudflare/workerd-windows-64@1.20260722.1': + resolution: {integrity: sha512-sYM8YgUpKnRz2xjvdJLX1Ojzoi4MlA4gk8WTTExhGydjYB2UTs5NIbv0ZmpKgMoK9io3ixgmiW56ZnTbcWOdiA==} engines: {node: '>=16'} cpu: [x64] os: [win32] @@ -71,168 +71,168 @@ packages: resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} engines: {node: '>=12'} - '@emnapi/runtime@1.9.0': - resolution: {integrity: sha512-QN75eB0IH2ywSpRpNddCRfQIhmJYBCJ1x5Lb3IscKAL8bMnVAKnRg8dCoXbHzVLLH7P38N2Z3mtulB7W0J0FKw==} + '@emnapi/runtime@1.11.2': + resolution: {integrity: sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA==} - '@esbuild/aix-ppc64@0.27.3': - resolution: {integrity: sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==} + '@esbuild/aix-ppc64@0.28.1': + resolution: {integrity: sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==} engines: {node: '>=18'} cpu: [ppc64] os: [aix] - '@esbuild/android-arm64@0.27.3': - resolution: {integrity: sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==} + '@esbuild/android-arm64@0.28.1': + resolution: {integrity: sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==} engines: {node: '>=18'} cpu: [arm64] os: [android] - '@esbuild/android-arm@0.27.3': - resolution: {integrity: sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==} + '@esbuild/android-arm@0.28.1': + resolution: {integrity: sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==} engines: {node: '>=18'} cpu: [arm] os: [android] - '@esbuild/android-x64@0.27.3': - resolution: {integrity: sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==} + '@esbuild/android-x64@0.28.1': + resolution: {integrity: sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==} engines: {node: '>=18'} cpu: [x64] os: [android] - '@esbuild/darwin-arm64@0.27.3': - resolution: {integrity: sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==} + '@esbuild/darwin-arm64@0.28.1': + resolution: {integrity: sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] - '@esbuild/darwin-x64@0.27.3': - resolution: {integrity: sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==} + '@esbuild/darwin-x64@0.28.1': + resolution: {integrity: sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==} engines: {node: '>=18'} cpu: [x64] os: [darwin] - '@esbuild/freebsd-arm64@0.27.3': - resolution: {integrity: sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==} + '@esbuild/freebsd-arm64@0.28.1': + resolution: {integrity: sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-x64@0.27.3': - resolution: {integrity: sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==} + '@esbuild/freebsd-x64@0.28.1': + resolution: {integrity: sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] - '@esbuild/linux-arm64@0.27.3': - resolution: {integrity: sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==} + '@esbuild/linux-arm64@0.28.1': + resolution: {integrity: sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==} engines: {node: '>=18'} cpu: [arm64] os: [linux] - '@esbuild/linux-arm@0.27.3': - resolution: {integrity: sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==} + '@esbuild/linux-arm@0.28.1': + resolution: {integrity: sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==} engines: {node: '>=18'} cpu: [arm] os: [linux] - '@esbuild/linux-ia32@0.27.3': - resolution: {integrity: sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==} + '@esbuild/linux-ia32@0.28.1': + resolution: {integrity: sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==} engines: {node: '>=18'} cpu: [ia32] os: [linux] - '@esbuild/linux-loong64@0.27.3': - resolution: {integrity: sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==} + '@esbuild/linux-loong64@0.28.1': + resolution: {integrity: sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==} engines: {node: '>=18'} cpu: [loong64] os: [linux] - '@esbuild/linux-mips64el@0.27.3': - resolution: {integrity: sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==} + '@esbuild/linux-mips64el@0.28.1': + resolution: {integrity: sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] - '@esbuild/linux-ppc64@0.27.3': - resolution: {integrity: sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==} + '@esbuild/linux-ppc64@0.28.1': + resolution: {integrity: sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] - '@esbuild/linux-riscv64@0.27.3': - resolution: {integrity: sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==} + '@esbuild/linux-riscv64@0.28.1': + resolution: {integrity: sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] - '@esbuild/linux-s390x@0.27.3': - resolution: {integrity: sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==} + '@esbuild/linux-s390x@0.28.1': + resolution: {integrity: sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==} engines: {node: '>=18'} cpu: [s390x] os: [linux] - '@esbuild/linux-x64@0.27.3': - resolution: {integrity: sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==} + '@esbuild/linux-x64@0.28.1': + resolution: {integrity: sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==} engines: {node: '>=18'} cpu: [x64] os: [linux] - '@esbuild/netbsd-arm64@0.27.3': - resolution: {integrity: sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==} + '@esbuild/netbsd-arm64@0.28.1': + resolution: {integrity: sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] - '@esbuild/netbsd-x64@0.27.3': - resolution: {integrity: sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==} + '@esbuild/netbsd-x64@0.28.1': + resolution: {integrity: sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] - '@esbuild/openbsd-arm64@0.27.3': - resolution: {integrity: sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==} + '@esbuild/openbsd-arm64@0.28.1': + resolution: {integrity: sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] - '@esbuild/openbsd-x64@0.27.3': - resolution: {integrity: sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==} + '@esbuild/openbsd-x64@0.28.1': + resolution: {integrity: sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] - '@esbuild/openharmony-arm64@0.27.3': - resolution: {integrity: sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==} + '@esbuild/openharmony-arm64@0.28.1': + resolution: {integrity: sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==} engines: {node: '>=18'} cpu: [arm64] os: [openharmony] - '@esbuild/sunos-x64@0.27.3': - resolution: {integrity: sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==} + '@esbuild/sunos-x64@0.28.1': + resolution: {integrity: sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==} engines: {node: '>=18'} cpu: [x64] os: [sunos] - '@esbuild/win32-arm64@0.27.3': - resolution: {integrity: sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==} + '@esbuild/win32-arm64@0.28.1': + resolution: {integrity: sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==} engines: {node: '>=18'} cpu: [arm64] os: [win32] - '@esbuild/win32-ia32@0.27.3': - resolution: {integrity: sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==} + '@esbuild/win32-ia32@0.28.1': + resolution: {integrity: sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==} engines: {node: '>=18'} cpu: [ia32] os: [win32] - '@esbuild/win32-x64@0.27.3': - resolution: {integrity: sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==} + '@esbuild/win32-x64@0.28.1': + resolution: {integrity: sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==} engines: {node: '>=18'} cpu: [x64] os: [win32] - '@hono/node-server@1.19.9': - resolution: {integrity: sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw==} - engines: {node: '>=18.14.1'} + '@hono/node-server@2.0.11': + resolution: {integrity: sha512-bjD221KPLoJTWUwso1J6fGKiTXEUFedG/s0visavY4zakFPkeGURMRNly+FhBHs7T8Dz4qHaZIMX9ZoJHSJtKA==} + engines: {node: '>=20'} peerDependencies: hono: ^4 @@ -240,136 +240,161 @@ packages: resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==} engines: {node: '>=18'} - '@img/sharp-darwin-arm64@0.34.5': - resolution: {integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + '@img/sharp-darwin-arm64@0.35.2': + resolution: {integrity: sha512-eEieHsMksAW4IiO5NzauESRl2D2qz3J/kwUxUrSfV06A93eEaRfMpHXyUb1mAqrR7i8U9A0GRqE9pjn6u1Jjpg==} + engines: {node: '>=20.9.0'} cpu: [arm64] os: [darwin] - '@img/sharp-darwin-x64@0.34.5': - resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + '@img/sharp-darwin-x64@0.35.2': + resolution: {integrity: sha512-BaktuGPCeHJMARpodR8jK4uKiZrPAy9WrfQW0sdI37clracq8Bp01AYS3SZgi5FS/y5twa9t4+LIuuxQjqRrWw==} + engines: {node: '>=20.9.0'} cpu: [x64] os: [darwin] - '@img/sharp-libvips-darwin-arm64@1.2.4': - resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==} + '@img/sharp-freebsd-wasm32@0.35.2': + resolution: {integrity: sha512-YoAxdnd8hPUkvLHd3bWY+YA8nw3xM/RyRopYucNsWHVSan8NLVM3X2volsfoRDcXdUJPg6tXahSd7HXPK7lRnw==} + engines: {node: '>=20.9.0'} + os: [freebsd] + + '@img/sharp-libvips-darwin-arm64@1.3.1': + resolution: {integrity: sha512-4V/M3roRMTYjiwZY9IOVQOE8OyeCxFAkYmyZDrZl51uOKjibm3oeEJ4WAmLxutAfzFbC9jqUiPs2gbnGflH+7g==} cpu: [arm64] os: [darwin] - '@img/sharp-libvips-darwin-x64@1.2.4': - resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==} + '@img/sharp-libvips-darwin-x64@1.3.1': + resolution: {integrity: sha512-c0/DxItpJv2+dGhgycJBBgotdqruGYDvA79drdh0MD1dFpy7JzJ/PlXwi1H4rFf0eTy8tgbI91aHDnZIceY3jQ==} cpu: [x64] os: [darwin] - '@img/sharp-libvips-linux-arm64@1.2.4': - resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} + '@img/sharp-libvips-linux-arm64@1.3.1': + resolution: {integrity: sha512-JznefmcK9j1JKPz8AkQDh89kjojubyfOasWBPKfzMIhPwsgDy9evpE/naJTXXXmghS1iFwR8u/kTwh/I2/+GCw==} cpu: [arm64] os: [linux] + libc: [glibc] - '@img/sharp-libvips-linux-arm@1.2.4': - resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} + '@img/sharp-libvips-linux-arm@1.3.1': + resolution: {integrity: sha512-aGGy9aWzXgHBG7HNyQPWorZthlp7+x6fDRoPAQbGO3ThcttuTyKIx3NuSHb6zb4gBNq6/yNn9f1cy9nFKS/Vmg==} cpu: [arm] os: [linux] + libc: [glibc] - '@img/sharp-libvips-linux-ppc64@1.2.4': - resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} + '@img/sharp-libvips-linux-ppc64@1.3.1': + resolution: {integrity: sha512-1EkwGNCZk6iWNCMWqrvdJ+r1j0PT1zIz60CNPhYnJlK/zyeWqlsPZIe+ocBVqPF8k/Ssee/NCk+tE9Ryrko6ng==} cpu: [ppc64] os: [linux] + libc: [glibc] - '@img/sharp-libvips-linux-riscv64@1.2.4': - resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} + '@img/sharp-libvips-linux-riscv64@1.3.1': + resolution: {integrity: sha512-Ilays+w2bXdnxzxtQdmXR62u8o8GYa3eL4+Gr+1KiE4xperMZUslRaVPJwwPkzlHEjGfXAfRVAa/7CYCtSqsBw==} cpu: [riscv64] os: [linux] + libc: [glibc] - '@img/sharp-libvips-linux-s390x@1.2.4': - resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} + '@img/sharp-libvips-linux-s390x@1.3.1': + resolution: {integrity: sha512-VfBwVHQTbRoj4XlpA/KLZ7ltgMpz+4WSejFzQ+GnoImjo1PtEJ59QB2qR1xQEeRPYIkNrPIm2L4cICMvz4C2ew==} cpu: [s390x] os: [linux] + libc: [glibc] - '@img/sharp-libvips-linux-x64@1.2.4': - resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} + '@img/sharp-libvips-linux-x64@1.3.1': + resolution: {integrity: sha512-+c8ukgwU62DS54nCAjw7keOfHUkmr0B5QHEdcOqRnodF/MNXJbVI8Eopoj4B/0H8Asr65I+A4Amrn7a85/md6A==} cpu: [x64] os: [linux] + libc: [glibc] - '@img/sharp-libvips-linuxmusl-arm64@1.2.4': - resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} + '@img/sharp-libvips-linuxmusl-arm64@1.3.1': + resolution: {integrity: sha512-qlKb/pwbkAi1WMsJrYHk7CuDrd12s27U2QnRhFYUoJNrRCmkosMTttuRFat/DDB3IlDm5qE1TJgZ4JDnHX8Ldw==} cpu: [arm64] os: [linux] + libc: [musl] - '@img/sharp-libvips-linuxmusl-x64@1.2.4': - resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} + '@img/sharp-libvips-linuxmusl-x64@1.3.1': + resolution: {integrity: sha512-yO21HwoUVLN8Qa+/SBjQLMYwBWAVJjeGPNe+hc0OUeMeifEtJqu5a1c4HayE1nNpDih9y3/KkoltfkDodmKAlg==} cpu: [x64] os: [linux] + libc: [musl] - '@img/sharp-linux-arm64@0.34.5': - resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + '@img/sharp-linux-arm64@0.35.2': + resolution: {integrity: sha512-af12Pnd0ZGu2HfP8NayB0kk6eC/lrfbQE6HlR4jD+34wdJ1Vw9TF6TMn6ZvffT+WgqVsl0hRbmNvz2u/23VmwA==} + engines: {node: '>=20.9.0'} cpu: [arm64] os: [linux] + libc: [glibc] - '@img/sharp-linux-arm@0.34.5': - resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + '@img/sharp-linux-arm@0.35.2': + resolution: {integrity: sha512-SE4kzF2mepn6z+6E7L6lsV8FzuLL6IPQdyX8ZiwROAG/G8td+hP/m7FsFPwidtrF19gvajuC9l6TxAVcsA4S7A==} + engines: {node: '>=20.9.0'} cpu: [arm] os: [linux] + libc: [glibc] - '@img/sharp-linux-ppc64@0.34.5': - resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + '@img/sharp-linux-ppc64@0.35.2': + resolution: {integrity: sha512-hYSBm7zcNtDCozCxQHYZJiu63b/bXsgRZuOxCIBZsStMM9Vap47iFHdbX4kCvQsblPB/k+clhELpdQJHQLSHvg==} + engines: {node: '>=20.9.0'} cpu: [ppc64] os: [linux] + libc: [glibc] - '@img/sharp-linux-riscv64@0.34.5': - resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + '@img/sharp-linux-riscv64@0.35.2': + resolution: {integrity: sha512-qQt0Kc13+Hoan/Awq/qMSQw3L+RI1NCRPgD5cUJ/1WSSmIoysLOc72jlRM3E0OHN9Yr313jgeQ2T+zW+F03QFA==} + engines: {node: '>=20.9.0'} cpu: [riscv64] os: [linux] + libc: [glibc] - '@img/sharp-linux-s390x@0.34.5': - resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + '@img/sharp-linux-s390x@0.35.2': + resolution: {integrity: sha512-E4fLLfRPzDLlEeDaTzI98OFLcv++WL5ChLLMwPoVd0CIoZQqupBSNbOisPL5am9XsbQ9T84+iiMpUvbFtkunbA==} + engines: {node: '>=20.9.0'} cpu: [s390x] os: [linux] + libc: [glibc] - '@img/sharp-linux-x64@0.34.5': - resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + '@img/sharp-linux-x64@0.35.2': + resolution: {integrity: sha512-gi0zFJJRLswfCZmHtJdikXPOc5u7qamSOS3NHedLqLd4W8Q0NqjdBr6TTRIgsfFjqfTsHFgdfvJ9LwqSgcHiAA==} + engines: {node: '>=20.9.0'} cpu: [x64] os: [linux] + libc: [glibc] - '@img/sharp-linuxmusl-arm64@0.34.5': - resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + '@img/sharp-linuxmusl-arm64@0.35.2': + resolution: {integrity: sha512-siWbOW1u6HFnFLrp0waKyW7VEf7jYvcDWdrXEFa8AkdAQgEvuu5Fz8/Y70w9EeqAdwDtfU012BhEHHaDqvQNzg==} + engines: {node: '>=20.9.0'} cpu: [arm64] os: [linux] + libc: [musl] - '@img/sharp-linuxmusl-x64@0.34.5': - resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + '@img/sharp-linuxmusl-x64@0.35.2': + resolution: {integrity: sha512-YBqMMcjDi4QGYiSn4vNOYBhmlC4z5AXqkOUUqI2e0AFA4urNv4ESgOgwNl3K+4etQhha0twXlzeF20bbULm9Yg==} + engines: {node: '>=20.9.0'} cpu: [x64] os: [linux] + libc: [musl] - '@img/sharp-wasm32@0.34.5': - resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + '@img/sharp-wasm32@0.35.2': + resolution: {integrity: sha512-Mrv4JQNYVQ94xH+jzZ9r+gowleN8mv2FTgKT+PI6bx5C0G8TdNYndu161pg2i7uoBwxy2ImPMHrJOM2LZef7Bw==} + engines: {node: '>=20.9.0'} + + '@img/sharp-webcontainers-wasm32@0.35.2': + resolution: {integrity: sha512-QNV27pxs9wpApEiCfvHM1RDoP1w1+2KrUWWDPEhEwg+latvOrfuhWrHWZKwdSFwU6jh3myjw/yOCRsUIuOft3g==} + engines: {node: '>=20.9.0'} cpu: [wasm32] - '@img/sharp-win32-arm64@0.34.5': - resolution: {integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + '@img/sharp-win32-arm64@0.35.2': + resolution: {integrity: sha512-BiVRYc/t6/Vl3e1hBx0hugG4oN9Pydf4fgMSpxTQJmwGUg/YoXTWHiFeRymHfCZzifxu4F4rpk/I67D0LQ20wQ==} + engines: {node: '>=20.9.0'} cpu: [arm64] os: [win32] - '@img/sharp-win32-ia32@0.34.5': - resolution: {integrity: sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + '@img/sharp-win32-ia32@0.35.2': + resolution: {integrity: sha512-YYEhx9PImCC7T0tI8JDMi4DB9LwLCXCU5OWNYEXAxh5Q1ShKkyC6byxzoBJ3gEFDnH2lQckWuDe70G7mB2XJog==} + engines: {node: ^20.9.0} cpu: [ia32] os: [win32] - '@img/sharp-win32-x64@0.34.5': - resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + '@img/sharp-win32-x64@0.35.2': + resolution: {integrity: sha512-imoOyBcoM/iiUr4J6VPpCNjPnjvP/Gks95898yB8YqoGGYmHYbOyCuNv9FMhFgtaiHFGbHW8bxKqRV6VjtXThQ==} + engines: {node: '>=20.9.0'} cpu: [x64] os: [win32] @@ -383,8 +408,8 @@ packages: '@jridgewell/trace-mapping@0.3.9': resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} - '@playwright/test@1.58.2': - resolution: {integrity: sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==} + '@playwright/test@1.61.1': + resolution: {integrity: sha512-8nKv6+0RJSL9FE4jYOEGXnPeM/Hg12qZpmqzZjRh3qM0Y7c3z1mrOTfFLids72RDQYVh9WpLEfR5WdpNX4fkig==} engines: {node: '>=18'} hasBin: true @@ -401,8 +426,8 @@ packages: resolution: {integrity: sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==} engines: {node: '>=18'} - '@speed-highlight/core@1.2.14': - resolution: {integrity: sha512-G4ewlBNhUtlLvrJTb88d2mdy2KRijzs4UhnlrOSRT4bmjh/IqNElZa3zkrZ+TC47TwtlDWzVLFADljF1Ijp5hA==} + '@speed-highlight/core@1.2.17': + resolution: {integrity: sha512-Z92FwKpCtfaW1V0jTU/fh3QzYEZN8wDwrzRIBoADCJfn4mJCNcJN/XegifX7BDrQ8/h9Xh/JnbyMchL0FqXrkg==} blake3-wasm@2.1.5: resolution: {integrity: sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==} @@ -418,8 +443,8 @@ packages: error-stack-parser-es@1.0.5: resolution: {integrity: sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==} - esbuild@0.27.3: - resolution: {integrity: sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==} + esbuild@0.28.1: + resolution: {integrity: sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==} engines: {node: '>=18'} hasBin: true @@ -433,9 +458,6 @@ packages: engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] - get-tsconfig@4.13.6: - resolution: {integrity: sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==} - hono@4.11.10: resolution: {integrity: sha512-kyWP5PAiMooEvGrA9jcD3IXF7ATu8+o7B3KCbPXid5se52NPqnOpM/r9qeW2heMnOekF4kqR1fXJqCYeCLKrZg==} engines: {node: '>=16.9.0'} @@ -444,9 +466,9 @@ packages: resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} engines: {node: '>=6'} - miniflare@4.20260312.1: - resolution: {integrity: sha512-YSWxec9ssisqkQgaCgcIQxZlB41E9hMiq1nxUgxXHRrE9NsfyC6ptSt8yfgBobsKIseAVKLTB/iEDpMumBv8oA==} - engines: {node: '>=18.0.0'} + miniflare@4.20260722.0: + resolution: {integrity: sha512-LW6ABMhCx/yIEFBLC/DO4yAhdm2T/G7jp7pr5T2kj895+CCIaHZqpMXdW9O6YE48LcYcCJChwWc8aEs1vpbTXw==} + engines: {node: '>=22.0.0'} hasBin: true path-to-regexp@6.3.0: @@ -455,27 +477,24 @@ packages: pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} - playwright-core@1.58.2: - resolution: {integrity: sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==} + playwright-core@1.61.1: + resolution: {integrity: sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==} engines: {node: '>=18'} hasBin: true - playwright@1.58.2: - resolution: {integrity: sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==} + playwright@1.61.1: + resolution: {integrity: sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ==} engines: {node: '>=18'} hasBin: true - resolve-pkg-maps@1.0.0: - resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} - - semver@7.7.4: - resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} + semver@7.8.5: + resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} engines: {node: '>=10'} hasBin: true - sharp@0.34.5: - resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + sharp@0.35.2: + resolution: {integrity: sha512-FVtFjtBCMiJS6yb5CX7Sop45WFMpeGw6oRKuJnXYgf/f1ms/D7LE/ZUSNxnW7rZ/dbslQWYkoqFHGPaDBtaK4w==} + engines: {node: '>=20.9.0'} supports-color@10.2.2: resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==} @@ -484,35 +503,35 @@ packages: tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} - tsx@4.21.0: - resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==} + tsx@4.23.1: + resolution: {integrity: sha512-GQHnkIfxyx1wYCOS/wonik5MVRZU9hi1TEZmzGZSCJB1y9YgoZ8H6itNE/u4suE+yLmOzuE4E5S4TZ/ZX2wcWQ==} engines: {node: '>=18.0.0'} hasBin: true - undici@7.18.2: - resolution: {integrity: sha512-y+8YjDFzWdQlSE9N5nzKMT3g4a5UBX1HKowfdXh0uvAnTaqqwqB92Jt4UXBAeKekDs5IaDKyJFR4X1gYVCgXcw==} + undici@7.28.0: + resolution: {integrity: sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==} engines: {node: '>=20.18.1'} unenv@2.0.0-rc.24: resolution: {integrity: sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==} - workerd@1.20260312.1: - resolution: {integrity: sha512-nNpPkw9jaqo79B+iBCOiksx+N62xC+ETIfyzofUEdY3cSOHJg6oNnVSHm7vHevzVblfV76c8Gr0cXHEapYMBEg==} + workerd@1.20260722.1: + resolution: {integrity: sha512-NycKuc1x2onvsRfGGpM093vRlLFU2zHDAM0+APpccfg4+gZxDGCH27RmdDvkeBuoZyYqgLo3oAfF6re4mvC3vQ==} engines: {node: '>=16'} hasBin: true - wrangler@4.74.0: - resolution: {integrity: sha512-3qprbhgdUyqYGHZ+Y1k0gsyHLMOlLrKL/HU0LDqLlCkbsKPprUA0/ThE4IZsxD84xAAXY6pv5JUuxS2+OnMa3A==} - engines: {node: '>=20.0.0'} + wrangler@4.114.0: + resolution: {integrity: sha512-M65P25t5UHA1TIJfgZXDcj+YzVobgKdRguM2QPz0xnxLFuOcuE3ErgllDht0iaho7MS4o0g/Bb4YK2+GT+bibg==} + engines: {node: '>=22.0.0'} hasBin: true peerDependencies: - '@cloudflare/workers-types': ^4.20260312.1 + '@cloudflare/workers-types': ^5.20260722.1 peerDependenciesMeta: '@cloudflare/workers-types': optional: true - ws@8.18.0: - resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==} + ws@8.21.0: + resolution: {integrity: sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==} engines: {node: '>=10.0.0'} peerDependencies: bufferutil: ^4.0.1 @@ -531,214 +550,224 @@ packages: snapshots: - '@cloudflare/kv-asset-handler@0.4.2': {} + '@cloudflare/kv-asset-handler@0.5.0': {} - '@cloudflare/unenv-preset@2.15.0(unenv@2.0.0-rc.24)(workerd@1.20260312.1)': + '@cloudflare/unenv-preset@2.16.1(unenv@2.0.0-rc.24)(workerd@1.20260722.1)': dependencies: unenv: 2.0.0-rc.24 optionalDependencies: - workerd: 1.20260312.1 + workerd: 1.20260722.1 - '@cloudflare/workerd-darwin-64@1.20260312.1': + '@cloudflare/workerd-darwin-64@1.20260722.1': optional: true - '@cloudflare/workerd-darwin-arm64@1.20260312.1': + '@cloudflare/workerd-darwin-arm64@1.20260722.1': optional: true - '@cloudflare/workerd-linux-64@1.20260312.1': + '@cloudflare/workerd-linux-64@1.20260722.1': optional: true - '@cloudflare/workerd-linux-arm64@1.20260312.1': + '@cloudflare/workerd-linux-arm64@1.20260722.1': optional: true - '@cloudflare/workerd-windows-64@1.20260312.1': + '@cloudflare/workerd-windows-64@1.20260722.1': optional: true '@cspotcode/source-map-support@0.8.1': dependencies: '@jridgewell/trace-mapping': 0.3.9 - '@emnapi/runtime@1.9.0': + '@emnapi/runtime@1.11.2': dependencies: tslib: 2.8.1 optional: true - '@esbuild/aix-ppc64@0.27.3': + '@esbuild/aix-ppc64@0.28.1': optional: true - '@esbuild/android-arm64@0.27.3': + '@esbuild/android-arm64@0.28.1': optional: true - '@esbuild/android-arm@0.27.3': + '@esbuild/android-arm@0.28.1': optional: true - '@esbuild/android-x64@0.27.3': + '@esbuild/android-x64@0.28.1': optional: true - '@esbuild/darwin-arm64@0.27.3': + '@esbuild/darwin-arm64@0.28.1': optional: true - '@esbuild/darwin-x64@0.27.3': + '@esbuild/darwin-x64@0.28.1': optional: true - '@esbuild/freebsd-arm64@0.27.3': + '@esbuild/freebsd-arm64@0.28.1': optional: true - '@esbuild/freebsd-x64@0.27.3': + '@esbuild/freebsd-x64@0.28.1': optional: true - '@esbuild/linux-arm64@0.27.3': + '@esbuild/linux-arm64@0.28.1': optional: true - '@esbuild/linux-arm@0.27.3': + '@esbuild/linux-arm@0.28.1': optional: true - '@esbuild/linux-ia32@0.27.3': + '@esbuild/linux-ia32@0.28.1': optional: true - '@esbuild/linux-loong64@0.27.3': + '@esbuild/linux-loong64@0.28.1': optional: true - '@esbuild/linux-mips64el@0.27.3': + '@esbuild/linux-mips64el@0.28.1': optional: true - '@esbuild/linux-ppc64@0.27.3': + '@esbuild/linux-ppc64@0.28.1': optional: true - '@esbuild/linux-riscv64@0.27.3': + '@esbuild/linux-riscv64@0.28.1': optional: true - '@esbuild/linux-s390x@0.27.3': + '@esbuild/linux-s390x@0.28.1': optional: true - '@esbuild/linux-x64@0.27.3': + '@esbuild/linux-x64@0.28.1': optional: true - '@esbuild/netbsd-arm64@0.27.3': + '@esbuild/netbsd-arm64@0.28.1': optional: true - '@esbuild/netbsd-x64@0.27.3': + '@esbuild/netbsd-x64@0.28.1': optional: true - '@esbuild/openbsd-arm64@0.27.3': + '@esbuild/openbsd-arm64@0.28.1': optional: true - '@esbuild/openbsd-x64@0.27.3': + '@esbuild/openbsd-x64@0.28.1': optional: true - '@esbuild/openharmony-arm64@0.27.3': + '@esbuild/openharmony-arm64@0.28.1': optional: true - '@esbuild/sunos-x64@0.27.3': + '@esbuild/sunos-x64@0.28.1': optional: true - '@esbuild/win32-arm64@0.27.3': + '@esbuild/win32-arm64@0.28.1': optional: true - '@esbuild/win32-ia32@0.27.3': + '@esbuild/win32-ia32@0.28.1': optional: true - '@esbuild/win32-x64@0.27.3': + '@esbuild/win32-x64@0.28.1': optional: true - '@hono/node-server@1.19.9(hono@4.11.10)': + '@hono/node-server@2.0.11(hono@4.11.10)': dependencies: hono: 4.11.10 '@img/colour@1.1.0': {} - '@img/sharp-darwin-arm64@0.34.5': + '@img/sharp-darwin-arm64@0.35.2': optionalDependencies: - '@img/sharp-libvips-darwin-arm64': 1.2.4 + '@img/sharp-libvips-darwin-arm64': 1.3.1 optional: true - '@img/sharp-darwin-x64@0.34.5': + '@img/sharp-darwin-x64@0.35.2': optionalDependencies: - '@img/sharp-libvips-darwin-x64': 1.2.4 + '@img/sharp-libvips-darwin-x64': 1.3.1 + optional: true + + '@img/sharp-freebsd-wasm32@0.35.2': + dependencies: + '@img/sharp-wasm32': 0.35.2 optional: true - '@img/sharp-libvips-darwin-arm64@1.2.4': + '@img/sharp-libvips-darwin-arm64@1.3.1': optional: true - '@img/sharp-libvips-darwin-x64@1.2.4': + '@img/sharp-libvips-darwin-x64@1.3.1': optional: true - '@img/sharp-libvips-linux-arm64@1.2.4': + '@img/sharp-libvips-linux-arm64@1.3.1': optional: true - '@img/sharp-libvips-linux-arm@1.2.4': + '@img/sharp-libvips-linux-arm@1.3.1': optional: true - '@img/sharp-libvips-linux-ppc64@1.2.4': + '@img/sharp-libvips-linux-ppc64@1.3.1': optional: true - '@img/sharp-libvips-linux-riscv64@1.2.4': + '@img/sharp-libvips-linux-riscv64@1.3.1': optional: true - '@img/sharp-libvips-linux-s390x@1.2.4': + '@img/sharp-libvips-linux-s390x@1.3.1': optional: true - '@img/sharp-libvips-linux-x64@1.2.4': + '@img/sharp-libvips-linux-x64@1.3.1': optional: true - '@img/sharp-libvips-linuxmusl-arm64@1.2.4': + '@img/sharp-libvips-linuxmusl-arm64@1.3.1': optional: true - '@img/sharp-libvips-linuxmusl-x64@1.2.4': + '@img/sharp-libvips-linuxmusl-x64@1.3.1': optional: true - '@img/sharp-linux-arm64@0.34.5': + '@img/sharp-linux-arm64@0.35.2': optionalDependencies: - '@img/sharp-libvips-linux-arm64': 1.2.4 + '@img/sharp-libvips-linux-arm64': 1.3.1 optional: true - '@img/sharp-linux-arm@0.34.5': + '@img/sharp-linux-arm@0.35.2': optionalDependencies: - '@img/sharp-libvips-linux-arm': 1.2.4 + '@img/sharp-libvips-linux-arm': 1.3.1 optional: true - '@img/sharp-linux-ppc64@0.34.5': + '@img/sharp-linux-ppc64@0.35.2': optionalDependencies: - '@img/sharp-libvips-linux-ppc64': 1.2.4 + '@img/sharp-libvips-linux-ppc64': 1.3.1 optional: true - '@img/sharp-linux-riscv64@0.34.5': + '@img/sharp-linux-riscv64@0.35.2': optionalDependencies: - '@img/sharp-libvips-linux-riscv64': 1.2.4 + '@img/sharp-libvips-linux-riscv64': 1.3.1 optional: true - '@img/sharp-linux-s390x@0.34.5': + '@img/sharp-linux-s390x@0.35.2': optionalDependencies: - '@img/sharp-libvips-linux-s390x': 1.2.4 + '@img/sharp-libvips-linux-s390x': 1.3.1 optional: true - '@img/sharp-linux-x64@0.34.5': + '@img/sharp-linux-x64@0.35.2': optionalDependencies: - '@img/sharp-libvips-linux-x64': 1.2.4 + '@img/sharp-libvips-linux-x64': 1.3.1 optional: true - '@img/sharp-linuxmusl-arm64@0.34.5': + '@img/sharp-linuxmusl-arm64@0.35.2': optionalDependencies: - '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 + '@img/sharp-libvips-linuxmusl-arm64': 1.3.1 optional: true - '@img/sharp-linuxmusl-x64@0.34.5': + '@img/sharp-linuxmusl-x64@0.35.2': optionalDependencies: - '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + '@img/sharp-libvips-linuxmusl-x64': 1.3.1 optional: true - '@img/sharp-wasm32@0.34.5': + '@img/sharp-wasm32@0.35.2': dependencies: - '@emnapi/runtime': 1.9.0 + '@emnapi/runtime': 1.11.2 optional: true - '@img/sharp-win32-arm64@0.34.5': + '@img/sharp-webcontainers-wasm32@0.35.2': + dependencies: + '@img/sharp-wasm32': 0.35.2 optional: true - '@img/sharp-win32-ia32@0.34.5': + '@img/sharp-win32-arm64@0.35.2': optional: true - '@img/sharp-win32-x64@0.34.5': + '@img/sharp-win32-ia32@0.35.2': + optional: true + + '@img/sharp-win32-x64@0.35.2': optional: true '@jridgewell/resolve-uri@3.1.2': {} @@ -750,9 +779,9 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 - '@playwright/test@1.58.2': + '@playwright/test@1.61.1': dependencies: - playwright: 1.58.2 + playwright: 1.61.1 '@poppinss/colors@4.1.6': dependencies: @@ -768,7 +797,7 @@ snapshots: '@sindresorhus/is@7.2.0': {} - '@speed-highlight/core@1.2.14': {} + '@speed-highlight/core@1.2.17': {} blake3-wasm@2.1.5: {} @@ -778,34 +807,34 @@ snapshots: error-stack-parser-es@1.0.5: {} - esbuild@0.27.3: + esbuild@0.28.1: optionalDependencies: - '@esbuild/aix-ppc64': 0.27.3 - '@esbuild/android-arm': 0.27.3 - '@esbuild/android-arm64': 0.27.3 - '@esbuild/android-x64': 0.27.3 - '@esbuild/darwin-arm64': 0.27.3 - '@esbuild/darwin-x64': 0.27.3 - '@esbuild/freebsd-arm64': 0.27.3 - '@esbuild/freebsd-x64': 0.27.3 - '@esbuild/linux-arm': 0.27.3 - '@esbuild/linux-arm64': 0.27.3 - '@esbuild/linux-ia32': 0.27.3 - '@esbuild/linux-loong64': 0.27.3 - '@esbuild/linux-mips64el': 0.27.3 - '@esbuild/linux-ppc64': 0.27.3 - '@esbuild/linux-riscv64': 0.27.3 - '@esbuild/linux-s390x': 0.27.3 - '@esbuild/linux-x64': 0.27.3 - '@esbuild/netbsd-arm64': 0.27.3 - '@esbuild/netbsd-x64': 0.27.3 - '@esbuild/openbsd-arm64': 0.27.3 - '@esbuild/openbsd-x64': 0.27.3 - '@esbuild/openharmony-arm64': 0.27.3 - '@esbuild/sunos-x64': 0.27.3 - '@esbuild/win32-arm64': 0.27.3 - '@esbuild/win32-ia32': 0.27.3 - '@esbuild/win32-x64': 0.27.3 + '@esbuild/aix-ppc64': 0.28.1 + '@esbuild/android-arm': 0.28.1 + '@esbuild/android-arm64': 0.28.1 + '@esbuild/android-x64': 0.28.1 + '@esbuild/darwin-arm64': 0.28.1 + '@esbuild/darwin-x64': 0.28.1 + '@esbuild/freebsd-arm64': 0.28.1 + '@esbuild/freebsd-x64': 0.28.1 + '@esbuild/linux-arm': 0.28.1 + '@esbuild/linux-arm64': 0.28.1 + '@esbuild/linux-ia32': 0.28.1 + '@esbuild/linux-loong64': 0.28.1 + '@esbuild/linux-mips64el': 0.28.1 + '@esbuild/linux-ppc64': 0.28.1 + '@esbuild/linux-riscv64': 0.28.1 + '@esbuild/linux-s390x': 0.28.1 + '@esbuild/linux-x64': 0.28.1 + '@esbuild/netbsd-arm64': 0.28.1 + '@esbuild/netbsd-x64': 0.28.1 + '@esbuild/openbsd-arm64': 0.28.1 + '@esbuild/openbsd-x64': 0.28.1 + '@esbuild/openharmony-arm64': 0.28.1 + '@esbuild/sunos-x64': 0.28.1 + '@esbuild/win32-arm64': 0.28.1 + '@esbuild/win32-ia32': 0.28.1 + '@esbuild/win32-x64': 0.28.1 fsevents@2.3.2: optional: true @@ -813,21 +842,17 @@ snapshots: fsevents@2.3.3: optional: true - get-tsconfig@4.13.6: - dependencies: - resolve-pkg-maps: 1.0.0 - hono@4.11.10: {} kleur@4.1.5: {} - miniflare@4.20260312.1: + miniflare@4.20260722.0: dependencies: '@cspotcode/source-map-support': 0.8.1 - sharp: 0.34.5 - undici: 7.18.2 - workerd: 1.20260312.1 - ws: 8.18.0 + sharp: 0.35.2 + undici: 7.28.0 + workerd: 1.20260722.1 + ws: 8.21.0 youch: 4.1.0-beta.10 transitivePeerDependencies: - bufferutil @@ -837,92 +862,90 @@ snapshots: pathe@2.0.3: {} - playwright-core@1.58.2: {} + playwright-core@1.61.1: {} - playwright@1.58.2: + playwright@1.61.1: dependencies: - playwright-core: 1.58.2 + playwright-core: 1.61.1 optionalDependencies: fsevents: 2.3.2 - resolve-pkg-maps@1.0.0: {} - - semver@7.7.4: {} + semver@7.8.5: {} - sharp@0.34.5: + sharp@0.35.2: dependencies: '@img/colour': 1.1.0 detect-libc: 2.1.2 - semver: 7.7.4 + semver: 7.8.5 optionalDependencies: - '@img/sharp-darwin-arm64': 0.34.5 - '@img/sharp-darwin-x64': 0.34.5 - '@img/sharp-libvips-darwin-arm64': 1.2.4 - '@img/sharp-libvips-darwin-x64': 1.2.4 - '@img/sharp-libvips-linux-arm': 1.2.4 - '@img/sharp-libvips-linux-arm64': 1.2.4 - '@img/sharp-libvips-linux-ppc64': 1.2.4 - '@img/sharp-libvips-linux-riscv64': 1.2.4 - '@img/sharp-libvips-linux-s390x': 1.2.4 - '@img/sharp-libvips-linux-x64': 1.2.4 - '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 - '@img/sharp-libvips-linuxmusl-x64': 1.2.4 - '@img/sharp-linux-arm': 0.34.5 - '@img/sharp-linux-arm64': 0.34.5 - '@img/sharp-linux-ppc64': 0.34.5 - '@img/sharp-linux-riscv64': 0.34.5 - '@img/sharp-linux-s390x': 0.34.5 - '@img/sharp-linux-x64': 0.34.5 - '@img/sharp-linuxmusl-arm64': 0.34.5 - '@img/sharp-linuxmusl-x64': 0.34.5 - '@img/sharp-wasm32': 0.34.5 - '@img/sharp-win32-arm64': 0.34.5 - '@img/sharp-win32-ia32': 0.34.5 - '@img/sharp-win32-x64': 0.34.5 + '@img/sharp-darwin-arm64': 0.35.2 + '@img/sharp-darwin-x64': 0.35.2 + '@img/sharp-freebsd-wasm32': 0.35.2 + '@img/sharp-libvips-darwin-arm64': 1.3.1 + '@img/sharp-libvips-darwin-x64': 1.3.1 + '@img/sharp-libvips-linux-arm': 1.3.1 + '@img/sharp-libvips-linux-arm64': 1.3.1 + '@img/sharp-libvips-linux-ppc64': 1.3.1 + '@img/sharp-libvips-linux-riscv64': 1.3.1 + '@img/sharp-libvips-linux-s390x': 1.3.1 + '@img/sharp-libvips-linux-x64': 1.3.1 + '@img/sharp-libvips-linuxmusl-arm64': 1.3.1 + '@img/sharp-libvips-linuxmusl-x64': 1.3.1 + '@img/sharp-linux-arm': 0.35.2 + '@img/sharp-linux-arm64': 0.35.2 + '@img/sharp-linux-ppc64': 0.35.2 + '@img/sharp-linux-riscv64': 0.35.2 + '@img/sharp-linux-s390x': 0.35.2 + '@img/sharp-linux-x64': 0.35.2 + '@img/sharp-linuxmusl-arm64': 0.35.2 + '@img/sharp-linuxmusl-x64': 0.35.2 + '@img/sharp-webcontainers-wasm32': 0.35.2 + '@img/sharp-win32-arm64': 0.35.2 + '@img/sharp-win32-ia32': 0.35.2 + '@img/sharp-win32-x64': 0.35.2 supports-color@10.2.2: {} tslib@2.8.1: optional: true - tsx@4.21.0: + tsx@4.23.1: dependencies: - esbuild: 0.27.3 - get-tsconfig: 4.13.6 + esbuild: 0.28.1 optionalDependencies: fsevents: 2.3.3 - undici@7.18.2: {} + undici@7.28.0: {} unenv@2.0.0-rc.24: dependencies: pathe: 2.0.3 - workerd@1.20260312.1: + workerd@1.20260722.1: optionalDependencies: - '@cloudflare/workerd-darwin-64': 1.20260312.1 - '@cloudflare/workerd-darwin-arm64': 1.20260312.1 - '@cloudflare/workerd-linux-64': 1.20260312.1 - '@cloudflare/workerd-linux-arm64': 1.20260312.1 - '@cloudflare/workerd-windows-64': 1.20260312.1 + '@cloudflare/workerd-darwin-64': 1.20260722.1 + '@cloudflare/workerd-darwin-arm64': 1.20260722.1 + '@cloudflare/workerd-linux-64': 1.20260722.1 + '@cloudflare/workerd-linux-arm64': 1.20260722.1 + '@cloudflare/workerd-windows-64': 1.20260722.1 - wrangler@4.74.0: + wrangler@4.114.0: dependencies: - '@cloudflare/kv-asset-handler': 0.4.2 - '@cloudflare/unenv-preset': 2.15.0(unenv@2.0.0-rc.24)(workerd@1.20260312.1) + '@cloudflare/kv-asset-handler': 0.5.0 + '@cloudflare/unenv-preset': 2.16.1(unenv@2.0.0-rc.24)(workerd@1.20260722.1) blake3-wasm: 2.1.5 - esbuild: 0.27.3 - miniflare: 4.20260312.1 + esbuild: 0.28.1 + miniflare: 4.20260722.0 path-to-regexp: 6.3.0 unenv: 2.0.0-rc.24 - workerd: 1.20260312.1 + workerd: 1.20260722.1 optionalDependencies: - fsevents: 2.3.2 + fsevents: 2.3.3 transitivePeerDependencies: - bufferutil - utf-8-validate - ws@8.18.0: {} + ws@8.21.0: {} youch-core@0.3.3: dependencies: @@ -933,6 +956,6 @@ snapshots: dependencies: '@poppinss/colors': 4.1.6 '@poppinss/dumper': 0.6.5 - '@speed-highlight/core': 1.2.14 + '@speed-highlight/core': 1.2.17 cookie: 1.1.1 youch-core: 0.3.3 diff --git a/src/actrun/moon.pkg b/src/actrun/moon.pkg index 86ce20d..5dc2409 100644 --- a/src/actrun/moon.pkg +++ b/src/actrun/moon.pkg @@ -1,3 +1,3 @@ import { - "moonbitlang/core/strconv" @strconv, -} \ No newline at end of file + "moonbitlang/core/string" @string, +} diff --git a/src/actrun/parser.mbt b/src/actrun/parser.mbt index cb81613..419728d 100644 --- a/src/actrun/parser.mbt +++ b/src/actrun/parser.mbt @@ -50,24 +50,24 @@ fn strip_yaml_comment(text : String) -> String { let mut idx = 0 while idx < text.length() { let ch = text.unsafe_get(idx) - if ch == '\'' && not(in_double) { - in_single = not(in_single) + if ch == '\'' && !(in_double) { + in_single = !(in_single) idx += 1 continue } - if ch == '"' && not(in_single) { - in_double = not(in_double) + if ch == '"' && !(in_single) { + in_double = !(in_double) idx += 1 continue } - if ch == '#' && not(in_single) && not(in_double) { + if ch == '#' && !(in_single) && !(in_double) { return String::unsafe_substring(text, start=0, end=idx) .trim_end(chars=" \t") - .to_string() + .to_owned() } idx += 1 } - text.trim_end(chars=" \t").to_string() + text.trim_end(chars=" \t").to_owned() } ///| @@ -75,7 +75,7 @@ fn collect_yaml_lines(text : String, errors : Array[String]) -> Array[YamlLine] let lines : Array[YamlLine] = [] let mut line_no = 1 for line_view in text.split("\n") { - let raw = line_view.to_string().trim_end(chars="\r").to_string() + let raw = line_view.to_owned().trim_end(chars="\r").to_owned() let indent = count_indent(raw, line_no, errors) let content = if indent >= raw.length() { "" @@ -97,7 +97,7 @@ fn yaml_parser_new(text : String) -> WorkflowYamlParser { ///| fn WorkflowYamlParser::skip_ignorable(self : WorkflowYamlParser) -> Unit { while self.idx < self.lines.length() { - let trimmed = self.lines[self.idx].text.trim(chars=" ").to_string() + let trimmed = self.lines[self.idx].text.trim(chars=" ").to_owned() if trimmed.length() == 0 || trimmed == "---" || trimmed == "..." { self.idx += 1 } else { @@ -118,7 +118,7 @@ fn after_sequence_dash(text : String) -> String { } else { String::unsafe_substring(text, start=2, end=text.length()) .trim_start(chars=" ") - .to_string() + .to_owned() } } @@ -131,17 +131,17 @@ fn split_key_value(text : String) -> (String, String?)? { let mut idx = 0 while idx < text.length() { let ch = text.unsafe_get(idx) - if ch == '\'' && not(in_double) { - in_single = not(in_single) + if ch == '\'' && !(in_double) { + in_single = !(in_single) idx += 1 continue } - if ch == '"' && not(in_single) { - in_double = not(in_double) + if ch == '"' && !(in_single) { + in_double = !(in_double) idx += 1 continue } - if not(in_single) && not(in_double) { + if !(in_single) && !(in_double) { if ch == '[' { bracket_depth += 1 } else if ch == ']' && bracket_depth > 0 { @@ -153,14 +153,14 @@ fn split_key_value(text : String) -> (String, String?)? { } else if ch == ':' && bracket_depth == 0 && brace_depth == 0 { let key = String::unsafe_substring(text, start=0, end=idx) .trim(chars=" ") - .to_string() + .to_owned() let rest = String::unsafe_substring( text, start=idx + 1, end=text.length(), ) .trim_start(chars=" ") - .to_string() + .to_owned() if rest.length() == 0 { return Some((key, None)) } @@ -183,17 +183,17 @@ fn split_inline_items(text : String) -> Array[String] { let mut idx = 0 while idx < text.length() { let ch = text.unsafe_get(idx) - if ch == '\'' && not(in_double) { - in_single = not(in_single) + if ch == '\'' && !(in_double) { + in_single = !(in_single) idx += 1 continue } - if ch == '"' && not(in_single) { - in_double = not(in_double) + if ch == '"' && !(in_single) { + in_double = !(in_double) idx += 1 continue } - if not(in_single) && not(in_double) { + if !(in_single) && !(in_double) { if ch == '[' { bracket_depth += 1 } else if ch == ']' && bracket_depth > 0 { @@ -206,7 +206,7 @@ fn split_inline_items(text : String) -> Array[String] { items.push( String::unsafe_substring(text, start~, end=idx) .trim(chars=" ") - .to_string(), + .to_owned(), ) start = idx + 1 idx += 1 @@ -217,7 +217,7 @@ fn split_inline_items(text : String) -> Array[String] { } let last = String::unsafe_substring(text, start~, end=text.length()) .trim(chars=" ") - .to_string() + .to_owned() if last.length() > 0 { items.push(last) } @@ -257,7 +257,7 @@ fn parse_inline_map(text : String) -> YamlValue { ///| fn parse_inline_value(text : String) -> YamlValue { - let trimmed = text.trim(chars=" ").to_string() + let trimmed = text.trim(chars=" ").to_owned() if trimmed.length() == 0 || trimmed == "null" || trimmed == "~" { return YamlValue::Null } @@ -395,7 +395,7 @@ fn WorkflowYamlParser::parse_sequence( self.skip_ignorable() guard self.idx < self.lines.length() else { break } let line = self.lines[self.idx] - if line.indent != indent || not(is_sequence_item(line.text)) { + if line.indent != indent || !(is_sequence_item(line.text)) { break } let item_text = after_sequence_dash(line.text) @@ -592,11 +592,11 @@ fn yaml_as_optional_int( ) -> Int? { match value { Some(Str(text)) => { - let trimmed = text.trim(chars=" \t\r\n").to_string() + let trimmed = text.trim(chars=" \t\r\n").to_owned() if trimmed.length() == 0 { return None } - let parsed = @strconv.parse_int64(trimmed) catch { + let parsed = @string.parse_int64(trimmed) catch { _ => { errors.push(ctx + " must be an integer") return None @@ -730,7 +730,7 @@ fn expand_matrix_includes( } row_index += 1 } - if not(matched) { + if !(matched) { expanded.push(matrix_row_merge({}, include_row)) } } @@ -754,7 +754,7 @@ fn filter_matrix_excludes( break } } - if not(excluded) { + if !(excluded) { filtered.push(row) } } @@ -1223,7 +1223,7 @@ fn parse_trigger(value : YamlValue?, errors : Array[String]) -> ParsedTriggers { let has_push_like = yaml_obj_get(fields, "push") is Some(_) || yaml_obj_get(fields, "pull_request") is Some(_) || yaml_obj_get(fields, "pull_request_target") is Some(_) - if not(has_push_like) && workflow_call is None { + if !(has_push_like) && workflow_call is None { errors.push("only push and workflow_call triggers are supported in MVP") } { diff --git a/src/actrun/trigger.mbt b/src/actrun/trigger.mbt index 597b2bd..8537450 100644 --- a/src/actrun/trigger.mbt +++ b/src/actrun/trigger.mbt @@ -63,7 +63,7 @@ fn has_non_ignored_path( changed_paths : Array[String], ) -> Bool { for path in changed_paths { - if not(matches_any_pattern(ignored_patterns, path)) { + if !(matches_any_pattern(ignored_patterns, path)) { return true } } @@ -73,7 +73,7 @@ fn has_non_ignored_path( ///| pub fn matches_push_trigger(trigger : PushTrigger, event : PushEvent) -> Bool { if trigger.tags.length() > 0 && - not(matches_any_pattern(trigger.tags, event.ref_name)) { + !(matches_any_pattern(trigger.tags, event.ref_name)) { return false } if trigger.tags_ignore.length() > 0 && @@ -81,7 +81,7 @@ pub fn matches_push_trigger(trigger : PushTrigger, event : PushEvent) -> Bool { return false } if trigger.branches.length() > 0 && - not(matches_any_pattern(trigger.branches, event.ref_name)) { + !(matches_any_pattern(trigger.branches, event.ref_name)) { return false } if trigger.branches_ignore.length() > 0 && @@ -89,12 +89,12 @@ pub fn matches_push_trigger(trigger : PushTrigger, event : PushEvent) -> Bool { return false } if trigger.paths.length() > 0 && - not(has_matching_changed_path(trigger.paths, event.changed_paths)) { + !(has_matching_changed_path(trigger.paths, event.changed_paths)) { return false } if trigger.paths_ignore.length() > 0 && event.changed_paths.length() > 0 && - not(has_non_ignored_path(trigger.paths_ignore, event.changed_paths)) { + !(has_non_ignored_path(trigger.paths_ignore, event.changed_paths)) { return false } true diff --git a/src/adapters/mars_http/api_routes.mbt b/src/adapters/mars_http/api_routes.mbt index 8989078..71a4685 100644 --- a/src/adapters/mars_http/api_routes.mbt +++ b/src/adapters/mars_http/api_routes.mbt @@ -194,7 +194,7 @@ fn register_api_routes(app : @mars.Server, api : @core.ApiState) -> Unit { let ok = api.add_comment(issue_id, author, body, timestamp) let resp : Map[String, Json] = {} resp.set("ok", ok.to_json()) - if not(ok) { + if !(ok) { resp.set("error", "issue not found".to_json()) } ctx.json(resp.to_json()) diff --git a/src/adapters/mars_http/helpers.mbt b/src/adapters/mars_http/helpers.mbt index 9d65f27..74abb04 100644 --- a/src/adapters/mars_http/helpers.mbt +++ b/src/adapters/mars_http/helpers.mbt @@ -90,7 +90,7 @@ fn query_param(raw_path : String, key : String) -> String? { None => None Some(query) => { for pair_view in query.split("&") { - let pair = pair_view.to_string() + let pair = pair_view.to_owned() if pair.length() == 0 { continue } diff --git a/src/adapters/mars_http/home_page.mbt b/src/adapters/mars_http/home_page.mbt index 7223580..fb6534d 100644 --- a/src/adapters/mars_http/home_page.mbt +++ b/src/adapters/mars_http/home_page.mbt @@ -157,7 +157,7 @@ fn render_home_page() -> String { #| render_layout("bithub", [ @luna.raw_html(""), - @luna.raw_html( + @luna.raw_html(( #|
#|

bithub

#|

Browse and collaborate on any GitHub repository.

@@ -188,7 +188,7 @@ fn render_home_page() -> String { #| #|

Recent workspaces

#|
- ), + )), ]) + home_js } @@ -277,11 +277,24 @@ fn render_workspace_spa(ws : String) -> String { #| const ta=document.getElementById('editor'); #| ta.addEventListener('keydown',e=>{if(e.key==='Tab'){e.preventDefault();const s=ta.selectionStart,end=ta.selectionEnd;ta.value=ta.value.substring(0,s)+' '+ta.value.substring(end);ta.selectionStart=ta.selectionEnd=s+2;}}); #|} + #|function simpleHash(s){let h=0;for(let i=0;i>>0).toString(16).padStart(8,'0');} #|async function saveEdit(){ #| const ta=document.getElementById('editor'); #| if(!ta)return; #| await idbPut(KEY_PFX+_editPath,ta.value); - #| navigate(subPath(),false); + #| await promptCommit(_editPath); + #|} + #|async function promptCommit(path){ + #| const msg=prompt('Commit message:','Edit '+path); + #| if(!msg){navigate(subPath(),false);return;} + #| const sha=simpleHash(msg+Date.now()+Math.random()); + #| const headSha=await idbGet('ws:'+WS_ID+':HEAD')||''; + #| const commit={sha,message:msg,author:'anonymous',date:new Date().toISOString(),parent:headSha,files:[{path,action:'modify'}]}; + #| await idbPut('ws:'+WS_ID+':commit:'+sha,JSON.stringify(commit)); + #| await idbPut('ws:'+WS_ID+':HEAD',sha); + #| const c=document.getElementById('spa-content'); + #| c.innerHTML='
Committed '+esc(sha.slice(0,8))+' — '+esc(msg)+'
'; + #| setTimeout(()=>navigate(subPath(),false),1500); #|} #| #|// --- Commits --- @@ -340,4 +353,3 @@ fn render_workspace_spa(ws : String) -> String { ], ) + spa_css + spa_js } - diff --git a/src/adapters/mars_http/layout.mbt b/src/adapters/mars_http/layout.mbt index 24002c9..7f111c7 100644 --- a/src/adapters/mars_http/layout.mbt +++ b/src/adapters/mars_http/layout.mbt @@ -245,7 +245,7 @@ fn render_diff_patch(patch : String) -> @luna.Node[Unit, String] { } let lines : Array[@luna.Node[Unit, String]] = [] for line_view in patch.split("\n") { - let line = line_view.to_string() + let line = line_view.to_owned() let cls = if line.has_prefix("+") { "diff-add" } else if line.has_prefix("-") { diff --git a/src/adapters/mars_http/moon.pkg b/src/adapters/mars_http/moon.pkg index d7c918d..ec5fd99 100644 --- a/src/adapters/mars_http/moon.pkg +++ b/src/adapters/mars_http/moon.pkg @@ -1,6 +1,8 @@ +supported_targets = "js" + import { "mizchi/luna" @luna, - "mizchi/luna/x/components" @components, + "bit-vcs/bithub/adapters/ui_components" @components, "mizchi/mars" @mars, "mizchi/markdown" @markdown, "mizchi/sol" @sol, @@ -8,5 +10,4 @@ import { } options( - "supported-targets": "js", ) diff --git a/src/adapters/mars_http/pkg.generated.mbti b/src/adapters/mars_http/pkg.generated.mbti index 8868d8b..47045a1 100644 --- a/src/adapters/mars_http/pkg.generated.mbti +++ b/src/adapters/mars_http/pkg.generated.mbti @@ -2,12 +2,17 @@ package "bit-vcs/bithub/adapters/mars_http" import { + "bit-vcs/bithub/core", "mizchi/mars", } // Values pub fn create_app() -> @mars.Server +pub fn create_app_from_repo(String) -> @mars.Server + +pub fn create_app_with_storage(@core.ApiState) -> @mars.Server + // Errors // Types and methods diff --git a/src/adapters/ui_components/components.mbt b/src/adapters/ui_components/components.mbt new file mode 100644 index 0000000..49b0491 --- /dev/null +++ b/src/adapters/ui_components/components.mbt @@ -0,0 +1,112 @@ +///| Semantic HTML primitives used by bithub's server-rendered views. +///| +/// The former `mizchi/luna/x/components` package is not present in the +/// current Luna release line. Keeping the small surface bithub needs here +/// decouples its public views from that removed package. + +///| +pub fn[E] link( + href : String, + children : Array[@luna.Node[E, String]], +) -> @luna.Node[E, String] { + @luna.h("a", [("href", @luna.attr_static(href))], children) +} + +///| +pub fn[E] banner( + children : Array[@luna.Node[E, String]], +) -> @luna.Node[E, String] { + @luna.h("header", [], children) +} + +///| +pub fn[E] nav( + aria_label? : String, + children : Array[@luna.Node[E, String]], +) -> @luna.Node[E, String] { + let attrs : Array[(String, @luna.Attr[E, String])] = [] + if aria_label is Some(label) { + attrs.push(("aria-label", @luna.attr_static(label))) + } + @luna.h("nav", attrs, children) +} + +///| +pub fn[E] main_content( + children : Array[@luna.Node[E, String]], +) -> @luna.Node[E, String] { + @luna.h("main", [], children) +} + +///| +pub fn[E] contentinfo( + children : Array[@luna.Node[E, String]], +) -> @luna.Node[E, String] { + @luna.h("footer", [], children) +} + +///| +pub fn[E] region( + aria_label? : String, + children : Array[@luna.Node[E, String]], +) -> @luna.Node[E, String] { + let attrs : Array[(String, @luna.Attr[E, String])] = [] + if aria_label is Some(label) { + attrs.push(("aria-label", @luna.attr_static(label))) + } + @luna.h("section", attrs, children) +} + +///| +pub fn[E] breadcrumb_from_path( + base_path : String, + segments : Array[(String, String)], + current_label : String, +) -> @luna.Node[E, String] { + let items : Array[@luna.Node[E, String]] = [] + let mut path = base_path + for entry in segments { + let (segment, label) = entry + path = path + "/" + segment + items.push(@luna.h("li", [], [link(path, [@luna.text(label)])])) + } + items.push( + @luna.h( + "li", + [], + [ + @luna.h( + "a", + [ + ("href", @luna.attr_static("#")), + ("aria-current", @luna.attr_static("page")), + ], + [@luna.text(current_label)], + ), + ], + ), + ) + @luna.h( + "nav", + [("aria-label", @luna.attr_static("Breadcrumb"))], + [@luna.h("ol", [], items)], + ) +} + +///| +pub fn[E] tree( + id? : String, + aria_label? : String, + tree_nodes : Array[@luna.Node[E, String]], +) -> @luna.Node[E, String] { + let attrs : Array[(String, @luna.Attr[E, String])] = [ + ("role", @luna.attr_static("tree")), + ] + if id is Some(value) { + attrs.push(("id", @luna.attr_static(value))) + } + if aria_label is Some(label) { + attrs.push(("aria-label", @luna.attr_static(label))) + } + @luna.h("ul", attrs, tree_nodes) +} diff --git a/src/adapters/ui_components/moon.pkg b/src/adapters/ui_components/moon.pkg new file mode 100644 index 0000000..8ad89e5 --- /dev/null +++ b/src/adapters/ui_components/moon.pkg @@ -0,0 +1,5 @@ +import { + "mizchi/luna" @luna, +} + +supported_targets = "js" diff --git a/src/artifacts_api.mjs b/src/artifacts_api.mjs new file mode 100644 index 0000000..8d89a8c --- /dev/null +++ b/src/artifacts_api.mjs @@ -0,0 +1,103 @@ +const REPOSITORY_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/; +const SHA1_PATTERN = /^[a-f0-9]{40}$/i; + +function jsonError(error, status) { + return Response.json({ ok: false, error }, { status }); +} + +function timingSafeEqual(left, right) { + if (left.length !== right.length) return false; + let mismatch = 0; + for (let index = 0; index < left.length; index += 1) { + mismatch |= left.charCodeAt(index) ^ right.charCodeAt(index); + } + return mismatch === 0; +} + +function hasBearerToken(request, expectedToken) { + const authorization = (request.headers.get('authorization') ?? '').trim(); + const matched = authorization.match(/^Bearer\s+(.+)$/i); + const token = matched?.[1]?.trim(); + return typeof token === 'string' && timingSafeEqual(token, expectedToken); +} + +function boundedInteger(value, defaultValue, min, max) { + if (value === null) return defaultValue; + if (!/^(0|[1-9][0-9]*)$/.test(value)) return null; + const parsed = Number(value); + if (!Number.isSafeInteger(parsed) || parsed < min || parsed > max) return null; + return parsed; +} + +function validRef(value) { + return value.length > 0 && value.length <= 256 && !/[\u0000-\u001f\u007f]/.test(value); +} + +function isArtifactsPath(pathname) { + return pathname === '/api/artifacts/repos' || pathname.startsWith('/api/artifacts/repos/'); +} + +/** + * Read-only adapter from bithub HTTP routes to the Cloudflare Artifacts binding. + * It intentionally never mints or returns a Git credential. + */ +export async function handleArtifactsApi(request, env) { + const url = new URL(request.url); + if (!isArtifactsPath(url.pathname)) return null; + if (request.method !== 'GET') return jsonError('method not allowed', 405); + + const expectedToken = (env?.BITHUB_ARTIFACTS_READ_TOKEN ?? '').trim(); + if (expectedToken.length === 0) { + return jsonError('artifacts browse API is not configured', 503); + } + if (!hasBearerToken(request, expectedToken)) return jsonError('unauthorized', 401); + if (!env?.ARTIFACTS) return jsonError('artifacts binding is not configured', 503); + + const listLimit = boundedInteger(url.searchParams.get('limit'), 20, 1, 100); + if (listLimit === null) return jsonError('limit must be an integer between 1 and 100', 400); + + try { + if (url.pathname === '/api/artifacts/repos') { + const cursor = url.searchParams.get('cursor') || undefined; + const page = await env.ARTIFACTS.list({ limit: listLimit, cursor }); + return Response.json({ ok: true, repos: page.repos, cursor: page.cursor ?? null }); + } + + const logMatch = url.pathname.match(/^\/api\/artifacts\/repos\/([A-Za-z0-9][A-Za-z0-9._-]{0,63})\/log$/); + const commitMatch = url.pathname.match( + /^\/api\/artifacts\/repos\/([A-Za-z0-9][A-Za-z0-9._-]{0,63})\/commits\/([^/]+)$/, + ); + const treeMatch = url.pathname.match( + /^\/api\/artifacts\/repos\/([A-Za-z0-9][A-Za-z0-9._-]{0,63})\/trees\/([^/]+)$/, + ); + + if (!logMatch && !commitMatch && !treeMatch) return jsonError('not found', 404); + const repository = (logMatch ?? commitMatch ?? treeMatch)[1]; + if (!REPOSITORY_NAME_PATTERN.test(repository)) return jsonError('invalid repository name', 400); + + if (logMatch) { + const ref = url.searchParams.get('ref') ?? 'main'; + if (!validRef(ref)) return jsonError('invalid ref', 400); + const limit = boundedInteger(url.searchParams.get('limit'), 20, 1, 100); + const offset = boundedInteger(url.searchParams.get('offset'), 0, 0, 10_000); + if (limit === null) return jsonError('limit must be an integer between 1 and 100', 400); + if (offset === null) return jsonError('offset must be an integer between 0 and 10000', 400); + const repo = await env.ARTIFACTS.get(repository); + const log = await repo.log({ ref, limit, offset }); + return Response.json({ ok: true, repo: repository, log }); + } + + const hash = (commitMatch ?? treeMatch)[2]; + if (!SHA1_PATTERN.test(hash)) return jsonError('invalid object hash', 400); + const repo = await env.ARTIFACTS.get(repository); + if (commitMatch) { + const commit = await repo.readCommit(hash); + return Response.json({ ok: true, repo: repository, commit }); + } + const tree = await repo.readTree(hash); + return Response.json({ ok: true, repo: repository, tree }); + } catch { + // A binding exception can mean an absent, non-ready, or inaccessible repo. + return jsonError('artifacts repository is unavailable', 502); + } +} diff --git a/src/cmd/bithub/hub_reader.mbt b/src/cmd/bithub/hub_reader.mbt index 41dcc7b..a8a97e8 100644 --- a/src/cmd/bithub/hub_reader.mbt +++ b/src/cmd/bithub/hub_reader.mbt @@ -1,7 +1,7 @@ ///| pub fn load_repo_issues(root : String, limit : Int) -> Array[IssueSummary] { let git_dir = @npath.join2(root, ".git") - if not(@nfs.existsSync(git_dir)) { + if !(@nfs.existsSync(git_dir)) { return [] } let os_fs = JsOsFs::new() @@ -31,8 +31,8 @@ fn JsOsFs::new() -> JsOsFs { } ///| -fn io_error(err : @fs.IOError) -> @bit.GitError { - @bit.GitError::IoError(err.to_string()) +fn io_error(_err : @fs.IOError) -> @bit.GitError { + @bit.GitError::IoError("filesystem operation failed") } ///| @@ -44,7 +44,7 @@ fn ensure_dir(path : String) -> Unit raise @bit.GitError { let parts = path.split("/").collect() let mut current = "" for part_view in parts { - let part = part_view.to_string() + let part = part_view.to_owned() if part.length() == 0 { current = "/" continue @@ -55,7 +55,7 @@ fn ensure_dir(path : String) -> Unit raise @bit.GitError { current + "/" + part } let exists = @fs.is_dir(current) catch { _ => false } - if not(exists) { + if !(exists) { @fs.create_dir(current) catch { err => raise io_error(err) } diff --git a/src/cmd/bithub/main.mbt b/src/cmd/bithub/main.mbt index 7e4942f..445d66e 100644 --- a/src/cmd/bithub/main.mbt +++ b/src/cmd/bithub/main.mbt @@ -1,6 +1,6 @@ ///| fn parse_port(raw : String) -> Int { - let parsed = @strconv.parse_int(raw) catch { _ => 8787 } + let parsed = @string.parse_int(raw) catch { _ => 8787 } if parsed > 0 { parsed } else { @@ -210,7 +210,7 @@ fn trim_trailing_slashes(path : String) -> String { ///| fn derive_catalog_name(path : String) -> String { - let normalized = trim_trailing_slashes(path.trim(chars=" \n\r\t").to_string()) + let normalized = trim_trailing_slashes(path.trim(chars=" \n\r\t").to_owned()) if normalized == "." { return "." } @@ -223,7 +223,7 @@ fn derive_catalog_name(path : String) -> String { ///| fn parse_catalog_line(line : String) -> (String, String)? { - let normalized = line.trim(chars=" \n\r\t").to_string() + let normalized = line.trim(chars=" \n\r\t").to_owned() if normalized.length() == 0 || normalized.has_prefix("#") { return None } @@ -235,11 +235,11 @@ fn parse_catalog_line(line : String) -> (String, String)? { start=i + 1, end=normalized.length(), ) - let path = raw_path.trim(chars=" \n\r\t").to_string() + let path = raw_path.trim(chars=" \n\r\t").to_owned() if path.length() == 0 { return None } - let name = raw_name.trim(chars=" \n\r\t").to_string() + let name = raw_name.trim(chars=" \n\r\t").to_owned() if name.length() == 0 { Some((derive_catalog_name(path), path)) } else { @@ -257,7 +257,7 @@ fn parse_catalog_line(line : String) -> (String, String)? { pub fn parse_catalog_text(text : String) -> Array[(String, String)] { let entries : Array[(String, String)] = [] for line_view in text.split("\n") { - let line = line_view.to_string() + let line = line_view.to_owned() match parse_catalog_line(line) { Some(entry) => entries.push(entry) None => () @@ -321,7 +321,7 @@ pub struct RelayPeer { ///| fn parse_relay_base_url(raw : String) -> String? { - let trimmed = trim_trailing_slashes(raw.trim(chars=" \n\r\t").to_string()) + let trimmed = trim_trailing_slashes(raw.trim(chars=" \n\r\t").to_owned()) if trimmed.length() == 0 { return None } @@ -359,7 +359,7 @@ fn is_sender_safe_char(c : Char) -> Bool { ///| fn normalize_relay_sender(raw : String) -> String { - let trimmed = raw.trim(chars=" \n\r\t").to_string() + let trimmed = raw.trim(chars=" \n\r\t").to_owned() let out = StringBuilder::new() for c in trimmed { if is_sender_safe_char(c) { @@ -396,7 +396,7 @@ fn resolve_relay_sender(options : CliOptions) -> String { fn resolve_relay_auth_token() -> String? { match @nprocess.env().get("BIT_RELAY_AUTH_TOKEN") { Some(token) => { - let normalized = token.trim(chars=" \n\r\t").to_string() + let normalized = token.trim(chars=" \n\r\t").to_owned() if normalized.length() == 0 { None } else { @@ -456,7 +456,7 @@ fn relay_payload_url(payload : Map[String, Json]) -> String { ///| fn relay_payload_name(payload : Map[String, Json], sender : String) -> String { let raw = json_get_string(payload, "name", "") - if raw.trim(chars=" \n\r\t").to_string().length() == 0 { + if raw.trim(chars=" \n\r\t").to_owned().length() == 0 { sender } else { raw @@ -480,7 +480,7 @@ pub fn parse_relay_nodes_from_poll(text : String) -> Array[RelayPeer] { for item in items { guard item is Json::Object(envelope) else { continue } let raw_sender = json_get_string(envelope, "sender", "") - if raw_sender.trim(chars=" \n\r\t").to_string().length() == 0 { + if raw_sender.trim(chars=" \n\r\t").to_owned().length() == 0 { continue } let sender = normalize_relay_sender(raw_sender) @@ -488,11 +488,11 @@ pub fn parse_relay_nodes_from_poll(text : String) -> Array[RelayPeer] { continue } let kind = relay_payload_kind(payload) - if not(is_bithub_presence_kind(kind)) { + if !(is_bithub_presence_kind(kind)) { continue } let url = relay_payload_url(payload) - if not(is_http_url(url)) { + if !(is_http_url(url)) { continue } let name = relay_payload_name(payload, sender) @@ -680,7 +680,7 @@ fn announce_relay_presence( viewer_url : String, name : String, ) -> Unit { - if not(is_http_url(viewer_url)) { + if !(is_http_url(viewer_url)) { return } relay_publish_presence_async( @@ -740,7 +740,7 @@ fn start_p2p_tunnel( host : String, on_public_url : (String) -> Unit, ) -> @nchild.ChildProcess? { - if not(options.enable_p2p) { + if !(options.enable_p2p) { return None } let local_url = "http://\{host}:\{options.port}" @@ -818,7 +818,7 @@ fn route_repo_response(state : RepoState, raw_url : String) -> PageResponse { ///| fn parse_catalog_repo_route(raw_url : String) -> (String, String)? { - if not(raw_url.has_prefix("/repo/")) { + if !(raw_url.has_prefix("/repo/")) { return None } let rest = String::unsafe_substring(raw_url, start=6, end=raw_url.length()) diff --git a/src/cmd/bithub/moon.pkg b/src/cmd/bithub/moon.pkg index fc35d5f..230bc27 100644 --- a/src/cmd/bithub/moon.pkg +++ b/src/cmd/bithub/moon.pkg @@ -1,20 +1,24 @@ +supported_targets = "js" + import { - "mizchi/bit" @bit, - "mizchi/bit/lib" @bitlib, - "mizchi/bit/x/hub" @hub, + "mizchi/bit_core" @bit, + "mizchi/bit_lib" @bitlib, + "mizchi/bit_object" @object, + "mizchi/bit_types" @types, + "mizchi/bitx_hub" @hub, "mizchi/js/core" @js, "mizchi/js/node/child_process" @nchild, "mizchi/js/node/fs" @nfs, "mizchi/js/node/path" @npath, "mizchi/js/node/process" @nprocess, "mizchi/luna" @luna, - "mizchi/luna/x/components" @components, + "bit-vcs/bithub/adapters/ui_components" @components, "mizchi/mars" @mars, "mizchi/markdown" @markdown, "mizchi/markdown/syntree_api" @mdsyntree, "mizchi/sol" @sol, "moonbitlang/core/json" @json, - "moonbitlang/core/strconv" @strconv, + "moonbitlang/core/string" @string, "moonbitlang/x/fs", } @@ -23,7 +27,6 @@ import { } for "test" options( - "supported-targets": "js", is_main: true, link: { "js": { "format": "cjs" } }, targets: { diff --git a/src/cmd/bithub/repo_scan.mbt b/src/cmd/bithub/repo_scan.mbt index 4585869..cb96c9d 100644 --- a/src/cmd/bithub/repo_scan.mbt +++ b/src/cmd/bithub/repo_scan.mbt @@ -55,7 +55,7 @@ fn scan_repo_files_recursive( if out.length() >= limit { return } - if not(should_skip_name(name)) { + if !(should_skip_name(name)) { let rel_path = if rel_dir.length() == 0 { name } else { diff --git a/src/cmd/bithub/viewer.mbt b/src/cmd/bithub/viewer.mbt index 1a6baf6..9eecd42 100644 --- a/src/cmd/bithub/viewer.mbt +++ b/src/cmd/bithub/viewer.mbt @@ -196,7 +196,7 @@ pub fn RepoState::render_layout( ///| fn normalize_route_base(route_base : String) -> String { - let trimmed = route_base.trim(chars=" \n\r\t").to_string() + let trimmed = route_base.trim(chars=" \n\r\t").to_owned() if trimmed.length() == 0 || trimmed == "/" { return "" } @@ -601,7 +601,7 @@ fn build_tree_nodes( match classify_child_path(path, prefix) { Some((name, is_dir)) => if is_dir { - if not(dir_names.contains(name)) { + if !(dir_names.contains(name)) { dir_names.push(name) } } else { @@ -781,12 +781,12 @@ fn classify_child_path(path : String, prefix : String) -> (String, Bool)? { if parts.length() == 0 { return None } - let name = parts[0].to_string() + let name = parts[0].to_owned() return Some((name, parts.length() > 1)) } let base = prefix + "/" - if not(path.has_prefix(base)) { + if !(path.has_prefix(base)) { return None } let rest = String::unsafe_substring( @@ -798,7 +798,7 @@ fn classify_child_path(path : String, prefix : String) -> (String, Bool)? { if parts.length() == 0 { return None } - let name = parts[0].to_string() + let name = parts[0].to_owned() Some((name, parts.length() > 1)) } diff --git a/src/cmd/bithub/viewer_test.mbt b/src/cmd/bithub/viewer_test.mbt index 5f378ea..38cd789 100644 --- a/src/cmd/bithub/viewer_test.mbt +++ b/src/cmd/bithub/viewer_test.mbt @@ -1,9 +1,9 @@ ///| test "sanitize_rel_path blocks traversal" { - inspect(sanitize_rel_path("README.md"), content="Some(\"README.md\")") - inspect(sanitize_rel_path("/src/main.mbt"), content="Some(\"src/main.mbt\")") - inspect(sanitize_rel_path("../secret"), content="None") - inspect(sanitize_rel_path("a/../../b"), content="None") + debug_inspect(sanitize_rel_path("README.md"), content="Some(\"README.md\")") + debug_inspect(sanitize_rel_path("/src/main.mbt"), content="Some(\"src/main.mbt\")") + debug_inspect(sanitize_rel_path("../secret"), content="None") + debug_inspect(sanitize_rel_path("a/../../b"), content="None") } ///| @@ -73,7 +73,7 @@ test "render_layout uses collapsible tree for filer" { assert_true(html.contains("class=\"file-tree-dir\"")) assert_true(html.contains("data-tree-path=\"src\"")) assert_true(html.contains("[*] main.mbt")) - assert_true(not(html.contains("[*] src/cmd/bithub/main.mbt"))) + assert_true(!(html.contains("[*] src/cmd/bithub/main.mbt"))) assert_true(html.contains(" Unit = + #| (path) => { + #| const fs = require("node:fs") + #| fs.rmSync(path, { recursive: true, force: true }) + #| fs.mkdirSync(path, { recursive: true }) + #| fs.writeFileSync(path + "/README.md", "# test fixture") + #| } + +///| +extern "js" fn remove_repo_scan_fixture(path : String) -> Unit = + #| (path) => { require("node:fs").rmSync(path, { recursive: true, force: true }) } + +///| +test "scan_repo_files includes files from an isolated fixture" { + let root = "/tmp/bithub-repo-scan-test" + create_repo_scan_fixture(root) + let files = scan_repo_files(root, 400) assert_true(files.contains("README.md")) + remove_repo_scan_fixture(root) } ///| diff --git a/src/cmd/bithub_bench/main.mbt b/src/cmd/bithub_bench/main.mbt index 20a565f..865c516 100644 --- a/src/cmd/bithub_bench/main.mbt +++ b/src/cmd/bithub_bench/main.mbt @@ -10,7 +10,7 @@ struct BenchResult { ///| fn parse_iterations(raw : String) -> Int { - let parsed = @strconv.parse_int(raw) catch { _ => 20 } + let parsed = @string.parse_int(raw) catch { _ => 20 } if parsed > 0 { parsed } else { diff --git a/src/cmd/bithub_bench/moon.pkg b/src/cmd/bithub_bench/moon.pkg index bb141f4..e852e96 100644 --- a/src/cmd/bithub_bench/moon.pkg +++ b/src/cmd/bithub_bench/moon.pkg @@ -1,12 +1,13 @@ +supported_targets = "js" + import { "bit-vcs/bithub/cmd/bithub" @bithub, "mizchi/js/builtins/date" @jdate, "mizchi/js/node/process" @nprocess, - "moonbitlang/core/strconv" @strconv, + "moonbitlang/core/string" @string, } options( - "supported-targets": "js", is_main: true, link: { "js": { "format": "cjs" } }, targets: { "main.mbt": [ "js" ] }, diff --git a/src/cmd/main/main.mbt b/src/cmd/main/main.mbt index 439355b..d23d33a 100644 --- a/src/cmd/main/main.mbt +++ b/src/cmd/main/main.mbt @@ -1,13 +1,13 @@ ///| -let lazy_app : Ref[@mars.Server?] = Ref::new(None) -let r2_initialized : Ref[Bool] = Ref::new(false) +let lazy_app : Ref[@mars.Server?] = Ref(None) +let r2_initialized : Ref[Bool] = Ref(false) ///| fn get_app(env : @mars.JsEnv) -> @mars.Server { match lazy_app.val { Some(app) => app None => { - let app = if has_r2_binding(env) && not(r2_initialized.val) { + let app = if has_r2_binding(env) && !(r2_initialized.val) { // Use R2-backed storage on Cloudflare Workers r2_initialized.val = true let r2 = @core.R2Storage::new("BITHUB_STORE") diff --git a/src/cmd/main/moon.pkg b/src/cmd/main/moon.pkg index c00d450..8a18800 100644 --- a/src/cmd/main/moon.pkg +++ b/src/cmd/main/moon.pkg @@ -1,3 +1,5 @@ +supported_targets = "js" + import { "moonbitlang/async/js_async" @js_async, "mizchi/mars", @@ -6,7 +8,6 @@ import { } options( - "supported-targets": "js", is_main: true, link: { "js": { "format": "esm", "exports": ["fetch"] } }, ) diff --git a/src/cmd/main_fetch_ssr/main.mbt b/src/cmd/main_fetch_ssr/main.mbt index bc20e59..ac1dcd3 100644 --- a/src/cmd/main_fetch_ssr/main.mbt +++ b/src/cmd/main_fetch_ssr/main.mbt @@ -1,6 +1,6 @@ ///| fn parse_port(raw : String) -> Int { - let parsed = @strconv.parse_int(raw) catch { _ => 4175 } + let parsed = @string.parse_int(raw) catch { _ => 4175 } if parsed > 0 { parsed } else { diff --git a/src/cmd/main_fetch_ssr/moon.pkg b/src/cmd/main_fetch_ssr/moon.pkg index ddb1ca0..37ada65 100644 --- a/src/cmd/main_fetch_ssr/moon.pkg +++ b/src/cmd/main_fetch_ssr/moon.pkg @@ -1,13 +1,14 @@ +supported_targets = "js" + import { "bit-vcs/bithub/cmd/main" @main_cmd, "mizchi/mars" @mars, "mizchi/js/node/process" @nprocess, "moonbitlang/async/js_async" @js_async, - "moonbitlang/core/strconv" @strconv, + "moonbitlang/core/string" @string, } options( - "supported-targets": "js", is_main: true, link: { "js": { "format": "cjs" } }, targets: { diff --git a/src/cmd/main_ssr/main.mbt b/src/cmd/main_ssr/main.mbt index 763013d..523ad8d 100644 --- a/src/cmd/main_ssr/main.mbt +++ b/src/cmd/main_ssr/main.mbt @@ -1,6 +1,6 @@ ///| fn parse_port(raw : String) -> Int { - let parsed = @strconv.parse_int(raw) catch { _ => 4174 } + let parsed = @string.parse_int(raw) catch { _ => 4174 } if parsed > 0 { parsed } else { diff --git a/src/cmd/main_ssr/moon.pkg b/src/cmd/main_ssr/moon.pkg index 02b721c..c2cee05 100644 --- a/src/cmd/main_ssr/moon.pkg +++ b/src/cmd/main_ssr/moon.pkg @@ -1,12 +1,13 @@ +supported_targets = "js" + import { "bit-vcs/bithub/adapters/mars_http" @mars_http, "mizchi/js/node/process" @nprocess, "mizchi/sol" @sol, - "moonbitlang/core/strconv" @strconv, + "moonbitlang/core/string" @string, } options( - "supported-targets": "js", is_main: true, link: { "js": { "format": "cjs" } }, targets: { diff --git a/src/core/api_state.mbt b/src/core/api_state.mbt index 654c77a..2702a97 100644 --- a/src/core/api_state.mbt +++ b/src/core/api_state.mbt @@ -12,109 +12,6 @@ pub struct FileLookup { content : String? } -///| -// Adapter: @bitfs.Fs -> @bitlib.WorkingTree -priv struct KvWorkingTree { - fs : @bitfs.Fs - backing_fs : &@bit.RepoFileSystem - write_fs : &@bit.FileSystem -} - -///| -impl @bitlib.WorkingTree for KvWorkingTree with read_file(self, path) { - self.fs.read_file(self.backing_fs, path) -} - -///| -impl @bitlib.WorkingTree for KvWorkingTree with write_file(self, path, data) { - self.fs.write_file(path, data) -} - -///| -impl @bitlib.WorkingTree for KvWorkingTree with remove_file(self, path) { - self.fs.remove_file(path) -} - -///| -impl @bitlib.WorkingTree for KvWorkingTree with is_file(self, path) { - self.fs.is_file(self.backing_fs, path) -} - -///| -impl @bitlib.WorkingTree for KvWorkingTree with is_dir(self, path) { - self.fs.is_dir(self.backing_fs, path) -} - -///| -impl @bitlib.WorkingTree for KvWorkingTree with readdir(self, path) { - self.fs.readdir(self.backing_fs, path) -} - -///| -impl @bitlib.WorkingTree for KvWorkingTree with is_dirty(self) { - self.fs.is_dirty() -} - -///| -impl @bitlib.WorkingTree for KvWorkingTree with rollback(self) { - self.fs.rollback() -} - -///| -impl @bitlib.WorkingTree for KvWorkingTree with get_working_files(self) { - self.fs.get_working_files() -} - -///| -impl @bitlib.WorkingTree for KvWorkingTree with snapshot( - self, - message, - author, - timestamp, -) { - let snapshot = self.fs.snapshot( - self.write_fs, - self.backing_fs, - message, - author, - timestamp, - ) - snapshot.commit_id -} - -///| -impl @bitlib.WorkingTree for KvWorkingTree with checkout(self, commit_id) { - self.fs.checkout_snapshot(self.backing_fs, commit_id) -} - -///| -// Adapter: filesystem object db -> @bitlib.ObjectStore -priv struct KvObjectStore { - backing_fs : &@bit.RepoFileSystem - write_fs : &@bit.FileSystem - git_dir : String -} - -///| -impl @bitlib.ObjectStore for KvObjectStore with get(self, id) { - let db = @bitlib.ObjectDb::load(self.backing_fs, self.git_dir) - db.get(self.backing_fs, id) -} - -///| -impl @bitlib.ObjectStore for KvObjectStore with put(self, obj_type, data) { - @bitlib.write_loose_object(self.write_fs, self.git_dir, obj_type, data) -} - -///| -impl @bitlib.ObjectStore for KvObjectStore with has(self, id) { - let db = @bitlib.ObjectDb::load(self.backing_fs, self.git_dir) catch { - _ => return false - } - let obj = db.get(self.backing_fs, id) catch { _ => return false } - obj is Some(_) -} - ///| pub struct ApiState { store : &Storage @@ -191,7 +88,7 @@ pub fn ApiState::list_entries( let entries : Array[FileEntry] = [] for name in names { let full_path = join_path(path, name) - entries.push({ name, path: full_path, is_dir: not(self.store.has(full_path)) }) + entries.push({ name, path: full_path, is_dir: !(self.store.has(full_path)) }) } entries } @@ -452,10 +349,10 @@ pub fn ApiState::list_branches(self : ApiState) -> Array[BranchEntry] { } } entries.sort_by(fn(a, b) { - if a.current && not(b.current) { + if a.current && !(b.current) { return -1 } - if not(a.current) && b.current { + if !(a.current) && b.current { return 1 } String::compare(a.name, b.name) @@ -504,7 +401,7 @@ pub fn ApiState::list_workflows(self : ApiState) -> Array[WorkflowEntry] { names.sort_by(fn(a, b) { String::compare(a, b) }) for name in names { let path = workflow_dir + "/" + name - if not(name.has_suffix(".yml") || name.has_suffix(".yaml")) { + if !(name.has_suffix(".yml") || name.has_suffix(".yaml")) { continue } match self.store.get(path) { @@ -683,7 +580,7 @@ fn collect_search_results( if results.length() >= max_results { return } - let line = line_view.to_string() + let line = line_view.to_owned() if string_contains(line, query) { results.push({ path: full_path, line_number: line_num, line }) } @@ -1386,7 +1283,7 @@ pub fn ApiState::blame( } let lines : Array[String] = [] for line_view in content.split("\n") { - lines.push(line_view.to_string()) + lines.push(line_view.to_owned()) } // Walk commits in reverse chronological order to find which commit // last touched each line. Simple approach: check if file changed in @@ -1533,7 +1430,7 @@ pub fn ApiState::compare_commits( Some(detail) => { commits.push(detail.commit) for df in detail.diff_files { - if not(seen.contains(df.path)) { + if !(seen.contains(df.path)) { seen.set(df.path, true) all_files.push(df) } @@ -1543,7 +1440,7 @@ pub fn ApiState::compare_commits( None => break } } - if not(found_base) && commits.length() == 0 { + if !(found_base) && commits.length() == 0 { return None } Some({ base_sha, head_sha, commits, all_files }) diff --git a/src/core/ci.mbt b/src/core/ci.mbt index eb7c8a0..70efac6 100644 --- a/src/core/ci.mbt +++ b/src/core/ci.mbt @@ -95,7 +95,7 @@ pub fn ApiState::process_relay_webhook( let workflow_dir = ".github/workflows" let names = self.store.list(workflow_dir) for name in names { - if not(name.has_suffix(".yml") || name.has_suffix(".yaml")) { + if !(name.has_suffix(".yml") || name.has_suffix(".yaml")) { continue } let path = workflow_dir + "/" + name @@ -483,7 +483,7 @@ fn deserialize_ci_step_run(text : String) -> CiStepRun? { return None } let idx = try { - @strconv.parse_int(idx_str) + @string.parse_int(idx_str) } catch { _ => return None } diff --git a/src/core/core_test.mbt b/src/core/core_test.mbt index 89c2e0b..88dac0f 100644 --- a/src/core/core_test.mbt +++ b/src/core/core_test.mbt @@ -49,8 +49,8 @@ test "ApiState exposes README markdown text" { test "ApiState lists filer entries as data" { let api = ApiState::new() let root = api.list_entries("") - assert_true(root.any(fn(entry) { entry.name == "README.md" && not(entry.is_dir) })) - assert_true(root.any(fn(entry) { entry.name == "moon.mod.json" && not(entry.is_dir) })) + assert_true(root.any(fn(entry) { entry.name == "README.md" && !(entry.is_dir) })) + assert_true(root.any(fn(entry) { entry.name == "moon.mod.json" && !(entry.is_dir) })) assert_true(root.any(fn(entry) { entry.name == "src" && entry.is_dir })) let core = api.list_entries("src/core") @@ -58,7 +58,7 @@ test "ApiState lists filer entries as data" { core.any(fn(entry) { entry.name == "api_state.mbt" && entry.path == "src/core/api_state.mbt" && - not(entry.is_dir) + !(entry.is_dir) }), ) } @@ -111,7 +111,9 @@ test "ApiState workflow_detail returns job and step info" { let build = detail.jobs[0] inspect(build.id, content="build") inspect(build.step_count, content="4") - inspect(build.steps[0].uses, content="Some(\"actions/checkout@v4\")") + debug_inspect(build.steps[0].uses, content=( + #|Some("actions/checkout@v4") + )) inspect(build.steps[1].name, content="Install dependencies") } @@ -123,16 +125,20 @@ test "ApiState workflow_detail returns None for missing file" { ///| test "extract_branch_from_relay_ref extracts branch name" { - inspect( + debug_inspect( extract_branch_from_relay_ref("refs/relay/incoming/main"), - content="Some(\"main\")", + content=( + #|Some("main") + ), ) - inspect( + debug_inspect( extract_branch_from_relay_ref("refs/relay/incoming/feature/foo"), - content="Some(\"feature/foo\")", + content=( + #|Some("feature/foo") + ), ) - inspect(extract_branch_from_relay_ref("refs/heads/main"), content="None") - inspect(extract_branch_from_relay_ref(""), content="None") + debug_inspect(extract_branch_from_relay_ref("refs/heads/main"), content="None") + debug_inspect(extract_branch_from_relay_ref(""), content="None") } ///| @@ -221,7 +227,7 @@ test "get_ci_run retrieves a specific run" { let json = make_relay_webhook("evt-020", "refs/relay/incoming/main") let _ = api.process_relay_webhook(json) let run = api.get_ci_run("evt-020") - assert_true(not(run is None)) + assert_true(!(run is None)) inspect(run.unwrap().workflow_name, content="CI") assert_true(api.get_ci_run("nonexistent") is None) } @@ -284,7 +290,7 @@ fn test_storage() -> &Storage { test "MapStorage: get/set/has/list" { let s = test_storage() assert_true(s.get("foo") is None) - assert_true(not(s.has("foo"))) + assert_true(!(s.has("foo"))) s.set_string("foo", "bar") assert_true(s.has("foo")) let bytes = s.get("foo").unwrap() @@ -300,13 +306,19 @@ test "MapStorage: list returns child keys at one level" { s.set_string("z", "4") let children = s.list("a") children.sort_by(fn(a, b) { String::compare(a, b) }) - inspect(children, content="[\"b\", \"x\"]") + debug_inspect(children, content=( + #|["b", "x"] + )) let grandchildren = s.list("a/b") grandchildren.sort_by(fn(a, b) { String::compare(a, b) }) - inspect(grandchildren, content="[\"c\", \"d\"]") + debug_inspect(grandchildren, content=( + #|["c", "d"] + )) let root = s.list("") root.sort_by(fn(a, b) { String::compare(a, b) }) - inspect(root, content="[\"a\", \"z\"]") + debug_inspect(root, content=( + #|["a", "z"] + )) } ///| @@ -319,7 +331,7 @@ test "FsStorage: get/set/has/list round-trip" { rm_rf(dir) let s : &Storage = FsStorage::new(dir) assert_true(s.get("foo") is None) - assert_true(not(s.has("foo"))) + assert_true(!(s.has("foo"))) s.set_string("foo", "bar") assert_true(s.has("foo")) let bytes = s.get("foo").unwrap() @@ -328,9 +340,13 @@ test "FsStorage: get/set/has/list round-trip" { s.set_string("a/b/c", "nested") assert_true(s.has("a/b/c")) let children = s.list("a") - inspect(children, content="[\"b\"]") + debug_inspect(children, content=( + #|["b"] + )) let grandchildren = s.list("a/b") - inspect(grandchildren, content="[\"c\"]") + debug_inspect(grandchildren, content=( + #|["c"] + )) } ///| diff --git a/src/core/git_loader.mbt b/src/core/git_loader.mbt index c633990..7a51781 100644 --- a/src/core/git_loader.mbt +++ b/src/core/git_loader.mbt @@ -98,7 +98,7 @@ pub fn write_file_and_commit( write_file_to_disk(full_path, content) let dir = parent_dir(full_path) git_exec(dir, "git add " + full_path) - let author_arg = if author.length() > 0 && not(author.contains("<")) { + let author_arg = if author.length() > 0 && !(author.contains("<")) { " --author=\"" + author + " <" + author + "@bithub>\"" } else if author.length() > 0 { " --author=\"" + author + "\"" @@ -170,7 +170,7 @@ pub fn load_git_tags_into_storage( for name in names { match read_file_string(tag_dir + "/" + name) { Some(sha_raw) => { - let sha = sha_raw.trim_start(chars=" \n\r\t").trim_end(chars=" \n\r\t").to_string() + let sha = sha_raw.trim_start(chars=" \n\r\t").trim_end(chars=" \n\r\t").to_owned() store.set_string("git/tags/" + name, "sha=" + sha + "\nmessage=" + name) } None => () diff --git a/src/core/moon.pkg b/src/core/moon.pkg index 87fc548..fe81cc1 100644 --- a/src/core/moon.pkg +++ b/src/core/moon.pkg @@ -1,11 +1,11 @@ import { "bit-vcs/bithub/actrun" @actrun, - "mizchi/bit" @bit, - "mizchi/bit/lib" @bitlib, - "mizchi/bit/x/fs" @bitfs, - "mizchi/bit/x/hub" @hub, - "mizchi/bit/x/kv" @kv, + "mizchi/bit_core" @bit, + "mizchi/bit_lib" @bitlib, + "mizchi/bit_object" @object, + "mizchi/bit_types" @types, + "mizchi/bitx_hub" @hub, "moonbitlang/core/encoding/utf8" @utf8, "moonbitlang/core/json" @json, - "moonbitlang/core/strconv" @strconv, + "moonbitlang/core/string" @string, } diff --git a/src/core/pkg.generated.mbti b/src/core/pkg.generated.mbti index dd76634..5870b40 100644 --- a/src/core/pkg.generated.mbti +++ b/src/core/pkg.generated.mbti @@ -2,40 +2,105 @@ package "bit-vcs/bithub/core" import { + "mizchi/bit/object", + "mizchi/bit/types", "mizchi/bit/x/kv", } // Values pub fn extract_branch_from_relay_ref(String) -> String? +pub fn fire_spawn_request(CiSpawnConfig, CiSpawnRequest) -> Bool + +pub fn git_head_sha(String) -> String? + pub fn healthz_text() -> String pub fn home_text() -> String +pub fn load_files_into_storage(&Storage, String, Array[String]) -> Unit + +pub fn load_git_branches_into_storage(&Storage, &@types.RepoFileSystem, String) -> Unit + +pub fn load_git_commits_into_storage(&Storage, &@types.RepoFileSystem, String, max_count? : Int) -> Unit + +pub fn load_git_tags_into_storage(&Storage, &@types.RepoFileSystem, String) -> Unit + +pub fn load_relay_issues_into_storage(&Storage, String) -> Int + +pub fn load_workflow_files(&Storage, String) -> Unit + pub fn mars_route_specs() -> Array[RouteSpec] +pub fn parse_int_safe(String) -> Int + +pub fn scan_files(String, limit? : Int) -> Array[String] + +pub fn seed_files(&Storage) -> Unit + +pub fn simple_hash(String) -> String + +pub fn spawn_request_to_env_json(CiSpawnRequest, CiSpawnConfig) -> String + +pub fn write_file_and_commit(String, String, String, String) -> Unit + +pub fn xml_escape(String) -> String + // Errors // Types and methods +pub struct ActivityEvent { + kind : String + id : String + title : String + timestamp : String + detail : String +} + pub struct ApiState { store : &Storage + repo_root : String } +pub fn ApiState::activity_feed(Self, limit? : Int) -> Array[ActivityEvent] +pub fn ApiState::activity_rss(Self, String) -> String +pub fn ApiState::add_comment(Self, String, String, String, String) -> Bool +pub fn ApiState::add_pr_comment(Self, String, String, String, String) -> Bool pub fn ApiState::authenticate(Self, String?) -> AuthResult +pub fn ApiState::blame(Self, String) -> Array[BlameLine]? +pub fn ApiState::build_spawn_requests(Self, String) -> Array[CiSpawnRequest] +pub fn ApiState::close_pull_request(Self, String) -> Bool +pub fn ApiState::compare_commits(Self, String, String) -> CompareResult? +pub fn ApiState::create_issue(Self, String, String, String, String) -> String +pub fn ApiState::create_pull_request(Self, String, String, String, String, String, String) -> String +pub fn ApiState::edit_file(Self, String, String, String, String, String) -> FileEditResult pub fn ApiState::file_text(Self, String) -> String? +pub fn ApiState::from_repo(String) -> Self pub fn ApiState::get_ci_run(Self, String) -> CiRun? pub fn ApiState::get_ci_run_detail(Self, String) -> CiRunDetail? pub fn ApiState::get_commit(Self, String) -> CommitDetail? +pub fn ApiState::get_issue(Self, String) -> IssueDetail? +pub fn ApiState::get_pull_request(Self, String) -> PullRequestDetail? pub fn ApiState::list_branches(Self) -> Array[BranchEntry] pub fn ApiState::list_ci_runs(Self) -> Array[CiRun] pub fn ApiState::list_commits(Self) -> Array[CommitEntry] pub fn ApiState::list_entries(Self, String) -> Array[FileEntry] +pub fn ApiState::list_issues(Self) -> Array[IssueEntry] +pub fn ApiState::list_pr_comments(Self, String) -> Array[IssueComment] +pub fn ApiState::list_pull_requests(Self) -> Array[PullRequest] +pub fn ApiState::list_tags(Self) -> Array[TagEntry] pub fn ApiState::list_tokens(Self) -> Array[TokenInfo] +pub fn ApiState::list_webhooks(Self) -> Array[WebhookLogEntry] pub fn ApiState::list_workflows(Self) -> Array[WorkflowEntry] +pub fn ApiState::log_webhook(Self, String, String, String, String, Int) -> Unit pub fn ApiState::lookup_file(Self, String) -> FileLookup +pub fn ApiState::merge_pull_request(Self, String) -> Bool pub fn ApiState::new() -> Self pub fn ApiState::process_relay_webhook(Self, Json) -> Array[CiRun] pub fn ApiState::readme_markdown(Self) -> String +pub fn ApiState::repo_stats(Self) -> RepoStats pub fn ApiState::resolve_token(Self, String) -> TokenInfo? +pub fn ApiState::search(Self, String, max_results? : Int) -> Array[SearchResult] +pub fn ApiState::update_issue_state(Self, String, String) -> Bool pub fn ApiState::update_step_status(Self, String, String, Int, String, String) -> Bool pub fn ApiState::with_storage(&Storage) -> Self pub fn ApiState::workflow_detail(Self, String) -> WorkflowDetail? @@ -46,6 +111,13 @@ pub enum AuthResult { InvalidToken } +pub struct BlameLine { + line_number : Int + content : String + commit_sha : String + author : String +} + pub struct BranchEntry { name : String sha : String @@ -80,6 +152,30 @@ pub struct CiRunDetail { jobs : Array[CiJobRunDetail] } +pub(all) struct CiSpawnConfig { + orchestrator_url : String + auth_token : String + bithub_url : String + bithub_token : String + container_image : String +} + +pub struct CiSpawnRequest { + run_id : String + job_id : String + job_name : String + steps : Array[CiSpawnStep] + repo_url : String + ref_name : String +} + +pub struct CiSpawnStep { + index : Int + name : String + uses : String + run_cmd : String +} + pub struct CiStepRun { index : Int name : String @@ -103,12 +199,25 @@ pub struct CommitEntry { timestamp : String } +pub struct CompareResult { + base_sha : String + head_sha : String + commits : Array[CommitEntry] + all_files : Array[DiffFileEntry] +} + pub struct DiffFileEntry { path : String kind : String patch : String } +pub struct FileEditResult { + ok : Bool + commit_sha : String + error : String +} + pub struct FileEntry { name : String path : String @@ -121,25 +230,106 @@ pub struct FileLookup { content : String? } +pub struct FsStorage { + root : String +} +pub fn FsStorage::new(String) -> Self +pub impl Storage for FsStorage + +pub struct IdbGitFs { + prefix : String +} +pub fn IdbGitFs::new(prefix? : String) -> Self +pub impl @types.FileSystem for IdbGitFs +pub impl @types.RepoFileSystem for IdbGitFs + +pub struct IdbGitRepo { + fs : IdbGitFs + root : String + git_dir : String +} +pub fn IdbGitRepo::add_and_commit(Self, Array[String], String, String) -> @object.ObjectId raise @object.GitError +pub fn IdbGitRepo::head_commit(Self) -> @object.ObjectId? +pub fn IdbGitRepo::init(Self) -> Unit raise @object.GitError +pub fn IdbGitRepo::new(workspace? : String) -> Self +pub fn IdbGitRepo::read_file(Self, String) -> String? +pub fn IdbGitRepo::write_file(Self, String, String) -> Unit raise @object.GitError + +pub struct IssueComment { + author : String + body : String + created_at : String +} + +pub struct IssueDetail { + issue : IssueEntry + comments : Array[IssueComment] +} + +pub struct IssueEntry { + id : String + title : String + state : String + body : String + author : String + created_at : String +} + pub struct MapStorage { data : Map[String, String] } pub fn MapStorage::new() -> Self +pub impl Storage for MapStorage pub struct MemoryStorage { kv : @kv.Kv } pub fn MemoryStorage::new_seeded() -> Self +pub struct NodeFs { + _dummy : Int +} +pub fn NodeFs::new() -> Self + +pub struct PullRequest { + id : String + title : String + state : String + author : String + base_ref : String + head_ref : String + created_at : String +} + +pub struct PullRequestDetail { + pr : PullRequest + body : String + compare : CompareResult? +} + +pub struct R2Storage { + bucket_name : String +} +pub fn R2Storage::new(String) -> Self +pub impl Storage for R2Storage + +pub struct RepoStats { + file_count : Int + commit_count : Int + branch_count : Int + issue_count : Int + pr_count : Int + contributors : Array[(String, Int)] +} + pub enum Role { Owner Write Read -} +} derive(Eq) pub fn Role::can_admin(Self) -> Bool pub fn Role::can_write(Self) -> Bool pub fn Role::to_string(Self) -> String -pub impl Eq for Role pub struct RouteSpec { id : String @@ -147,11 +337,31 @@ pub struct RouteSpec { path : String } +pub struct SearchResult { + path : String + line_number : Int + line : String +} + +pub struct TagEntry { + name : String + sha : String + message : String +} + pub struct TokenInfo { identity : String role : Role } +pub struct WebhookLogEntry { + id : String + event_type : String + source : String + timestamp : String + triggered : Int +} + pub struct WorkflowDetail { filename : String name : String @@ -185,9 +395,9 @@ pub struct WorkflowStepSummary { // Traits pub(open) trait Storage { - get(Self, String) -> Bytes? - set_string(Self, String, String) -> Unit - list(Self, String) -> Array[String] - has(Self, String) -> Bool + fn get(Self, String) -> Bytes? + fn set_string(Self, String, String) -> Unit + fn list(Self, String) -> Array[String] + fn has(Self, String) -> Bool } diff --git a/src/core/storage.mbt b/src/core/storage.mbt index 9d51d3b..d3ba1d8 100644 --- a/src/core/storage.mbt +++ b/src/core/storage.mbt @@ -12,48 +12,37 @@ pub(open) trait Storage { has(Self, String) -> Bool } -///| MemoryStorage wraps the existing @kv.Kv for in-memory use. +///| MemoryStorage is the in-memory implementation used by demo pages. pub struct MemoryStorage { - kv : @kv.Kv + data : Map[String, String] } ///| impl Storage for MemoryStorage with get(self, key) { - self.kv.get(key) + match self.data.get(key) { + Some(value) => Some(@utf8.encode(value)) + None => None + } } ///| impl Storage for MemoryStorage with set_string(self, key, value) { - self.kv.set_string(key, value) + self.data.set(key, value) } ///| impl Storage for MemoryStorage with list(self, prefix) { - self.kv.list(prefix) + list_map_keys(self.data, prefix) } ///| impl Storage for MemoryStorage with has(self, key) { - self.kv.has(key) + self.data.contains(key) } ///| Create a MemoryStorage with seeded demo data. pub fn MemoryStorage::new_seeded() -> MemoryStorage { - let fs = @bit.TestFs::new() - let bit_fs = @bitfs.Fs::empty("/repo/.git") - let tree : KvWorkingTree = { fs: bit_fs, backing_fs: fs, write_fs: fs } - let store : KvObjectStore = { - backing_fs: fs, - write_fs: fs, - git_dir: "/repo/.git", - } - let kv = @kv.Kv::new( - @kv.NodeId::new("bithub"), - tree, - store, - @bit.ObjectId::zero(), - ) - let mem : MemoryStorage = { kv, } + let mem : MemoryStorage = { data: Map([]) } seed_files(mem) mem } @@ -84,6 +73,11 @@ pub impl Storage for MapStorage with set_string(self, key, value) { ///| pub impl Storage for MapStorage with list(self, prefix) { + list_map_keys(self.data, prefix) +} + +///| +fn list_map_keys(data : Map[String, String], prefix : String) -> Array[String] { let result : Array[String] = [] let seen : Map[String, Bool] = {} let prefix_with_slash = if prefix.length() == 0 { @@ -91,7 +85,7 @@ pub impl Storage for MapStorage with list(self, prefix) { } else { prefix + "/" } - for key, _ in self.data { + for key, _ in data { if prefix_with_slash.length() == 0 || key.has_prefix(prefix_with_slash) { let rest = String::unsafe_substring( key, @@ -105,7 +99,7 @@ pub impl Storage for MapStorage with list(self, prefix) { Some(i) => String::unsafe_substring(rest, start=0, end=i) None => rest } - if not(seen.contains(name)) { + if !(seen.contains(name)) { seen.set(name, true) result.push(name) } @@ -123,7 +117,7 @@ pub impl Storage for MapStorage with has(self, key) { fn parse_kv_fields(text : String) -> Map[String, String] { let fields : Map[String, String] = {} for line_view in text.split("\n") { - let line = line_view.to_string() + let line = line_view.to_owned() if line.length() == 0 { continue } @@ -141,4 +135,4 @@ fn parse_kv_fields(text : String) -> Map[String, String] { } } fields -} \ No newline at end of file +} diff --git a/tests/artifacts_api.test.mjs b/tests/artifacts_api.test.mjs new file mode 100644 index 0000000..72f3084 --- /dev/null +++ b/tests/artifacts_api.test.mjs @@ -0,0 +1,147 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { handleArtifactsApi } from '../src/artifacts_api.mjs'; + +function createArtifactsBinding() { + const calls = []; + const repo = { + async log(options) { + calls.push({ method: 'log', options }); + return { commits: [{ hash: 'a'.repeat(40), message: 'Initial commit' }] }; + }, + async readCommit(hash) { + calls.push({ method: 'readCommit', hash }); + return { hash, tree: 'b'.repeat(40), message: 'Initial commit' }; + }, + async readTree(hash) { + calls.push({ method: 'readTree', hash }); + return { hash, entries: [{ path: 'README.md', type: 'blob' }] }; + }, + }; + return { + binding: { + async get(name) { + calls.push({ method: 'get', name }); + return repo; + }, + async list(options) { + calls.push({ method: 'list', options }); + return { repos: [{ name: 'demo', status: 'ready' }], cursor: null }; + }, + }, + calls, + }; +} + +const authorizedEnv = (binding) => ({ + ARTIFACTS: binding, + BITHUB_ARTIFACTS_READ_TOKEN: 'bithub-reader-secret', +}); + +test('artifacts browse API is disabled until its dedicated read token is configured', async () => { + const { binding } = createArtifactsBinding(); + const response = await handleArtifactsApi( + new Request('https://bithub.example/api/artifacts/repos'), + { ARTIFACTS: binding }, + ); + + assert.equal(response.status, 503); + assert.deepEqual(await response.json(), { + ok: false, + error: 'artifacts browse API is not configured', + }); +}); + +test('artifacts browse API rejects callers without its dedicated bearer token', async () => { + const { binding } = createArtifactsBinding(); + const response = await handleArtifactsApi( + new Request('https://bithub.example/api/artifacts/repos'), + authorizedEnv(binding), + ); + + assert.equal(response.status, 401); + assert.deepEqual(await response.json(), { ok: false, error: 'unauthorized' }); +}); + +test('artifacts browse API lists repositories and reads commit, tree data without exposing a git token', async () => { + const { binding, calls } = createArtifactsBinding(); + const env = authorizedEnv(binding); + const headers = { authorization: 'Bearer bithub-reader-secret' }; + + const list = await handleArtifactsApi( + new Request('https://bithub.example/api/artifacts/repos?limit=25', { headers }), + env, + ); + assert.equal(list.status, 200); + assert.deepEqual(await list.json(), { + ok: true, + repos: [{ name: 'demo', status: 'ready' }], + cursor: null, + }); + + const log = await handleArtifactsApi( + new Request('https://bithub.example/api/artifacts/repos/demo/log?ref=main&limit=5&offset=2', { + headers, + }), + env, + ); + assert.equal(log.status, 200); + assert.deepEqual(await log.json(), { + ok: true, + repo: 'demo', + log: { commits: [{ hash: 'a'.repeat(40), message: 'Initial commit' }] }, + }); + + const commit = await handleArtifactsApi( + new Request(`https://bithub.example/api/artifacts/repos/demo/commits/${'a'.repeat(40)}`, { + headers, + }), + env, + ); + assert.equal(commit.status, 200); + assert.equal((await commit.json()).commit.tree, 'b'.repeat(40)); + + const tree = await handleArtifactsApi( + new Request(`https://bithub.example/api/artifacts/repos/demo/trees/${'b'.repeat(40)}`, { + headers, + }), + env, + ); + assert.equal(tree.status, 200); + assert.equal((await tree.json()).tree.entries[0].path, 'README.md'); + + assert.deepEqual(calls, [ + { method: 'list', options: { limit: 25, cursor: undefined } }, + { method: 'get', name: 'demo' }, + { method: 'log', options: { ref: 'main', limit: 5, offset: 2 } }, + { method: 'get', name: 'demo' }, + { method: 'readCommit', hash: 'a'.repeat(40) }, + { method: 'get', name: 'demo' }, + { method: 'readTree', hash: 'b'.repeat(40) }, + ]); +}); + +test('artifacts browse API validates repository, hash, and pagination inputs before using the binding', async () => { + const { binding, calls } = createArtifactsBinding(); + const env = authorizedEnv(binding); + const headers = { authorization: 'Bearer bithub-reader-secret' }; + + const invalidRepo = await handleArtifactsApi( + new Request('https://bithub.example/api/artifacts/repos/not%2Fa%2Frepo/log', { headers }), + env, + ); + assert.equal(invalidRepo.status, 404); + + const invalidHash = await handleArtifactsApi( + new Request('https://bithub.example/api/artifacts/repos/demo/commits/short', { headers }), + env, + ); + assert.equal(invalidHash.status, 400); + + const invalidLimit = await handleArtifactsApi( + new Request('https://bithub.example/api/artifacts/repos/demo/log?limit=101', { headers }), + env, + ); + assert.equal(invalidLimit.status, 400); + assert.deepEqual(calls, []); +}); diff --git a/vrt/SKILL.md b/vrt/SKILL.md new file mode 100644 index 0000000..6f44a67 --- /dev/null +++ b/vrt/SKILL.md @@ -0,0 +1,195 @@ +# VRT + Semantic Verification — Agent Skill Guide + +## 概要 + +Visual Regression Testing (VRT) とアクセシビリティセマンティクス検証を組み合わせた、 +コーディングエージェント向けの品質保証ツール。 + +変更が視覚的にもセマンティック(a11y)にも意図通りであることを自動検証し、 +リグレッションを検出・修復するループを回す。 + +## CLI コマンド + +すべて **プロジェクトルート** から実行する。サーバーが起動している前提。 + +```bash +# サーバー起動(別ターミナル) +just serve + +# 初回: ベースライン作成 +just vrt-init + +# 変更後: スナップショット取得 → 検証 +just vrt-capture +just vrt-verify +# または一括 +just vrt + +# 変更を承認: スナップショットを新ベースラインに昇格 +just vrt-approve + +# レポート確認 +just vrt-report + +# 影響範囲確認 +just vrt-affected +``` + +## エージェントのワークフロー + +### 基本ループ + +``` +┌─────────────────────────────────────────────┐ +│ 1. just vrt-init (初回のみ) │ +└─────────┬───────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────┐ +│ 2. コード変更を実施 │ +│ - commit message に意図を明記 │ +│ (feat: / fix: / style: / refactor: / │ +│ a11y: / deps:) │ +└─────────┬───────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────┐ +│ 3. just vrt │ +│ (= capture + verify) │ +└─────────┬───────────────────────────────────┘ + │ + ┌────┴────────────────┐ + │ │ + PASS FAIL/ESCALATE + │ │ + ▼ ▼ +┌──────────┐ ┌─────────────────────┐ +│ 4a. │ │ 4b. just vrt-report │ +│ approve │ │ → 問題を特定 │ +│ (任意) │ │ → コード修正 │ +└──────────┘ │ → 3 に戻る │ + └─────────────────────┘ +``` + +### 検証パイプライン(自動で実行される) + +``` +変更 ─→ 3 トラック並列実行: + +Track 1: Diff Intent — git diff + commit message → 変更意図を推測 +Track 2: Visual Diff — ピクセル比較 → ヒートマップ → 領域分類 +Track 3: A11y Diff — a11y ツリー差分 → セマンティック変化検出 + +→ Cross-Validation (3つの突き合わせ): + +| Visual | A11y | Intent | → 判定 | +|--------|-------|---------|----------------------| +| なし | なし | any | APPROVE (変化なし) | +| あり | あり | match | APPROVE (期待通り) | +| あり | あり | none | ESCALATE (意図不明) | +| あり | なし | style | APPROVE (見た目のみ) | +| あり | なし | refac | ESCALATE (意図しない) | +| なし | あり | a11y | APPROVE (a11y 改善) | +| なし | あり | other | REJECT (セマンティクス破壊) | +| any | regr | any | REJECT (a11y リグレ) | + +→ Quality Gate: + - 白飛び検出 (画面が真っ白) + - エラー状態検出 (赤い警告表示) + - 空コンテンツ検出 + - A11y リグレッション (ラベル消失、ランドマーク削除) + - VRT カバレッジ +``` + +## exit code + +| code | 意味 | +|------|------| +| 0 | PASS — 変化なし、または全て approved | +| 1 | FAIL — reject された変更あり、または品質エラー | + +escalate は exit 0 だが警告が出る。 + +## commit message の書き方 + +検証パイプラインは commit message から変更意図を推測する。 +意図が正しく推測されると、期待通りの視覚変化は自動承認される。 + +``` +feat: ダークモードトグル追加 → visual + a11y の追加が期待される +fix: モバイルでのレイアウト崩れ修正 → 修正対象のみの変化が期待される +refactor: ユーティリティ関数抽出 → visual/a11y ともに変化なしが期待される +style: ボタンの色を青→緑に変更 → visual 変化あり、a11y 変化なしが期待される +a11y: フォームにラベルを追加 → a11y 変化あり、visual 変化は最小限 +deps: React 19 にアップデート → visual/a11y ともに変化なしが期待される +``` + +## レポートの読み方 + +`just vrt-report` で出力される情報: + +``` +[APPROVE] home + Diff matches expected change: "color change" (confidence: 80%) + +[REJECT] settings + A11y regression detected: Removed button "Delete" + +[ESCALATE] profile + Visual change during refactor — appearance changed unexpectedly +``` + +- **APPROVE**: 期待通りの変更。問題なし。 +- **REJECT**: リグレッション検出。必ず修正が必要。 +- **ESCALATE**: 判断が分かれる変更。人間レビューまたは追加調査が必要。 + +## A11y チェックの活用 + +VRT verify は A11y ツリーも同時に検査する。以下が検出される: + +- ボタン/リンクにラベルがない (`label-missing`) +- 画像に alt テキストがない (`img-alt-missing`) +- ランドマーク要素の削除 (`landmark-changed`) +- インタラクティブ要素の削除 (`node-removed`) +- role の不適切な変更 (`role-changed`) + +リファクタリング中にこれらが検出された場合は、 +セマンティクスが壊れている可能性が高い。 + +## ファイル構成 + +``` +vrt/ +├── SKILL.md ← このファイル +├── package.json # 独立パッケージ +├── playwright.config.ts # VRT 用 Playwright 設定 +├── e2e/ +│ └── vrt-capture.spec.ts # スクリーンショット + a11y 収集 +├── src/ +│ ├── vrt-cli.ts # CLI エントリポイント +│ ├── types.ts # 全型定義 +│ ├── playwright-analyzer.ts # Playwright 出力解析 +│ ├── dep-graph.ts # 依存ツリー (TS/MoonBit/Rust) +│ ├── heatmap.ts # ピクセル比較 + ヒートマップ +│ ├── visual-semantic.ts # Visual Semantic Diff 分類 +│ ├── a11y-semantic.ts # A11y ツリー差分 + 品質チェック +│ ├── cross-validation.ts # Visual x A11y x Intent 突き合わせ +│ ├── intent.ts # Diff → 変更意図の推測 +│ ├── quality.ts # 品質ゲート +│ └── agent.ts # 5段階検証ループ +└── test-results/ # 実行結果 (gitignore 推奨) + ├── baselines/ # ベースライン PNG + a11y JSON + ├── snapshots/ # 最新スナップショット + ├── output/ # ヒートマップ等の出力 + └── vrt-report.json # 検証レポート +``` + +## トラブルシューティング + +| 問題 | 対処 | +|------|------| +| "Is the server running?" | `just serve` でサーバーを起動 | +| "No baselines found" | `just vrt-init` を実行 | +| フォントレンダリングの差分 | pixelmatch の threshold を調整 (heatmap.ts) | +| a11y ツリーが null | ページのレンダリング完了を待つ (waitFor 調整) | +| 全部 ESCALATE になる | commit message に prefix をつける (feat:/fix:/style: 等) | diff --git a/vrt/docs/architecture.md b/vrt/docs/architecture.md new file mode 100644 index 0000000..abfaa7a --- /dev/null +++ b/vrt/docs/architecture.md @@ -0,0 +1,163 @@ +# VRT + Semantic Verification — Architecture + +## 2層サイクル設計 + +```mermaid +graph TB + subgraph LongCycle["Long Cycle (spec.json)"] + direction TB + INTROSPECT["introspect
現在のUIからspec自動生成"] + SPEC["spec.json
不変条件 (invariants)"] + INTROSPECT --> SPEC + SPEC --> VERIFY_SPEC["verifySpec()
不変条件の検証"] + end + + subgraph ShortCycle["Short Cycle (expectation.json)"] + direction TB + EXP["expectation.json
このコミットで何が変わるか"] + EXP --> MATCH["matchA11yExpectation()
期待と実際の照合"] + MATCH --> CV["crossValidateWithExpectation()
Visual x A11y x Intent"] + end + + subgraph Pipeline["Verification Pipeline"] + direction TB + CAPTURE["capture
screenshots + a11y trees"] + DIFF["diff
baseline vs snapshot"] + CAPTURE --> DIFF + DIFF --> CV + DIFF --> VERIFY_SPEC + CV --> VERDICT["Unified Verdict"] + VERIFY_SPEC --> VERDICT + VERDICT --> SCORE["Score
usability / practicality / quality"] + end + + subgraph DepGraph["Dep Graph (コスト最適化)"] + CHANGED["Changed Files"] + GRAPH["Module Dependency Graph"] + CHANGED --> GRAPH + GRAPH --> SKIP{"Skip?"} + SKIP -->|unaffected| SKIP_CHECK["Skip invariant check"] + SKIP -->|affected| RUN_CHECK["Run check"] + SKIP -->|high-cost + unaffected| SKIP_NL["Skip NL assertion"] + end + + SKIP --> VERIFY_SPEC + + style LongCycle fill:#e8f5e9,stroke:#4CAF50 + style ShortCycle fill:#e8f4f8,stroke:#2196F3 + style Pipeline fill:#fff3e0,stroke:#FF9800 + style DepGraph fill:#f3e5f5,stroke:#9C27B0 +``` + +## 2層の役割分担 + +| | Short Cycle (expectation) | Long Cycle (spec) | +|---|---|---| +| **寿命** | 1コミット | 複数コミットにまたがる | +| **内容** | 「何が変わるか」 | 「何が常に成り立つべきか」 | +| **例** | "nav を消す" | "全ページに main ランドマークがある" | +| **regression** | 期待された regression を approve | 不変条件の violation を reject | +| **生成** | 人間 or エージェントが書く | introspect で自動生成 | +| **上書き** | spec の不変条件を一時的に上書き | expectation で上書き可能 | + +## Introspect フロー + +``` +現在の a11y ツリー + ↓ +introspect() + ↓ +PageIntrospection + - landmarks: banner, main, nav, ... + - interactive: 3 buttons, 5 links, 2 inputs + - unlabeled: 0 + - suggestedInvariants: [...] + ↓ +introspectToSpec() + ↓ +spec.json (UiSpec) + - pages: + - home: nav exists, all labeled, no whiteout + - about: main exists, heading h1 + - global: no whiteout, all labeled +``` + +## NL Assertion (将来) + +```typescript +// Playwright テスト内での使い方 (将来) +test("home page", async ({ page }) => { + await page.goto("/"); + + // 通常のアサーション (安価) + await expect(page.getByRole("heading")).toBeVisible(); + + // NL assertion (高価 — テスト失敗時のみ発火) + await nlAssert(page, "ナビゲーションバーに5つ以上のリンクがある", { + dependsOn: ["src/Header.tsx"], + onlyOnFailure: true, // 他のアサーションが落ちた時のみ実行 + }); +}); +``` + +### onlyOnFailure パターン + +```mermaid +graph LR + TEST["テスト実行"] --> PASS{"Pass?"} + PASS -->|Yes| DONE["完了"] + PASS -->|No| NL["NL Assertion 発火
(Vision LLM)"] + NL --> REPORT["修正ヒント付きレポート"] + REPORT --> AGENT["エージェントに投げる"] + AGENT --> FIX["修正 → 再テスト"] +``` + +### dep graph でのスキップ + +``` +変更ファイル: src/Footer.tsx + ↓ +dep graph 解析 + ↓ +src/Header.tsx は影響を受けない + ↓ +Header に依存する NL assertion はスキップ + ↓ +Footer に依存するアサーションのみ実行 +``` + +## コスト構造 + +| チェック種別 | コスト | 実行タイミング | +|---|---|---| +| ヒューリスティクス (whiteout, label) | 低 | 常に | +| ピクセル比較 (pixelmatch) | 中 | 常に | +| A11y ツリー diff | 低 | 常に | +| dep graph 解析 | 低 | 常に | +| Spec invariant 検証 | 低 | 影響ありの場合のみ | +| Visual Semantic 分類 | 低 | diff がある場合のみ | +| NL assertion (Vision LLM) | **高** | テスト失敗時 + 影響ありの場合のみ | +| LLM リーズニング | **高** | escalate された場合のみ | + +## ファイル構成 + +``` +vrt/ +├── expectation.json # Short cycle: このコミットの期待 +├── spec.json # Long cycle: 不変条件 (introspect で生成) +├── src/ +│ ├── types.ts # 全型定義 (UiSpec, NlAssertion 含む) +│ ├── introspect.ts # introspect + verifySpec +│ ├── expectation.ts # matchA11yExpectation + scoring +│ ├── cross-validation.ts # Visual x A11y x Intent +│ ├── dep-graph.ts # 依存ツリー (スキップ判定) +│ └── ... +├── fixtures/ +│ └── react-sample/ # 再現可能テスト fixtures +│ ├── baseline.a11y.json +│ ├── snapshot-nav-removed.a11y.json +│ └── snapshot-label-broken.a11y.json +└── docs/ + ├── pipeline.md # パイプライン図 + └── architecture.md # この文書 +``` diff --git a/vrt/docs/pipeline.md b/vrt/docs/pipeline.md new file mode 100644 index 0000000..530bed3 --- /dev/null +++ b/vrt/docs/pipeline.md @@ -0,0 +1,107 @@ +# VRT + Semantic Verification Pipeline + +## 全体設計 + +3つの独立した Diff ソースを並列に生成し、統合検証で突き合わせる。 + +```mermaid +graph TB + subgraph Input["入力ソース"] + GIT["Git Diff
(code change)"] + PW["Playwright Execution"] + end + + subgraph Parallel["並列パイプライン"] + direction TB + + subgraph Track_Intent["Track 1: Diff Intent"] + GIT --> PARSE_DIFF["Parse Unified Diff"] + PARSE_DIFF --> DEP_GRAPH["Dependency Graph
(TS / MoonBit / Rust)"] + DEP_GRAPH --> AFFECTED["Affected Components"] + PARSE_DIFF --> INTENT["Change Intent
ヒューリスティック or LLM"] + end + + subgraph Track_Visual["Track 2: Visual Semantic Diff"] + PW --> SCREENSHOT["Screenshots
(current + baseline)"] + SCREENSHOT --> PIXEL_DIFF["Pixel Diff
(pixelmatch)"] + PIXEL_DIFF --> HEATMAP["Heatmap +
Region Detection"] + HEATMAP --> VIS_SEM["Visual Semantic Diff
- 領域分類 (text/icon/layout/color)
- 変化の性質 (added/removed/moved/restyled)"] + end + + subgraph Track_A11y["Track 3: Accessibility Semantic Diff"] + PW --> A11Y_TREE["A11y Tree Snapshot
(current + baseline)"] + A11Y_TREE --> A11Y_DIFF["A11y Tree Diff
- ノード追加/削除/変更
- role, name, state 変化"] + A11Y_DIFF --> A11Y_SEM["A11y Semantic Diff
- ARIA 契約の検証
- ナビゲーション構造変化
- ラベル/ランドマーク整合性"] + end + end + + subgraph Merge["統合検証"] + VIS_SEM --> CROSS["Cross-Validation
Visual ↔ A11y 整合性チェック"] + A11Y_SEM --> CROSS + INTENT --> JUDGE["Unified Verdict Engine"] + AFFECTED --> JUDGE + CROSS --> JUDGE + JUDGE --> VERDICTS["Verdicts
(approve / reject / escalate)"] + end + + subgraph Quality["品質ゲート"] + VERDICTS --> QC["Quality Checks"] + QC --> WH["白飛び検出"] + QC --> ERR["エラー状態検出"] + QC --> COV["VRT + A11y カバレッジ"] + QC --> A11Y_REG["A11y リグレッション
(role欠損, label消失)"] + QC --> REPORT["Verification Report"] + end + + style Track_Intent fill:#e8f4f8,stroke:#2196F3 + style Track_Visual fill:#fff3e0,stroke:#FF9800 + style Track_A11y fill:#e8f5e9,stroke:#4CAF50 + style Merge fill:#f3e5f5,stroke:#9C27B0 + style Quality fill:#fce4ec,stroke:#E91E63 +``` + +## Cross-Validation マトリクス + +Visual Diff と A11y Diff の突き合わせで、変更の妥当性を判定する。 + +| Visual Diff | A11y Diff | Intent Match | 判定 | +|-------------|-----------|-------------|------| +| なし | なし | any | **Auto-approve** (変化なし) | +| あり | あり | あり | **Auto-approve** (期待通り) | +| あり | あり | なし | **Escalate** (意図しない変更) | +| あり | なし | style | **Approve** (見た目のみの変更、セマンティクス維持) | +| あり | なし | refactor | **Warning** (リファクタなのに見た目が変化) | +| なし | あり | any | **Reject** (見た目は同じだがセマンティクス破壊) | +| any | regression | any | **Reject** (A11y リグレッション) | + +## データフロー詳細 + +### Visual Semantic Diff + +ピクセル差分の「意味」を分類: +- **text-change**: テキスト領域の変化 (OCR ベースの検出) +- **color-change**: 色のみの変化 (形状は同一) +- **layout-shift**: 要素の位置移動 +- **element-added**: 新しい要素の出現 +- **element-removed**: 要素の消失 +- **icon-change**: アイコン/画像の変化 + +### Accessibility Semantic Diff + +A11y ツリーの構造差分: +- **node-added**: 新しい a11y ノード +- **node-removed**: ノードの消失 (リグレッション候補) +- **role-changed**: role 属性の変化 +- **name-changed**: accessible name の変化 +- **state-changed**: aria-* 状態の変化 +- **structure-changed**: ツリー構造の変化 (親子関係) +- **landmark-changed**: ランドマーク (