diff --git a/Plan.md b/Plan.md
index 7950e40..60a2753 100644
--- a/Plan.md
+++ b/Plan.md
@@ -322,3 +322,67 @@ integrity proof を追加します。
exact ASCII comparison と UTF-16 code-unit 順の versioned canonicalizer へ修正しました。
- 2026-07-13: forged resource、event、deployment、output、cross-scope proof、unsigned / malformed proof、
response-loss replay を含む focused 44 tests、root typecheck、textlint、architecture harness が成功しました。
+
+### [console Cloudscape 統一] - [2026-07-16]
+
+#### 目的
+
+Issue 9 の承認済み設計 `docs/design/2026-07-15-console-cloudscape.md` (選択肢 B) に従い、
+`apps/console` の表示層を Cloudscape Design System へ全面移行します。behavior
+(`model.ts` / `client.ts` / `loader.ts` / `launch-token.ts`) は維持したまま、view 層と
+view 層テストだけを刷新します。
+
+#### 制約
+
+- behavior テストは実 HTTP (`Bun.serve`) と実 SQLite のまま変更しません。view の
+ `renderToStaticMarkup` assertion だけを client-side rendering へ移送します。
+- `aria-busy` (loading)、role=alert (error)、launch token 秘匿 (`tc_sim_v1` 非表示)、
+ `useActionState` の pending 表示、`crypto.randomUUID()` の idempotency 既定値、
+ MissingProvider 診断、未知 status の pending フォールバックを維持します。
+- カバレッジ 100% と日本語 BDD スタイルを維持します。
+
+#### タスク
+
+- [x] spike: DOM 環境と `@testing-library/react` で Cloudscape 代表 component を Red-Green で描画します。
+- [x] shell: `AppLayout` + `TopNavigation` + `ContentLayout` へ骨格を移し、3 状態テストを client 版へ移送します。
+- [x] ready 本体: metrics、resource graph、operation form、outputs、diagnostics、event timeline を移行します。
+- [x] `styles.css` を console 固有の最小レイアウトへ削減します。
+- [x] gate: typecheck、test、coverage 100%、architecture-harness、biome、production build を緑にします。
+
+#### 検証手順
+
+`cd apps/console && bun run typecheck && bun test test && bun test --coverage test`、
+`bun scripts/architecture-harness.ts --fail-on=error`、`bun biome check apps/console`、
+`cd apps/console && bun run build` を順に実行します。
+
+#### 進捗ログ
+
+- 2026-07-16: 設計ドキュメントの未確定 2 点を判断しました。DOM 環境は Bun test での既知の
+ 実績とネットワーク実装を差し替えない構成を取りやすい happy-dom
+ (`@happy-dom/global-registrator`) を採用します。表示密度は resource を `Cards`
+ (入れ子の property category を per-item で展開表示するため)、event と diagnostics を
+ `Table` (列が均質で密度が高いため) で出すことにしました。
+- 2026-07-16: spike で Bun が CommonJS 依存を module graph の link 時に先行実行する挙動を確認しました。
+ `@testing-library/dom` の `screen` は import 時の document を束縛して壊れるため、view テストは
+ `render()` が返す query だけを使います。
+- 2026-07-16: react-dom が load 時に DOM の有無 (canUseDOM) で event system の経路を固定するため、
+ テストファイル内 import では Cloudscape Input の onChange が発火しない問題を特定しました。DOM 登録は
+ bunfig.toml の `[test].preload` (root と apps/console の両方) へ移し、setup がネットワーク・
+ ストリーム・WebSocket 実装を Bun native へ戻すことで、他 workspace の実 HTTP / 実 SQLite テストの
+ 経路を変えずに全体を緑にしました。
+- 2026-07-16: view.tsx を Cloudscape へ全面移行し、view テスト 10 件を client render で追加、
+ console.test.tsx の view assertion を削除して behavior テスト 9 件を維持しました。styles.css は
+ 857 行から 17 行 (sticky header と body margin のみ) へ削減し、apps/console のカバレッジ 100% を
+ 維持しました。apps/server の workload-runner import が console のカバレッジ集計へ漏れていた
+ 既存問題は、bunfig.toml の coveragePathIgnorePatterns へ `../../tools/**` を追加して解消しました。
+
+#### 振り返り
+
+- 問題: happy-dom の登録をテストファイルの import で行うと、CommonJS 依存の link 時実行より遅れて
+ React の event system が DOM なし経路へ固定され、controlled input の onChange が発火しない状態でも
+ form 送信テストが DOM 値経由で成功してしまいました。
+- 根本原因: Bun の CommonJS 先行実行と react-dom の load 時 canUseDOM 判定という 2 つの初期化順序の
+ 制約を、テスト成功という表面のシグナルだけでは検出できなかったためです。
+- 予防策: DOM 環境は必ず preload で登録し、controlled input のテストでは DOM の値ではなく state に
+ 接続された経路 (onChange 由来の再描画とカバレッジ) を確認します。カバレッジ 100% ゲートが
+ この問題を実際に検出したので、ゲートを維持します。
diff --git a/apps/console/bunfig.toml b/apps/console/bunfig.toml
index 6d68ba5..5a2e7d1 100644
--- a/apps/console/bunfig.toml
+++ b/apps/console/bunfig.toml
@@ -1,4 +1,8 @@
[test]
+# React (react-dom) は load 時に DOM の有無で event system の経路を固定する
+# CommonJS のため、view テストの DOM 環境 (happy-dom) はテストファイルの
+# import ではなく preload で全 module より先に登録する。
+preload = ["./test/dom-setup.ts"]
coverageThreshold = 1
coverageSkipTestFiles = true
-coveragePathIgnorePatterns = ["../../contracts/**", "../../core/**", "../../providers/**", "../api/**", "../server/**"]
+coveragePathIgnorePatterns = ["../../contracts/**", "../../core/**", "../../providers/**", "../../tools/**", "../api/**", "../server/**"]
diff --git a/apps/console/package.json b/apps/console/package.json
index e5f20cb..4c32a1f 100644
--- a/apps/console/package.json
+++ b/apps/console/package.json
@@ -11,11 +11,16 @@
"typecheck": "tsc --noEmit"
},
"dependencies": {
+ "@cloudscape-design/components": "3.0.1324",
+ "@cloudscape-design/global-styles": "1.0.62",
"@tenkacloud/simulator-contracts": "workspace:*",
"react": "19.2.7",
"react-dom": "19.2.7"
},
"devDependencies": {
+ "@happy-dom/global-registrator": "20.10.6",
+ "@testing-library/dom": "10.4.1",
+ "@testing-library/react": "16.3.2",
"@tenkacloud/simulator-api": "workspace:*",
"@tenkacloud/simulator-core": "workspace:*",
"@tenkacloud/simulator-provider-gcp": "workspace:*",
diff --git a/apps/console/src/main.tsx b/apps/console/src/main.tsx
index ba05be8..aec5b8d 100644
--- a/apps/console/src/main.tsx
+++ b/apps/console/src/main.tsx
@@ -1,3 +1,5 @@
+import '@cloudscape-design/global-styles/index.css';
+import { applyMode, Mode } from '@cloudscape-design/global-styles';
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { type ConsoleBootstrap, SimulatorConsoleApp } from './app';
@@ -5,6 +7,12 @@ import { SimulatorConsoleClient } from './client';
import { consumeLaunchToken } from './launch-token';
import './styles.css';
+// OS のカラースキーム設定に追従する。判定できない環境では旧 bespoke テーマと
+// 視覚的連続性のある dark を既定にする。
+const prefersLight =
+ window.matchMedia?.('(prefers-color-scheme: light)').matches ?? false;
+applyMode(prefersLight ? Mode.Light : Mode.Dark);
+
const root = document.getElementById('root');
if (!root) throw new Error('Console root element was not found');
diff --git a/apps/console/src/styles.css b/apps/console/src/styles.css
index 22dfc99..f262b18 100644
--- a/apps/console/src/styles.css
+++ b/apps/console/src/styles.css
@@ -1,857 +1,17 @@
-:root {
- color-scheme: dark;
- font-family:
- Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont,
- "Segoe UI", sans-serif;
- color: #e8eef8;
- background: #07101c;
- font-synthesis: none;
- text-rendering: optimizeLegibility;
-}
-
-* {
- box-sizing: border-box;
-}
+/*
+ * Console 固有の最小レイアウトのみを置く。
+ * 色・余白・タイポグラフィ・状態表現・レスポンシブはすべて
+ * Cloudscape Design System (@cloudscape-design/components と
+ * @cloudscape-design/global-styles) の design token に委ねる。
+ */
body {
- min-width: 320px;
- min-height: 100vh;
margin: 0;
- background:
- radial-gradient(
- circle at 20% -10%,
- rgba(46, 136, 255, 0.16),
- transparent 32rem
- ),
- radial-gradient(
- circle at 95% 20%,
- rgba(24, 207, 162, 0.08),
- transparent 30rem
- ),
- #07101c;
-}
-
-button,
-a,
-summary {
- -webkit-tap-highlight-color: transparent;
-}
-
-button,
-a {
- font: inherit;
}
-button:focus-visible,
-a:focus-visible,
-summary:focus-visible {
- outline: 3px solid #65b5ff;
- outline-offset: 3px;
-}
-
-code,
-pre {
- font-family: "SFMono-Regular", Consolas, "Liberation Mono", monospace;
-}
-
-.app-shell {
- min-height: 100vh;
-}
-
-.shell-header {
+/* AppLayout の headerSelector と対にする sticky ヘッダー領域。 */
+.console-header {
position: sticky;
- z-index: 20;
+ z-index: 1002;
top: 0;
- display: flex;
- align-items: center;
- justify-content: space-between;
- min-height: 68px;
- padding: 0 4vw;
- border-bottom: 1px solid rgba(150, 176, 214, 0.16);
- background: rgba(7, 16, 28, 0.86);
- backdrop-filter: blur(18px);
-}
-
-.brand {
- display: flex;
- align-items: center;
- gap: 10px;
- color: #f8fbff;
- font-size: 0.95rem;
- font-weight: 740;
- letter-spacing: -0.01em;
- text-decoration: none;
-}
-
-.brand-mark {
- display: grid;
- width: 32px;
- height: 32px;
- place-items: center;
- border: 1px solid rgba(101, 181, 255, 0.5);
- border-radius: 10px;
- color: #07101c;
- background: linear-gradient(140deg, #8bd0ff, #4ee7bd);
- box-shadow: 0 8px 26px rgba(62, 166, 255, 0.18);
-}
-
-.brand-product {
- padding-left: 10px;
- border-left: 1px solid #30415a;
- color: #95a8c5;
- font-weight: 560;
-}
-
-.protocol-badge,
-.stream-badge {
- display: inline-flex;
- align-items: center;
- gap: 8px;
- color: #aebdd3;
- font-family: "SFMono-Regular", Consolas, monospace;
- font-size: 0.72rem;
- letter-spacing: 0.04em;
- text-transform: uppercase;
-}
-
-.live-dot {
- width: 7px;
- height: 7px;
- border-radius: 50%;
- background: #3be0a5;
- box-shadow: 0 0 0 4px rgba(59, 224, 165, 0.12);
-}
-
-.console-main {
- width: min(1520px, 92vw);
- margin: 0 auto;
- padding: clamp(44px, 7vw, 92px) 0 72px;
-}
-
-.hero {
- display: flex;
- align-items: end;
- justify-content: space-between;
- gap: 40px;
- margin-bottom: 36px;
-}
-
-.eyebrow {
- margin: 0 0 10px;
- color: #68c9ff;
- font-size: 0.7rem;
- font-weight: 720;
- letter-spacing: 0.15em;
- text-transform: uppercase;
-}
-
-h1,
-h2,
-h3,
-h4,
-p {
- overflow-wrap: anywhere;
-}
-
-h1,
-h2,
-h3,
-h4 {
- margin: 0;
- color: #f5f8fd;
-}
-
-h1 {
- max-width: 880px;
- font-size: clamp(2.1rem, 5vw, 4.7rem);
- line-height: 0.98;
- letter-spacing: -0.055em;
-}
-
-h2 {
- font-size: clamp(1.18rem, 2vw, 1.5rem);
- letter-spacing: -0.025em;
-}
-
-h3 {
- font-size: 1rem;
- text-transform: capitalize;
-}
-
-h4 {
- margin-top: 4px;
- font-family: "SFMono-Regular", Consolas, monospace;
- font-size: 0.78rem;
- font-weight: 580;
- line-height: 1.45;
-}
-
-.hero-copy {
- max-width: 650px;
- margin: 20px 0 0;
- color: #95a8c5;
- font-size: clamp(0.96rem, 1.4vw, 1.1rem);
- line-height: 1.7;
-}
-
-.primary-button,
-.secondary-button {
- min-height: 44px;
- padding: 10px 16px;
- border-radius: 10px;
- cursor: pointer;
- transition:
- border-color 160ms ease,
- background 160ms ease,
- transform 160ms ease;
-}
-
-.primary-button {
- border: 1px solid #65b5ff;
- color: #06111f;
- background: #78c3ff;
- font-weight: 720;
-}
-
-.secondary-button {
- flex: none;
- border: 1px solid #344a68;
- color: #d9e5f6;
- background: rgba(19, 34, 54, 0.84);
-}
-
-.primary-button:hover,
-.secondary-button:hover {
- transform: translateY(-1px);
- border-color: #6bbcff;
- background: #8dccff;
- color: #07101c;
-}
-
-.metrics {
- display: grid;
- grid-template-columns: repeat(4, 1fr);
- margin-bottom: 24px;
- border: 1px solid rgba(138, 168, 207, 0.16);
- border-radius: 14px;
- background: rgba(13, 25, 42, 0.74);
- box-shadow: 0 24px 80px rgba(1, 7, 16, 0.24);
-}
-
-.metric {
- min-width: 0;
- padding: 19px 22px;
- border-right: 1px solid rgba(138, 168, 207, 0.12);
-}
-
-.metric:last-child {
- border-right: 0;
-}
-
-.metric span {
- display: block;
- margin-bottom: 8px;
- color: #8da1be;
- font-size: 0.68rem;
- letter-spacing: 0.1em;
- text-transform: uppercase;
-}
-
-.metric strong {
- display: block;
- overflow: hidden;
- color: #edf5ff;
- font-size: 1.2rem;
- text-overflow: ellipsis;
- text-transform: capitalize;
- white-space: nowrap;
-}
-
-.content-grid {
- display: grid;
- grid-template-columns: minmax(0, 1fr) minmax(280px, 340px);
- align-items: start;
- gap: 24px;
-}
-
-.side-column {
- display: grid;
- gap: 24px;
-}
-
-.operation-form {
- display: grid;
- gap: 16px;
- padding: 20px;
-}
-
-.operation-form > p {
- margin: 0;
- color: #91a6c2;
- font-size: 0.72rem;
- line-height: 1.6;
-}
-
-.operation-fields {
- display: grid;
- grid-template-columns: repeat(2, minmax(0, 1fr));
- gap: 12px;
-}
-
-.operation-field {
- display: grid;
- gap: 7px;
- min-width: 0;
-}
-
-.operation-field span {
- color: #91a6c2;
- font-size: 0.68rem;
-}
-
-.operation-field input,
-.operation-field textarea {
- width: 100%;
- border: 1px solid #304965;
- border-radius: 8px;
- color: #e7eff9;
- background: #071423;
- font:
- 0.72rem / 1.5 "SFMono-Regular",
- Consolas,
- monospace;
-}
-
-.operation-field input {
- min-height: 40px;
- padding: 8px 10px;
-}
-
-.operation-field textarea {
- resize: vertical;
- padding: 10px;
-}
-
-.operation-field input:focus-visible,
-.operation-field textarea:focus-visible {
- outline: 3px solid #65b5ff;
- outline-offset: 2px;
-}
-
-.operation-result {
- padding: 10px 12px;
- border: 1px solid rgba(57, 214, 160, 0.38);
- border-radius: 8px;
- color: #7df2c8;
- background: rgba(26, 119, 88, 0.17);
-}
-
-.operation-result-error {
- border-color: rgba(244, 112, 122, 0.4);
- color: #ff9aa5;
- background: rgba(140, 38, 52, 0.18);
-}
-
-.panel {
- overflow: hidden;
- border: 1px solid rgba(138, 168, 207, 0.17);
- border-radius: 14px;
- background: rgba(10, 22, 38, 0.86);
- box-shadow: 0 24px 70px rgba(0, 5, 14, 0.2);
-}
-
-.section-heading {
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 20px;
- padding: 22px 24px;
- border-bottom: 1px solid rgba(138, 168, 207, 0.13);
-}
-
-.section-heading .eyebrow {
- margin-bottom: 6px;
-}
-
-.cursor-badge,
-.count-badge {
- flex: none;
- padding: 5px 8px;
- border: 1px solid #2b4563;
- border-radius: 7px;
- color: #9db3d0;
- background: #0b1b2d;
- font-family: "SFMono-Regular", Consolas, monospace;
- font-size: 0.68rem;
-}
-
-.provider-grid {
- display: grid;
- grid-template-columns: repeat(auto-fit, minmax(min(100%, 350px), 1fr));
- gap: 1px;
- background: rgba(138, 168, 207, 0.12);
-}
-
-.provider-lane {
- min-width: 0;
- padding: 22px;
- background: #0a1626;
-}
-
-.provider-lane > header {
- display: flex;
- align-items: center;
- gap: 12px;
- margin-bottom: 16px;
-}
-
-.provider-lane > header p {
- margin: 4px 0 0;
- color: #8296b2;
- font-size: 0.72rem;
-}
-
-.provider-glyph {
- display: grid;
- width: 38px;
- height: 38px;
- flex: none;
- place-items: center;
- border: 1px solid #31577a;
- border-radius: 10px;
- color: #87d2ff;
- background: linear-gradient(145deg, #102d47, #0d2035);
- font-size: 0.68rem;
- font-weight: 760;
- letter-spacing: 0.04em;
-}
-
-.resource-list {
- display: grid;
- gap: 12px;
-}
-
-.resource-card {
- overflow: hidden;
- border: 1px solid rgba(142, 174, 214, 0.16);
- border-radius: 11px;
- background: linear-gradient(
- 145deg,
- rgba(20, 38, 61, 0.88),
- rgba(11, 25, 43, 0.96)
- );
-}
-
-.resource-heading {
- display: flex;
- align-items: start;
- justify-content: space-between;
- gap: 14px;
- padding: 16px;
-}
-
-.resource-type {
- margin: 0;
- color: #8bb5de;
- font-size: 0.68rem;
- font-weight: 680;
- letter-spacing: 0.035em;
-}
-
-.status {
- display: inline-flex;
- flex: none;
- align-items: center;
- padding: 4px 8px;
- border: 1px solid #3b5d76;
- border-radius: 999px;
- color: #b9c9dd;
- background: rgba(26, 48, 70, 0.8);
- font-size: 0.65rem;
- font-weight: 700;
- letter-spacing: 0.04em;
- text-transform: uppercase;
-}
-
-.status-ready,
-.status-running {
- border-color: rgba(57, 214, 160, 0.38);
- color: #7df2c8;
- background: rgba(26, 119, 88, 0.17);
-}
-
-.status-failed,
-.status-deleted {
- border-color: rgba(244, 112, 122, 0.4);
- color: #ff9aa5;
- background: rgba(140, 38, 52, 0.18);
-}
-
-.resource-meta {
- display: grid;
- grid-template-columns: auto minmax(0, 1fr);
- gap: 10px;
- padding: 10px 16px;
- border-top: 1px solid rgba(142, 174, 214, 0.11);
- color: #8397b3;
- font-size: 0.67rem;
-}
-
-.resource-meta code {
- overflow: hidden;
- color: #b9cce4;
- text-align: right;
- text-overflow: ellipsis;
- white-space: nowrap;
-}
-
-details {
- border-top: 1px solid rgba(142, 174, 214, 0.11);
-}
-
-summary {
- padding: 11px 16px;
- color: #9fb2cc;
- cursor: pointer;
- font-size: 0.7rem;
- font-weight: 700;
- letter-spacing: 0.055em;
- text-transform: uppercase;
-}
-
-.properties {
- margin: 0;
- border-top: 1px solid rgba(142, 174, 214, 0.08);
-}
-
-.property-row {
- display: grid;
- grid-template-columns: minmax(100px, 0.7fr) minmax(0, 1fr);
- gap: 12px;
- padding: 9px 16px;
- border-bottom: 1px solid rgba(142, 174, 214, 0.07);
- font-size: 0.7rem;
-}
-
-.property-row:last-child {
- border-bottom: 0;
-}
-
-.property-row dt {
- color: #7f94b1;
-}
-
-.property-row dd {
- min-width: 0;
- margin: 0;
- color: #d7e2f1;
- text-align: right;
- white-space: pre-wrap;
-}
-
-.property-row .code-value {
- font-family: "SFMono-Regular", Consolas, monospace;
- font-size: 0.64rem;
- line-height: 1.5;
-}
-
-.output-list {
- margin: 0;
-}
-
-.output-list > div {
- padding: 14px 20px;
- border-bottom: 1px solid rgba(142, 174, 214, 0.1);
-}
-
-.output-list > div:last-child {
- border-bottom: 0;
-}
-
-.output-list dt {
- margin-bottom: 6px;
- color: #8296b3;
- font-size: 0.68rem;
-}
-
-.output-list dd {
- margin: 0;
- color: #d9e7f7;
- font-family: "SFMono-Regular", Consolas, monospace;
- font-size: 0.72rem;
- line-height: 1.5;
- overflow-wrap: anywhere;
-}
-
-.diagnostic-list {
- display: grid;
- gap: 1px;
- margin: 0;
- padding: 0;
- background: rgba(142, 174, 214, 0.1);
- list-style: none;
-}
-
-.diagnostic-list li {
- padding: 16px 20px;
- background: #0b192a;
-}
-
-.diagnostic-list strong {
- color: #ffc67a;
- font-size: 0.75rem;
-}
-
-.diagnostic-list p {
- margin: 7px 0;
- color: #b6c5d8;
- font-size: 0.72rem;
- line-height: 1.55;
-}
-
-.diagnostic-list code {
- color: #8399b7;
- font-size: 0.65rem;
-}
-
-.empty-state {
- margin: 0;
- padding: 64px 24px;
- color: #8297b4;
- text-align: center;
-}
-
-.empty-state.compact {
- padding: 28px 20px;
- font-size: 0.75rem;
-}
-
-.events-panel {
- margin-top: 24px;
-}
-
-.timeline {
- margin: 0;
- padding: 8px 24px 24px;
- list-style: none;
-}
-
-.event-item {
- position: relative;
- display: grid;
- grid-template-columns: 34px minmax(0, 1fr);
- gap: 16px;
- padding: 18px 0;
-}
-
-.event-item::before {
- position: absolute;
- top: 50px;
- bottom: -18px;
- left: 16px;
- width: 1px;
- background: #29415e;
- content: "";
-}
-
-.event-item:last-child::before {
- display: none;
-}
-
-.event-sequence {
- display: grid;
- width: 34px;
- height: 34px;
- z-index: 1;
- place-items: center;
- border: 1px solid #36587b;
- border-radius: 50%;
- color: #7bcaff;
- background: #0a192a;
- font-family: "SFMono-Regular", Consolas, monospace;
- font-size: 0.66rem;
-}
-
-.event-heading {
- display: flex;
- align-items: baseline;
- justify-content: space-between;
- gap: 16px;
-}
-
-.event-heading strong {
- color: #e8f1fc;
- font-size: 0.83rem;
-}
-
-.event-heading time {
- color: #798eaa;
- font-family: "SFMono-Regular", Consolas, monospace;
- font-size: 0.62rem;
-}
-
-.event-command {
- margin: 5px 0 12px;
- color: #8da2bf;
- font-size: 0.68rem;
-}
-
-.event-item details {
- border: 1px solid rgba(142, 174, 214, 0.11);
- border-radius: 7px;
- background: rgba(7, 16, 28, 0.55);
-}
-
-.event-item pre {
- overflow: auto;
- max-height: 260px;
- margin: 0;
- padding: 14px 16px;
- border-top: 1px solid rgba(142, 174, 214, 0.1);
- color: #b9cce2;
- font-size: 0.68rem;
- line-height: 1.55;
-}
-
-.state-page {
- display: grid;
- width: min(680px, 88vw);
- min-height: calc(100vh - 132px);
- margin: 0 auto;
- place-content: center;
- justify-items: start;
-}
-
-.state-page h1 {
- margin-bottom: 18px;
- font-size: clamp(2.2rem, 6vw, 4.3rem);
-}
-
-.state-page > p:not(.eyebrow) {
- max-width: 520px;
- margin: 0 0 24px;
- color: #9aadc7;
- line-height: 1.7;
-}
-
-.loader {
- width: 34px;
- height: 34px;
- margin-bottom: 28px;
- border: 3px solid #243c59;
- border-top-color: #66c7ff;
- border-radius: 50%;
- animation: spin 800ms linear infinite;
-}
-
-.state-icon {
- display: grid;
- width: 44px;
- height: 44px;
- margin-bottom: 28px;
- place-items: center;
- border: 1px solid rgba(255, 122, 135, 0.45);
- border-radius: 13px;
- color: #ff9aa5;
- background: rgba(132, 34, 48, 0.2);
- font-weight: 800;
-}
-
-.shell-footer {
- display: flex;
- justify-content: space-between;
- gap: 24px;
- padding: 24px 4vw;
- border-top: 1px solid rgba(150, 176, 214, 0.13);
- color: #6f849f;
- font-size: 0.66rem;
- letter-spacing: 0.03em;
-}
-
-@keyframes spin {
- to {
- transform: rotate(360deg);
- }
-}
-
-@media (prefers-reduced-motion: reduce) {
- *,
- *::before,
- *::after {
- scroll-behavior: auto;
- animation-duration: 0.01ms;
- animation-iteration-count: 1;
- transition-duration: 0.01ms;
- }
-}
-
-@media (max-width: 980px) {
- .content-grid {
- grid-template-columns: 1fr;
- }
-
- .side-column {
- grid-template-columns: repeat(2, minmax(0, 1fr));
- }
-}
-
-@media (max-width: 700px) {
- .shell-header {
- padding: 0 20px;
- }
-
- .protocol-badge {
- font-size: 0;
- }
-
- .protocol-badge .live-dot {
- display: block;
- }
-
- .console-main {
- width: min(100% - 32px, 1520px);
- padding-top: 46px;
- }
-
- .hero {
- align-items: stretch;
- flex-direction: column;
- gap: 24px;
- }
-
- .metrics {
- grid-template-columns: repeat(2, 1fr);
- }
-
- .metric:nth-child(2) {
- border-right: 0;
- }
-
- .metric:nth-child(-n + 2) {
- border-bottom: 1px solid rgba(138, 168, 207, 0.12);
- }
-
- .side-column {
- grid-template-columns: 1fr;
- }
-
- .section-heading {
- padding: 18px;
- }
-
- .provider-lane {
- padding: 16px;
- }
-
- .event-heading {
- align-items: start;
- flex-direction: column;
- gap: 4px;
- }
-
- .shell-footer {
- align-items: start;
- flex-direction: column;
- padding: 24px 20px;
- }
}
diff --git a/apps/console/src/view.tsx b/apps/console/src/view.tsx
index fc35872..8399c61 100644
--- a/apps/console/src/view.tsx
+++ b/apps/console/src/view.tsx
@@ -1,10 +1,34 @@
-import type {
- JsonValue,
- SimulatorDiagnostic,
- SimulatorEvent,
- SimulatorResourceRecord,
+import Alert from '@cloudscape-design/components/alert';
+import AppLayout from '@cloudscape-design/components/app-layout';
+import Box from '@cloudscape-design/components/box';
+import Button from '@cloudscape-design/components/button';
+import Cards, { type CardsProps } from '@cloudscape-design/components/cards';
+import ColumnLayout from '@cloudscape-design/components/column-layout';
+import Container from '@cloudscape-design/components/container';
+import ContentLayout from '@cloudscape-design/components/content-layout';
+import ExpandableSection from '@cloudscape-design/components/expandable-section';
+import Form from '@cloudscape-design/components/form';
+import FormField from '@cloudscape-design/components/form-field';
+import Grid from '@cloudscape-design/components/grid';
+import Header from '@cloudscape-design/components/header';
+import Input from '@cloudscape-design/components/input';
+import KeyValuePairs from '@cloudscape-design/components/key-value-pairs';
+import SpaceBetween from '@cloudscape-design/components/space-between';
+import Spinner from '@cloudscape-design/components/spinner';
+import StatusIndicator, {
+ type StatusIndicatorProps,
+} from '@cloudscape-design/components/status-indicator';
+import Table, { type TableProps } from '@cloudscape-design/components/table';
+import Textarea from '@cloudscape-design/components/textarea';
+import TopNavigation from '@cloudscape-design/components/top-navigation';
+import {
+ type JsonValue,
+ SIMULATOR_PROTOCOL_VERSION,
+ type SimulatorDiagnostic,
+ type SimulatorEvent,
+ type SimulatorResourceRecord,
} from '@tenkacloud/simulator-contracts';
-import { useActionState, useRef } from 'react';
+import { useActionState, useRef, useState } from 'react';
import {
type ConsoleLoadState,
diagnostics,
@@ -48,43 +72,47 @@ export async function runConsoleOperationAction(
}
}
-export function ConsoleOperationResult({
- state,
-}: {
- readonly state: ConsoleOperationActionState;
-}): React.JSX.Element | null {
- if (state.kind === 'idle') return null;
- return (
-
- {state.message}
-
- );
+const STATUS_INDICATOR_TYPES: Readonly<
+ Record
+> = {
+ accepted: 'pending',
+ deleted: 'stopped',
+ deleting: 'in-progress',
+ deploying: 'in-progress',
+ failed: 'error',
+ pending: 'pending',
+ ready: 'success',
+ running: 'success',
+};
+
+export function statusIndicatorType(status: string): StatusIndicatorProps.Type {
+ return STATUS_INDICATOR_TYPES[status] ?? 'pending';
}
-function BrandMark(): React.JSX.Element {
+function ConsoleStatus({
+ status,
+}: {
+ readonly status: string;
+}): React.JSX.Element {
return (
-
- T
-
+
+ {status}
+
);
}
-function ShellHeader(): React.JSX.Element {
+export function ConsoleOperationResult({
+ state,
+}: {
+ readonly state: ConsoleOperationActionState;
+}): React.JSX.Element | null {
+ if (state.kind === 'idle') return null;
return (
-
+
);
}
@@ -94,12 +122,20 @@ function LoadingState({
readonly worldId: string;
}): React.JSX.Element {
return (
-
-
- World {worldId}
- Reading the event-sourced world
- Loading resources, deployment output, and the replay cursor.
-
+
+
+
+
+
+ World {worldId}
+ Reading the event-sourced world
+
+ Loading resources, deployment output, and the replay cursor.
+
+
+
+
+
);
}
@@ -113,95 +149,102 @@ function ErrorState({
readonly onRefresh: () => void;
}): React.JSX.Element {
return (
-
-
- !
-
- World {worldId}
- World unavailable
- {message}
-
-
- );
-}
-
-function Metric({
- label,
- value,
-}: {
- readonly label: string;
- readonly value: string | number;
-}): React.JSX.Element {
- return (
-
-
{label}
-
{value}
+
+
Try again}
+ header="World unavailable"
+ type="error"
+ >
+
+ World {worldId}
+ {message}
+
+
);
}
-function StatusBadge({
- status,
-}: {
- readonly status: string;
-}): React.JSX.Element {
- return
{status};
-}
-
-function JsonEntry({
- name,
+function PropertyValue({
value,
}: {
- readonly name: string;
readonly value: JsonValue;
}): React.JSX.Element {
const rendered = displayValue(value);
- return (
-
-
{name}
-
- {rendered}
-
-
- );
+ const variant = rendered.includes('\n') ? 'pre' : 'span';
+ return
{rendered};
}
-function ResourceCard({
+function ResourceCategories({
resource,
}: {
readonly resource: SimulatorResourceRecord;
}): React.JSX.Element {
return (
-
-
-
-
{resource.resourceType}
-
{resource.resourceId}
-
-
-
-
- Target
- {resource.targetId}
- Deployment
- {resource.deploymentId}
-
+
{propertyCategories(resource).map((category) => (
-
- {category.label}
-
- {category.entries.map(([name, value]) => (
-
- ))}
-
-
+
+ ({
+ label: name,
+ value: ,
+ }))}
+ />
+
))}
-
+
);
}
+function resourceKey(resource: SimulatorResourceRecord): string {
+ return `${resource.deploymentId}:${resource.targetId}:${resource.provider}:${resource.resourceType}:${resource.resourceId}`;
+}
+
+const RESOURCE_CARD_DEFINITION: CardsProps.CardDefinition
=
+ {
+ header: (resource) => (
+
+ {resource.resourceId}
+
+
+ ),
+ sections: [
+ {
+ id: 'type',
+ header: 'Type',
+ content: (resource) => (
+ {resource.resourceType}
+ ),
+ },
+ {
+ id: 'placement',
+ content: (resource) => (
+ {resource.targetId},
+ },
+ {
+ label: 'Deployment',
+ value: {resource.deploymentId},
+ },
+ ]}
+ />
+ ),
+ },
+ {
+ id: 'categories',
+ content: (resource) => ,
+ },
+ ],
+ };
+
function ResourceGraph({
resources,
}: {
@@ -209,245 +252,312 @@ function ResourceGraph({
}): React.JSX.Element {
const groups = groupResources({ resources });
if (groups.length === 0) {
- return No resources have been projected yet.
;
+ return (
+
+
+ No resources have been projected yet.
+
+ }
+ items={[]}
+ trackBy={resourceKey}
+ />
+
+ );
}
return (
-
+
{groups.map((group) => (
-
-
-
- {group.resources.map((resource) => (
-
- ))}
-
-
+
+ {group.provider}
+
+ }
+ key={group.provider}
+ >
+
+
))}
-
+
);
}
-function EventItem({
- event,
-}: {
- readonly event: SimulatorEvent;
-}): React.JSX.Element {
- return (
-
- {event.sequence}
-
-
- {event.type}
-
-
-
- {event.command.operation} · {event.command.id}
-
-
- Event payload
- {JSON.stringify(event.payload, null, 2)}
-
-
-
- );
-}
+const EVENT_COLUMN_DEFINITIONS: readonly TableProps.ColumnDefinition[] =
+ [
+ {
+ id: 'sequence',
+ header: 'Sequence',
+ cell: (event) => event.sequence,
+ },
+ {
+ id: 'type',
+ header: 'Type',
+ cell: (event) => {event.type},
+ },
+ {
+ id: 'timestamp',
+ header: 'Virtual timestamp',
+ cell: (event) => (
+
+ ),
+ },
+ {
+ id: 'command',
+ header: 'Command',
+ cell: (event) => (
+
+ {event.command.operation} · {event.command.id}
+
+ ),
+ },
+ {
+ id: 'payload',
+ header: 'Payload',
+ cell: (event) => (
+
+ {JSON.stringify(event.payload, null, 2)}
+
+ ),
+ },
+ ];
function EventTimeline({
events,
}: {
readonly events: readonly SimulatorEvent[];
}): React.JSX.Element {
- if (events.length === 0) {
- return No events exist after this cursor.
;
- }
return (
-
- {events.toReversed().map((event) => (
-
- ))}
-
+
+ No events exist after this cursor.
+
+ }
+ items={events.toReversed()}
+ trackBy={(event) => String(event.sequence)}
+ variant="embedded"
+ />
);
}
-function OutputList({
- outputs,
-}: {
- readonly outputs: Readonly>;
-}): React.JSX.Element {
- const entries = Object.entries(outputs);
- if (entries.length === 0) {
- return No deployment outputs.
;
- }
- return (
-
- {entries.map(([key, value]) => (
-
-
- {key}
- - {value}
-
- ))}
-
- );
+function diagnosticSource(diagnostic: SimulatorDiagnostic): string {
+ const line = diagnostic.source?.line ? `:${diagnostic.source.line}` : '';
+ return diagnostic.source ? `${diagnostic.source.file}${line}` : '—';
}
-function DiagnosticItem({
- diagnostic,
+const DIAGNOSTIC_COLUMN_DEFINITIONS: readonly TableProps.ColumnDefinition[] =
+ [
+ {
+ id: 'code',
+ header: 'Code',
+ cell: (diagnostic) => (
+ {diagnostic.code}
+ ),
+ },
+ {
+ id: 'message',
+ header: 'Message',
+ cell: (diagnostic) => diagnostic.message,
+ },
+ {
+ id: 'source',
+ header: 'Source',
+ cell: (diagnostic) => (
+ {diagnosticSource(diagnostic)}
+ ),
+ },
+ ];
+
+function diagnosticKey(diagnostic: SimulatorDiagnostic): string {
+ return `${diagnostic.code}:${diagnostic.provider}:${diagnostic.service}:${diagnostic.resourceType}:${diagnostic.operation}:${diagnostic.source?.file}:${diagnostic.source?.line}`;
+}
+
+function Diagnostics({
+ entries,
}: {
- readonly diagnostic: SimulatorDiagnostic;
+ readonly entries: readonly SimulatorDiagnostic[];
}): React.JSX.Element {
return (
-
- {diagnostic.code}
- {diagnostic.message}
- {diagnostic.source ? (
-
- {diagnostic.source.file}
- {diagnostic.source.line ? `:${diagnostic.source.line}` : ''}
-
- ) : null}
-
+ No deployment diagnostics.}
+ items={[...entries]}
+ trackBy={diagnosticKey}
+ variant="embedded"
+ />
);
}
-function Diagnostics({
- entries,
+function OutputList({
+ outputs,
}: {
- readonly entries: readonly SimulatorDiagnostic[];
+ readonly outputs: Readonly>;
}): React.JSX.Element {
+ const entries = Object.entries(outputs);
if (entries.length === 0) {
- return No deployment diagnostics.
;
+ return No deployment outputs.;
}
return (
-
- {entries.map((diagnostic) => (
-
- ))}
-
+ ({
+ label,
+ value: {value},
+ }))}
+ />
);
}
-function OperationField({
- label,
- name,
- defaultValue,
- placeholder,
-}: {
+interface OperationFormValues {
+ readonly provider: string;
+ readonly targetId: string;
+ readonly engine: string;
+ readonly service: string;
+ readonly resourceType: string;
+ readonly operation: string;
+ readonly input: string;
+ readonly idempotencyKey: string;
+}
+
+interface OperationFieldDefinition {
readonly label: string;
- readonly name: string;
- readonly defaultValue?: string;
+ readonly name: Exclude;
readonly placeholder?: string;
-}): React.JSX.Element {
- return (
-
- );
}
-function ProviderOperationPanel({
+const OPERATION_FIELD_DEFINITIONS: readonly OperationFieldDefinition[] = [
+ { label: 'Provider', name: 'provider', placeholder: 'gcp' },
+ { label: 'Target ID', name: 'targetId' },
+ { label: 'Engine', name: 'engine', placeholder: 'infra-manager' },
+ { label: 'Service', name: 'service', placeholder: 'run' },
+ {
+ label: 'Resource type',
+ name: 'resourceType',
+ placeholder: 'google_cloud_run_v2_service',
+ },
+ { label: 'Operation', name: 'operation', placeholder: 'UpdateService' },
+];
+
+function ProviderOperationForm({
deploymentId,
onOperation,
}: {
- readonly deploymentId?: string;
+ readonly deploymentId: string;
readonly onOperation: (formData: FormData) => Promise;
}): React.JSX.Element {
const idempotencyKey = useRef(`console-${crypto.randomUUID()}`).current;
+ const [fields, setFields] = useState({
+ provider: '',
+ targetId: 'default',
+ engine: '',
+ service: '',
+ resourceType: '',
+ operation: '',
+ input: '{}',
+ idempotencyKey,
+ });
const [actionState, formAction, pending] = useActionState(
runConsoleOperationAction.bind(null, onOperation),
INITIAL_OPERATION_STATE
);
+ const setField = (name: keyof OperationFormValues, value: string): void =>
+ setFields((current) => ({ ...current, [name]: value }));
return (
-
-
-
-
Shared command API
-
Provider operation
-
-
- {deploymentId ? (
-
+
+
+
+ );
+}
+
+function ProviderOperationPanel({
+ deploymentId,
+ onOperation,
+}: {
+ readonly deploymentId?: string;
+ readonly onOperation: (formData: FormData) => Promise;
+}): React.JSX.Element {
+ return (
+
+ Provider operation
+
+ }
+ >
+ {deploymentId ? (
+
) : (
-
+
Select a deployment in the Console URL before executing a command.
-
+
)}
-
+
);
}
@@ -464,93 +574,105 @@ function ReadyConsole({
const providers = groupResources(data.resources);
const deploymentDiagnostics = diagnostics(data.deployment);
return (
-
-
-
-
Deterministic world
-
{data.worldId}
-
- One source of truth for every provider, command, and projected
- resource.
-
-
-
-
-
-
-
-
-
Refresh projection}
+ description="One source of truth for every provider, command, and projected resource."
+ variant="h1"
>
-
-
-
Resource graph
-
Provider projections
-
-
cursor {data.cursor}
-
-
-
-
-
-
-
-
-
-
-
Replayable audit trail
-
Event timeline
-
-
- SSE replay
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+ ) : null
+ }
+ description="Deployment"
+ variant="h2"
+ >
+ Outputs
+
+ }
+ >
+
+
+
+ Diagnostics
+
+ }
+ >
+
+
+
+
+ SSE replay}
+ description="Replayable audit trail"
+ variant="h2"
+ >
+ Event timeline
+
+ }
+ >
+
+
+
+
);
}
@@ -560,29 +682,46 @@ export function WorldConsoleView({
onOperation,
}: WorldConsoleViewProps): React.JSX.Element {
return (
-
-
- {state.kind === 'loading' ? (
-
- ) : null}
- {state.kind === 'error' ? (
-
- ) : null}
- {state.kind === 'ready' ? (
-
+
+
+ {state.kind === 'loading' ? (
+
+ ) : null}
+ {state.kind === 'error' ? (
+
+ ) : null}
+ {state.kind === 'ready' ? (
+
+ ) : null}
+ >
+ }
+ navigationHide
+ toolsHide
+ />
);
}
diff --git a/apps/console/test/console.test.tsx b/apps/console/test/console.test.tsx
index b1a1185..a3b2a81 100644
--- a/apps/console/test/console.test.tsx
+++ b/apps/console/test/console.test.tsx
@@ -28,7 +28,6 @@ import {
createAuthenticatedSimulatorApp,
LaunchTokenAuthority,
} from '@tenkacloud/simulator-server';
-import { renderToStaticMarkup } from 'react-dom/server';
import {
ConsoleClientError,
parseEventStream,
@@ -54,11 +53,7 @@ import {
mergeEvents,
propertyCategories,
} from '../src/model';
-import {
- ConsoleOperationResult,
- runConsoleOperationAction,
- WorldConsoleView,
-} from '../src/view';
+import { runConsoleOperationAction } from '../src/view';
const GCP_TEMPLATE = readFileSync(
new URL(
@@ -307,18 +302,6 @@ describe('Console の API client と route', () => {
expect(cleaned).toBe('/console/world-a');
expect(duplicate.hash).toBe('');
expect(tokenError).toBeInstanceOf(ConsoleLaunchTokenError);
- const message =
- tokenError instanceof Error ? tokenError.message : 'Launch denied';
- const errorView = renderToStaticMarkup(
- undefined}
- onOperation={async () => undefined}
- />
- );
- expect(errorView).toContain('role="alert"');
- expect(errorView).toContain('simulator launch token');
- expect(errorView).not.toContain('tc_sim_v1');
});
it('実 HTTP と SQLite の world を route から読み、SSE cursor で replay する', async () => {
@@ -480,29 +463,6 @@ describe('Console の provider-neutral projection model', () => {
],
})
).toEqual([diagnostic]);
- const rejectedEvents = await rejectedLaunch.client.events(
- rejectedLaunch.world.worldId
- );
- const rendered = renderToStaticMarkup(
- undefined}
- onOperation={async () => undefined}
- />
- );
- expect(rendered).toContain('MissingProvider');
- expect(rendered).toContain('unavailable.json');
});
it('外部 API mutation の SSE を受けると resource と deployment projection を再取得する', async () => {
@@ -617,93 +577,5 @@ describe('Console の provider-neutral projection model', () => {
expect(refreshed.events.map((event) => event.type)).toContain(
'GcpServiceUpdated'
);
- expect(
- renderToStaticMarkup(
-
- )
- ).toContain('role="status"');
- expect(
- renderToStaticMarkup(
-
- )
- ).toContain('role="alert"');
- expect(
- renderToStaticMarkup()
- ).toBe('');
- });
-});
-
-describe('Console view の状態表示', () => {
- it('実 world の resource、policy、reachability、output、event を表示する', async () => {
- const { client, world, deployment } = await readyWorld();
- const data = await loadConsoleData(client, {
- worldId: world.worldId,
- deploymentId: deployment.deploymentId,
- });
- const html = renderToStaticMarkup(
- undefined}
- onOperation={async () => undefined}
- />
- );
- expect(html).toContain('Provider projections');
- expect(html).toContain('google_cloud_run_v2_service');
- expect(html).toContain('Targetdefault');
- expect(html).toContain('Policy');
- expect(html).toContain('Reachability');
- expect(html).toContain('GcpHelloUrl');
- expect(html).toContain('DeploymentReady');
- expect(html).toContain('SSE replay');
- expect(html).toContain('Provider operation');
- expect(html).toContain('Execute command');
- expect(html).toMatch(/console-[a-f0-9-]{36}/);
- });
-
- it('loading、error、空 projection の状態を accessible text で表示する', async () => {
- const loading = renderToStaticMarkup(
- undefined}
- onOperation={async () => undefined}
- />
- );
- const error = renderToStaticMarkup(
- undefined}
- onOperation={async () => undefined}
- />
- );
- const launch = await createWorld('deployment-empty');
- const realData = await loadConsoleData(launch.client, {
- worldId: launch.world.worldId,
- });
- const empty = renderToStaticMarkup(
- undefined}
- onOperation={async () => undefined}
- />
- );
- expect(loading).toContain('Reading the event-sourced world');
- expect(loading).toContain('aria-busy="true"');
- expect(error).toContain('role="alert"');
- expect(error).toContain('World was deleted');
- expect(empty).toContain('No resources have been projected yet.');
- expect(empty).toContain('No events exist after this cursor.');
- expect(empty).toContain('No deployment outputs.');
- expect(empty).toContain('No deployment diagnostics.');
});
});
diff --git a/apps/console/test/dom-setup.ts b/apps/console/test/dom-setup.ts
new file mode 100644
index 0000000..cd2a4c4
--- /dev/null
+++ b/apps/console/test/dom-setup.ts
@@ -0,0 +1,78 @@
+/**
+ * view テスト用の DOM 環境 setup。bunfig.toml の [test].preload
+ * (repository root と apps/console の両方) から読み込まれる。
+ *
+ * preload である理由: Bun は CommonJS 依存 (react-dom や
+ * @testing-library/dom) を module graph の link 時に先行実行する。
+ * react-dom は load 時に document の有無 (canUseDOM) を判定して
+ * onChange などの event system の経路を固定するため、テストファイル内の
+ * import 順では登録が間に合わない。preload だけが全 module より先に
+ * DOM を用意できる。
+ *
+ * happy-dom の GlobalRegistrator は DOM API に加えて fetch などの
+ * ネットワーク・ストリーム実装も happy-dom のエミュレーションへ
+ * 差し替える。このリポジトリの behavior テストは実 HTTP (Bun.serve) と
+ * 実 SQLite を使う No Mock 方針なので、DOM API だけを happy-dom から
+ * 借り、それ以外は Bun native の実装へ戻して通信経路を一切変えない。
+ * FormData は React 19 の form action が DOM の form 要素から値を
+ * 収集するのに必要なため happy-dom 実装のまま残す。
+ */
+import { GlobalRegistrator } from '@happy-dom/global-registrator';
+
+declare global {
+ // グローバル拡張の ambient 宣言は var で行う (TypeScript の仕様)。
+ var IS_REACT_ACT_ENVIRONMENT: boolean;
+}
+
+const bunNative = {
+ AbortController: globalThis.AbortController,
+ AbortSignal: globalThis.AbortSignal,
+ Blob: globalThis.Blob,
+ CloseEvent: globalThis.CloseEvent,
+ ErrorEvent: globalThis.ErrorEvent,
+ File: globalThis.File,
+ Headers: globalThis.Headers,
+ MessageChannel: globalThis.MessageChannel,
+ MessageEvent: globalThis.MessageEvent,
+ MessagePort: globalThis.MessagePort,
+ ReadableStream: globalThis.ReadableStream,
+ Request: globalThis.Request,
+ Response: globalThis.Response,
+ TextDecoder: globalThis.TextDecoder,
+ TextEncoder: globalThis.TextEncoder,
+ TransformStream: globalThis.TransformStream,
+ URL: globalThis.URL,
+ URLSearchParams: globalThis.URLSearchParams,
+ WebSocket: globalThis.WebSocket,
+ WritableStream: globalThis.WritableStream,
+ fetch: globalThis.fetch,
+ structuredClone: globalThis.structuredClone,
+} as const;
+
+GlobalRegistrator.register();
+
+globalThis.AbortController = bunNative.AbortController;
+globalThis.AbortSignal = bunNative.AbortSignal;
+globalThis.Blob = bunNative.Blob;
+globalThis.CloseEvent = bunNative.CloseEvent;
+globalThis.ErrorEvent = bunNative.ErrorEvent;
+globalThis.File = bunNative.File;
+globalThis.Headers = bunNative.Headers;
+globalThis.MessageChannel = bunNative.MessageChannel;
+globalThis.MessageEvent = bunNative.MessageEvent;
+globalThis.MessagePort = bunNative.MessagePort;
+globalThis.ReadableStream = bunNative.ReadableStream;
+globalThis.Request = bunNative.Request;
+globalThis.Response = bunNative.Response;
+globalThis.TextDecoder = bunNative.TextDecoder;
+globalThis.TextEncoder = bunNative.TextEncoder;
+globalThis.TransformStream = bunNative.TransformStream;
+globalThis.URL = bunNative.URL;
+globalThis.URLSearchParams = bunNative.URLSearchParams;
+globalThis.WebSocket = bunNative.WebSocket;
+globalThis.WritableStream = bunNative.WritableStream;
+globalThis.fetch = bunNative.fetch;
+globalThis.structuredClone = bunNative.structuredClone;
+
+// React Testing Library の render / act を有効化する公式フラグ。
+globalThis.IS_REACT_ACT_ENVIRONMENT = true;
diff --git a/apps/console/test/view.test.tsx b/apps/console/test/view.test.tsx
new file mode 100644
index 0000000..9254a03
--- /dev/null
+++ b/apps/console/test/view.test.tsx
@@ -0,0 +1,461 @@
+import './dom-setup';
+import { afterEach, beforeEach, describe, expect, it } from 'bun:test';
+import { mkdtempSync, readFileSync, rmSync } from 'node:fs';
+import { tmpdir } from 'node:os';
+import path from 'node:path';
+import Alert from '@cloudscape-design/components/alert';
+import Container from '@cloudscape-design/components/container';
+import Header from '@cloudscape-design/components/header';
+import StatusIndicator from '@cloudscape-design/components/status-indicator';
+import { PROTOCOL_HEADER } from '@tenkacloud/simulator-api';
+import {
+ assertSimulatorDeploymentResponse,
+ assertSimulatorWorldResponse,
+ SIMULATOR_PROTOCOL_VERSION,
+ type SimulatorDeploymentResponse,
+ type SimulatorWorldResponse,
+} from '@tenkacloud/simulator-contracts';
+import {
+ ProviderRegistry,
+ SimulationCore,
+ SimulationStore,
+} from '@tenkacloud/simulator-core';
+import {
+ CLOUD_RUN_SERVICE,
+ GcpProvider,
+} from '@tenkacloud/simulator-provider-gcp';
+import {
+ createAuthenticatedSimulatorApp,
+ LaunchTokenAuthority,
+} from '@tenkacloud/simulator-server';
+import { cleanup, fireEvent, render } from '@testing-library/react';
+import { SimulatorConsoleClient } from '../src/client';
+import {
+ ConsoleLaunchTokenError,
+ consumeLaunchToken,
+} from '../src/launch-token';
+import { createConsoleOperationAction, loadConsoleData } from '../src/loader';
+import type { ConsoleWorldData } from '../src/model';
+import {
+ ConsoleOperationResult,
+ statusIndicatorType,
+ WorldConsoleView,
+} from '../src/view';
+
+const GCP_TEMPLATE = readFileSync(
+ new URL(
+ '../../../providers/gcp/test/fixtures/hello-multicloud/main.tf',
+ import.meta.url
+ ),
+ 'utf8'
+);
+const TOKEN_SECRET = 'console-view-secret-0123456789abcdef';
+
+interface TestRuntime {
+ readonly authority: LaunchTokenAuthority;
+ readonly baseUrl: string;
+ readonly directory: string;
+ readonly server: Bun.Server;
+ readonly store: SimulationStore;
+}
+
+let runtime: TestRuntime;
+
+function namespace(deploymentId: string) {
+ return {
+ tenantId: 'console-view-tenant',
+ eventId: 'console-view-event',
+ teamId: `team-${deploymentId}`,
+ deploymentId,
+ };
+}
+
+async function createWorld(deploymentId: string): Promise<{
+ readonly client: SimulatorConsoleClient;
+ readonly token: string;
+ readonly world: SimulatorWorldResponse;
+}> {
+ const token = runtime.authority.issue(namespace(deploymentId));
+ const client = new SimulatorConsoleClient(runtime.baseUrl, token);
+ const response = await fetch(`${runtime.baseUrl}/v1/worlds`, {
+ method: 'POST',
+ headers: {
+ authorization: `Bearer ${token}`,
+ 'content-type': 'application/json',
+ [PROTOCOL_HEADER]: SIMULATOR_PROTOCOL_VERSION,
+ },
+ body: JSON.stringify({
+ ...namespace(deploymentId),
+ seed: `seed-${deploymentId}`,
+ virtualClock: '2026-07-16T00:00:00.000Z',
+ }),
+ });
+ expect(response.status).toBe(201);
+ const body: unknown = await response.json();
+ assertSimulatorWorldResponse(body);
+ return { client, token, world: body };
+}
+
+async function deploy(
+ worldId: string,
+ token: string,
+ provider: string,
+ engine: string,
+ entry: string,
+ templateBody: string
+): Promise {
+ return fetch(`${runtime.baseUrl}/v1/worlds/${worldId}/deployments`, {
+ method: 'POST',
+ headers: {
+ authorization: `Bearer ${token}`,
+ 'content-type': 'application/json',
+ [PROTOCOL_HEADER]: SIMULATOR_PROTOCOL_VERSION,
+ },
+ body: JSON.stringify({
+ problemId: `console-view-${provider}`,
+ runtime: { provider, engine, entry },
+ templateBody,
+ }),
+ });
+}
+
+async function readyWorldData(): Promise<{
+ readonly client: SimulatorConsoleClient;
+ readonly data: ConsoleWorldData;
+ readonly deployment: SimulatorDeploymentResponse;
+}> {
+ const deploymentId = 'deployment-view-gcp';
+ const launch = await createWorld(deploymentId);
+ const response = await deploy(
+ launch.world.worldId,
+ launch.token,
+ 'gcp',
+ 'infra-manager',
+ 'main.tf',
+ GCP_TEMPLATE
+ );
+ expect(response.status).toBe(201);
+ const deployment: unknown = await response.json();
+ assertSimulatorDeploymentResponse(deployment);
+ const data = await loadConsoleData(launch.client, {
+ worldId: launch.world.worldId,
+ deploymentId,
+ });
+ return { client: launch.client, data, deployment };
+}
+
+const noRefresh: () => void = () => undefined;
+
+const noOperation: () => Promise = () => Promise.resolve(undefined);
+
+beforeEach(() => {
+ const directory = mkdtempSync(path.join(tmpdir(), 'simulator-console-view-'));
+ const store = new SimulationStore(path.join(directory, 'simulation.sqlite'));
+ const registry = new ProviderRegistry([new GcpProvider()]);
+ const core = new SimulationCore(store, registry);
+ const authority = new LaunchTokenAuthority(TOKEN_SECRET);
+ const app = createAuthenticatedSimulatorApp({
+ core,
+ registry,
+ consoleBaseUrl: 'http://127.0.0.1:4173/console',
+ launchTokens: authority,
+ });
+ const server = Bun.serve({
+ hostname: '127.0.0.1',
+ port: 0,
+ fetch: app.fetch,
+ });
+ runtime = {
+ authority,
+ baseUrl: server.url.origin,
+ directory,
+ server,
+ store,
+ };
+});
+
+afterEach(async () => {
+ cleanup();
+ await runtime.server.stop(true);
+ runtime.store.close();
+ rmSync(runtime.directory, { recursive: true, force: true });
+});
+
+describe('Cloudscape コンポーネントを DOM 環境で描画できること', () => {
+ it('代表 component の Alert と StatusIndicator と Container を DOM 環境で描画する', () => {
+ const view = render(
+ spike container}>
+ spike alert body
+ spike status
+
+ );
+ expect(view.getByText('spike container')).toBeTruthy();
+ expect(view.getByText('spike alert body')).toBeTruthy();
+ expect(view.getByText('spike status')).toBeTruthy();
+ });
+});
+
+describe('Console shell の状態表示', () => {
+ it('loading 状態を aria-busy な領域と world ID の accessible text で表示する', () => {
+ const view = render(
+
+ );
+ expect(view.container.querySelector('[aria-busy="true"]')).toBeTruthy();
+ expect(view.getByText('Reading the event-sourced world')).toBeTruthy();
+ expect(view.getByText('World world-loading')).toBeTruthy();
+ expect(view.getAllByText('TenkaCloud Simulator').length).toBeGreaterThan(0);
+ });
+
+ it('error 状態を role=alert と再試行 Button で表示する', () => {
+ let refreshes = 0;
+ const view = render(
+ {
+ refreshes += 1;
+ }}
+ onOperation={noOperation}
+ />
+ );
+ const alert = view.getByRole('alert');
+ expect(alert.textContent).toContain('World unavailable');
+ expect(alert.textContent).toContain('World was deleted');
+ expect(alert.textContent).toContain('world-error');
+ fireEvent.click(view.getByText('Try again'));
+ expect(refreshes).toBe(1);
+ });
+
+ it('launch token エラーを表示しても token 値を DOM に露出しない', () => {
+ const duplicate = new URL(
+ 'http://console.local/console/world-a#token=tc_sim_v1.a.b&token=tc_sim_v1.c.d'
+ );
+ let tokenError: unknown;
+ try {
+ consumeLaunchToken(duplicate, () => undefined);
+ } catch (error) {
+ tokenError = error;
+ }
+ if (!(tokenError instanceof ConsoleLaunchTokenError)) {
+ throw new Error('ConsoleLaunchTokenError が発生しませんでした');
+ }
+ const view = render(
+
+ );
+ expect(view.getByRole('alert').textContent).toContain(
+ 'simulator launch token'
+ );
+ expect(view.baseElement.innerHTML).not.toContain('tc_sim_v1');
+ });
+});
+
+describe('Console ready 表示', () => {
+ it('実 world の provider projection と output と event を表示する', async () => {
+ const { data } = await readyWorldData();
+ let refreshes = 0;
+ const view = render(
+ {
+ refreshes += 1;
+ }}
+ onOperation={noOperation}
+ />
+ );
+ expect(view.getByText('Provider projections')).toBeTruthy();
+ expect(view.getAllByText('gcp').length).toBeGreaterThan(0);
+ expect(view.getAllByText(CLOUD_RUN_SERVICE).length).toBeGreaterThan(0);
+ expect(view.getAllByText('Target').length).toBeGreaterThan(0);
+ expect(view.getAllByText('default').length).toBeGreaterThan(0);
+ expect(view.getAllByText('Policy').length).toBeGreaterThan(0);
+ expect(view.getAllByText('Reachability').length).toBeGreaterThan(0);
+ expect(view.getAllByText('Properties').length).toBeGreaterThan(0);
+ expect(view.getByText('GcpHelloUrl')).toBeTruthy();
+ expect(view.getByText('DeploymentReady')).toBeTruthy();
+ expect(view.getByText('SSE replay')).toBeTruthy();
+ expect(view.getByText(`cursor ${data.cursor}`)).toBeTruthy();
+ expect(view.getByText('Provider operation')).toBeTruthy();
+ expect(view.getByText('Execute command')).toBeTruthy();
+ expect(view.getAllByText('running').length).toBeGreaterThan(0);
+ expect(view.getAllByText('ready').length).toBeGreaterThan(0);
+ const idempotency = view.getByLabelText('Idempotency key');
+ if (!(idempotency instanceof HTMLInputElement)) {
+ throw new Error('Idempotency key input がありません');
+ }
+ expect(idempotency.value).toMatch(/^console-[a-f0-9-]{36}$/);
+ fireEvent.click(view.getByText('Refresh projection'));
+ expect(refreshes).toBe(1);
+ });
+
+ it('空 projection と deployment 未選択の状態を empty text で表示する', async () => {
+ const launch = await createWorld('deployment-view-empty');
+ const realData = await loadConsoleData(launch.client, {
+ worldId: launch.world.worldId,
+ });
+ const view = render(
+
+ );
+ expect(
+ view.getByText('No resources have been projected yet.')
+ ).toBeTruthy();
+ expect(view.getByText('No events exist after this cursor.')).toBeTruthy();
+ expect(view.getByText('No deployment outputs.')).toBeTruthy();
+ expect(view.getByText('No deployment diagnostics.')).toBeTruthy();
+ expect(view.getByText('not selected')).toBeTruthy();
+ expect(
+ view.getByText(
+ 'Select a deployment in the Console URL before executing a command.'
+ )
+ ).toBeTruthy();
+ });
+
+ it('未実装 provider の diagnostics に MissingProvider と source を表示する', async () => {
+ const rejectedId = 'deployment-view-rejected';
+ const launch = await createWorld(rejectedId);
+ const response = await deploy(
+ launch.world.worldId,
+ launch.token,
+ 'unavailable-provider',
+ 'declarative',
+ 'unavailable.json',
+ '{}'
+ );
+ expect(response.status).toBe(422);
+ const data = await loadConsoleData(launch.client, {
+ worldId: launch.world.worldId,
+ deploymentId: rejectedId,
+ });
+ const view = render(
+
+ );
+ expect(view.getByText('MissingProvider')).toBeTruthy();
+ expect(view.getAllByText(/unavailable\.json/).length).toBeGreaterThan(0);
+ expect(view.getAllByText('failed').length).toBeGreaterThan(0);
+ });
+
+ it('未知 status を StatusIndicator の pending 表示へフォールバックする', () => {
+ expect(statusIndicatorType('mystery-status')).toBe('pending');
+ expect(statusIndicatorType('running')).toBe('success');
+ expect(statusIndicatorType('ready')).toBe('success');
+ expect(statusIndicatorType('failed')).toBe('error');
+ expect(statusIndicatorType('deploying')).toBe('in-progress');
+ });
+});
+
+describe('Console 操作フォームがコマンド送信状態を表示すること', () => {
+ it('送信中は Executing… と disabled を表示し、成功で role=status を出す', async () => {
+ const { client, data, deployment } = await readyWorldData();
+ const service = data.resources.resources.find(
+ (resource) => resource.resourceType === CLOUD_RUN_SERVICE
+ );
+ if (!service) throw new Error('Cloud Run service projection がありません');
+ let release = (): void => undefined;
+ const gate = new Promise((resolve) => {
+ release = resolve;
+ });
+ let refreshes = 0;
+ const realAction = createConsoleOperationAction(
+ client,
+ { worldId: data.worldId, deploymentId: deployment.deploymentId },
+ () => {
+ refreshes += 1;
+ }
+ );
+ const view = render(
+ {
+ await realAction(formData);
+ await gate;
+ }}
+ />
+ );
+ fireEvent.input(view.getByLabelText('Provider'), {
+ target: { value: 'gcp' },
+ });
+ fireEvent.input(view.getByLabelText('Engine'), {
+ target: { value: 'infra-manager' },
+ });
+ fireEvent.input(view.getByLabelText('Service'), {
+ target: { value: 'run' },
+ });
+ fireEvent.input(view.getByLabelText('Resource type'), {
+ target: { value: CLOUD_RUN_SERVICE },
+ });
+ fireEvent.input(view.getByLabelText('Operation'), {
+ target: { value: 'UpdateService' },
+ });
+ fireEvent.input(view.getByLabelText('Input JSON object'), {
+ target: {
+ value: JSON.stringify({
+ id: service.resourceId,
+ patch: { minInstanceCount: 1, maxInstanceCount: 3 },
+ }),
+ },
+ });
+ fireEvent.input(view.getByLabelText('Idempotency key'), {
+ target: { value: 'console-view-update-service' },
+ });
+ const submit = view.getByText('Execute command').closest('button');
+ if (!submit) throw new Error('Execute command button がありません');
+ fireEvent.click(submit);
+ await view.findByText('Executing…');
+ const pendingButton = view.getByText('Executing…').closest('button');
+ expect(pendingButton?.disabled).toBe(true);
+ release();
+ const result = await view.findByText(
+ 'Command accepted. Waiting for the shared projection.'
+ );
+ expect(result.closest('[role="status"]')).toBeTruthy();
+ expect(refreshes).toBe(1);
+ expect(view.getByText('Execute command')).toBeTruthy();
+ });
+
+ it('ConsoleOperationResult が success を role=status、error を role=alert、idle を非表示にする', () => {
+ const success = render(
+
+ );
+ expect(success.getByRole('status').textContent).toContain(
+ 'Command accepted.'
+ );
+ success.unmount();
+ const error = render(
+
+ );
+ expect(error.getByRole('alert').textContent).toContain('Command rejected.');
+ error.unmount();
+ const idle = render();
+ expect(idle.container.innerHTML).toBe('');
+ });
+});
diff --git a/bun.lock b/bun.lock
index c5ed8c2..a32a871 100644
--- a/bun.lock
+++ b/bun.lock
@@ -4,6 +4,11 @@
"workspaces": {
"": {
"name": "typescript-template",
+ "dependencies": {
+ "@cloudscape-design/collection-hooks": "^1.0.99",
+ "@cloudscape-design/theming-runtime": "^1.0.118",
+ "ws": "^8.21.0",
+ },
"devDependencies": {
"@biomejs/biome": "2.4.16",
"@textlint-ja/textlint-rule-preset-ai-writing": "1.7.0",
@@ -35,16 +40,21 @@
"name": "@tenkacloud/simulator-console",
"version": "0.1.0",
"dependencies": {
+ "@cloudscape-design/components": "3.0.1324",
+ "@cloudscape-design/global-styles": "1.0.62",
"@tenkacloud/simulator-contracts": "workspace:*",
"react": "19.2.7",
"react-dom": "19.2.7",
},
"devDependencies": {
+ "@happy-dom/global-registrator": "20.10.6",
"@tenkacloud/simulator-api": "workspace:*",
"@tenkacloud/simulator-core": "workspace:*",
"@tenkacloud/simulator-provider-gcp": "workspace:*",
"@tenkacloud/simulator-provider-sakura": "workspace:*",
"@tenkacloud/simulator-server": "workspace:*",
+ "@testing-library/dom": "10.4.1",
+ "@testing-library/react": "16.3.2",
"@types/bun": "1.3.14",
"@types/react": "19.2.17",
"@types/react-dom": "19.2.3",
@@ -190,6 +200,8 @@
"@babel/parser": ["@babel/parser@7.23.9", "", { "bin": { "parser": "bin/babel-parser.js" } }, "sha512-9tcKgqKbs3xGJ+NtKF2ndOBBLVwPjl1SHxPQkd36r3Dlirw3xWUeGaTbqr7uGZcTaxkVNwc+03SVP7aCdWrTlA=="],
+ "@babel/runtime": ["@babel/runtime@7.29.7", "", {}, "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw=="],
+
"@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="],
"@biomejs/biome": ["@biomejs/biome@2.4.16", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.4.16", "@biomejs/cli-darwin-x64": "2.4.16", "@biomejs/cli-linux-arm64": "2.4.16", "@biomejs/cli-linux-arm64-musl": "2.4.16", "@biomejs/cli-linux-x64": "2.4.16", "@biomejs/cli-linux-x64-musl": "2.4.16", "@biomejs/cli-win32-arm64": "2.4.16", "@biomejs/cli-win32-x64": "2.4.16" }, "bin": { "biome": "bin/biome" } }, "sha512-x9ajFh1zChVybCiM3TN6OD4phAqLgtPZjFrZF+aTMYCPjwBO+k529TX7PPsAqtGNLeV4UgzwQnowEgS7bGmzcA=="],
@@ -214,12 +226,44 @@
"@cacheable/utils": ["@cacheable/utils@2.3.2", "", { "dependencies": { "hashery": "^1.2.0", "keyv": "^5.5.4" } }, "sha512-8kGE2P+HjfY8FglaOiW+y8qxcaQAfAhVML+i66XJR3YX5FtyDqn6Txctr3K2FrbxLKixRRYYBWMbuGciOhYNDg=="],
+ "@cloudscape-design/collection-hooks": ["@cloudscape-design/collection-hooks@1.0.99", "", { "dependencies": { "@cloudscape-design/component-toolkit": "^1.0.0-beta" }, "peerDependencies": { "react": ">=16.8.0" } }, "sha512-o53CKwva+VECL4MMPS3gyxn7+GAONmWy+LxyiWRoM3q8wkPzN8G3Ooc1kALeRrPWYCWwDCc5PHutcxv4Chhs7g=="],
+
+ "@cloudscape-design/component-toolkit": ["@cloudscape-design/component-toolkit@1.0.0-beta.170", "", { "dependencies": { "tslib": "^2.3.1", "weekstart": "^2.0.0" }, "peerDependencies": { "react": ">=16.8.0" } }, "sha512-3O+tNmCACZDL9qe5YZhBg/d2RA18m4P669N5A2EzEDlw7fvrp7q76CzkrpaGTUGzSaqlqI1SCce7eZKtkNeBUA=="],
+
+ "@cloudscape-design/components": ["@cloudscape-design/components@3.0.1324", "", { "dependencies": { "@cloudscape-design/collection-hooks": "^1.0.0", "@cloudscape-design/component-toolkit": "^1.0.0-beta", "@cloudscape-design/test-utils-core": "^1.0.0", "@cloudscape-design/theming-runtime": "^1.0.0", "@dnd-kit/core": "^6.0.8", "@dnd-kit/sortable": "^7.0.2", "@dnd-kit/utilities": "^3.2.1", "ace-builds": "^1.34.0", "clsx": "^1.1.0", "d3-shape": "^1.3.7", "date-fns": "^2.25.0", "intl-messageformat": "^10.3.1", "mnth": "^2.0.0", "react-is": "^18.2.0", "react-transition-group": "^4.4.2", "tslib": "^2.4.0", "weekstart": "^1.1.0" }, "peerDependencies": { "react": ">=16.8.0" } }, "sha512-efnyyE6bjDVnBuS60NTDiesXrUJ6GGnNlbroKCvfi8OTLiUUOjt4d0BSLPiMhf8Rnn+M/pSpPc5/aj98GhfPVg=="],
+
+ "@cloudscape-design/global-styles": ["@cloudscape-design/global-styles@1.0.62", "", {}, "sha512-8ll8m/Z2/nwHNBhGtEj/pqkBb08OkboyXoKef+D73HmUFgAOhhXCKUaqtF2SVOsvjHm5IffEQRvKK3/CQUeR1g=="],
+
+ "@cloudscape-design/test-utils-core": ["@cloudscape-design/test-utils-core@1.0.85", "", { "dependencies": { "css-selector-tokenizer": "^0.8.0", "css.escape": "^1.5.1" } }, "sha512-9zHTaYWdEmnBl83EVr3ZXck2NPAf1ctoj7Tro+n7q6G7boqL8PpGXDVwr9xyMAywBVgZi+RppyRd1/MLqngl3g=="],
+
+ "@cloudscape-design/theming-runtime": ["@cloudscape-design/theming-runtime@1.0.118", "", { "dependencies": { "@material/material-color-utilities": "^0.3.0", "tslib": "^2.4.0" } }, "sha512-G0OUqf+ESOTCenyUYh4QAP0YE8ZAvqE+ApgHve6bpLpJN3QUsasvd+MyGipU49ilzJBtm/F8L3U5wd5d50CTFA=="],
+
+ "@dnd-kit/accessibility": ["@dnd-kit/accessibility@3.1.1", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "react": ">=16.8.0" } }, "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw=="],
+
+ "@dnd-kit/core": ["@dnd-kit/core@6.3.1", "", { "dependencies": { "@dnd-kit/accessibility": "^3.1.1", "@dnd-kit/utilities": "^3.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ=="],
+
+ "@dnd-kit/sortable": ["@dnd-kit/sortable@7.0.2", "", { "dependencies": { "@dnd-kit/utilities": "^3.2.0", "tslib": "^2.0.0" }, "peerDependencies": { "@dnd-kit/core": "^6.0.7", "react": ">=16.8.0" } }, "sha512-wDkBHHf9iCi1veM834Gbk1429bd4lHX4RpAwT0y2cHLf246GAvU2sVw/oxWNpPKQNQRQaeGXhAVgrOl1IT+iyA=="],
+
+ "@dnd-kit/utilities": ["@dnd-kit/utilities@3.2.2", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "react": ">=16.8.0" } }, "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg=="],
+
"@emnapi/core": ["@emnapi/core@1.11.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" } }, "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ=="],
"@emnapi/runtime": ["@emnapi/runtime@1.11.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw=="],
"@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA=="],
+ "@formatjs/ecma402-abstract": ["@formatjs/ecma402-abstract@2.3.6", "", { "dependencies": { "@formatjs/fast-memoize": "2.2.7", "@formatjs/intl-localematcher": "0.6.2", "decimal.js": "^10.4.3", "tslib": "^2.8.0" } }, "sha512-HJnTFeRM2kVFVr5gr5kH1XP6K0JcJtE7Lzvtr3FS/so5f1kpsqqqxy5JF+FRaO6H2qmcMfAUIox7AJteieRtVw=="],
+
+ "@formatjs/fast-memoize": ["@formatjs/fast-memoize@2.2.7", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-Yabmi9nSvyOMrlSeGGWDiH7rf3a7sIwplbvo/dlz9WCIjzIQAfy1RMf4S0X3yG724n5Ghu2GmEl5NJIV6O9sZQ=="],
+
+ "@formatjs/icu-messageformat-parser": ["@formatjs/icu-messageformat-parser@2.11.4", "", { "dependencies": { "@formatjs/ecma402-abstract": "2.3.6", "@formatjs/icu-skeleton-parser": "1.8.16", "tslib": "^2.8.0" } }, "sha512-7kR78cRrPNB4fjGFZg3Rmj5aah8rQj9KPzuLsmcSn4ipLXQvC04keycTI1F7kJYDwIXtT2+7IDEto842CfZBtw=="],
+
+ "@formatjs/icu-skeleton-parser": ["@formatjs/icu-skeleton-parser@1.8.16", "", { "dependencies": { "@formatjs/ecma402-abstract": "2.3.6", "tslib": "^2.8.0" } }, "sha512-H13E9Xl+PxBd8D5/6TVUluSpxGNvFSlN/b3coUp0e0JpuWXXnQDiavIpY3NnvSp4xhEMoXyyBvVfdFX8jglOHQ=="],
+
+ "@formatjs/intl-localematcher": ["@formatjs/intl-localematcher@0.6.2", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-XOMO2Hupl0wdd172Y06h6kLpBz6Dv+J4okPLl4LPtzbr8f66WbIoy4ev98EBuZ6ZK4h5ydTN6XneT4QVpD7cdA=="],
+
+ "@happy-dom/global-registrator": ["@happy-dom/global-registrator@20.10.6", "", { "dependencies": { "@types/node": ">=20.0.0", "happy-dom": "^20.10.6" } }, "sha512-Nu/IjRkkNxmeG2ywWsyJSO4d1BrWTqVzxCPL+gXj0b97klhmjd6wLzt6Bx/laNpkZ3WLW7zNCqmtMIbIlahqug=="],
+
"@hono/node-server": ["@hono/node-server@1.19.14", "", { "peerDependencies": { "hono": "^4" } }, "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw=="],
"@keyv/bigmap": ["@keyv/bigmap@1.3.0", "", { "dependencies": { "hashery": "^1.2.0", "hookified": "^1.13.0" }, "peerDependencies": { "keyv": "^5.5.4" } }, "sha512-KT01GjzV6AQD5+IYrcpoYLkCu1Jod3nau1Z7EsEuViO3TZGRacSbO9MfHmbJ1WaOXFtWLxPVj169cn2WNKPkIg=="],
@@ -236,6 +280,8 @@
"@kvs/types": ["@kvs/types@2.1.4", "", {}, "sha512-XtTOUatnJrDcuYNPfN+9x1xuVp11IuyPi9zOLVJPpfnKp+vUCatka/Crp2cfz0uKt4QE1Pb4Dg/TNYgVYA2Org=="],
+ "@material/material-color-utilities": ["@material/material-color-utilities@0.3.0", "", {}, "sha512-ztmtTd6xwnuh2/xu+Vb01btgV8SQWYCaK56CkRK8gEkWe5TuDyBcYJ0wgkMRn+2VcE9KUmhvkz+N9GHrqw/C0g=="],
+
"@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.29.0", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ=="],
"@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.6", "", { "dependencies": { "@tybys/wasm-util": "^0.10.3" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg=="],
@@ -302,6 +348,10 @@
"@tenkacloud/simulator-workload-runner": ["@tenkacloud/simulator-workload-runner@workspace:tools/workload-runner"],
+ "@testing-library/dom": ["@testing-library/dom@10.4.1", "", { "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", "@types/aria-query": "^5.0.1", "aria-query": "5.3.0", "dom-accessibility-api": "^0.5.9", "lz-string": "^1.5.0", "picocolors": "1.1.1", "pretty-format": "^27.0.2" } }, "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg=="],
+
+ "@testing-library/react": ["@testing-library/react@16.3.2", "", { "dependencies": { "@babel/runtime": "^7.12.5" }, "peerDependencies": { "@testing-library/dom": "^10.0.0", "@types/react": "^18.0.0 || ^19.0.0", "@types/react-dom": "^18.0.0 || ^19.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g=="],
+
"@textlint-ja/textlint-rule-preset-ai-writing": ["@textlint-ja/textlint-rule-preset-ai-writing@1.7.0", "", { "dependencies": { "@textlint/regexp-string-matcher": "^2.0.2", "kuromojin": "^3.0.1", "textlint-util-to-string": "^3.3.4" } }, "sha512-xq+y592q4qP4SK5Vs40JRi98BmpJJoIaUO8/U+puOErCyazQvoZqDswIuKsFi1IEFmZ/wNn8OkfJ/5GNhrppNw=="],
"@textlint-rule/textlint-rule-no-invalid-control-character": ["@textlint-rule/textlint-rule-no-invalid-control-character@3.0.0", "", {}, "sha512-2o9n4z49ntSPtJPlcJtxakyB4dAg2MKSvR9ZCZEHjye0ee27oWYzK6yHz2HjsXQqt9VeCwxNHDOIGIx2CQX0Dw=="],
@@ -346,6 +396,8 @@
"@tybys/wasm-util": ["@tybys/wasm-util@0.10.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg=="],
+ "@types/aria-query": ["@types/aria-query@5.0.4", "", {}, "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw=="],
+
"@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="],
"@types/debug": ["@types/debug@0.0.30", "", {}, "sha512-orGL5LXERPYsLov6CWs3Fh6203+dXzJkR7OnddIr2514Hsecwc8xRpzCapshBbKFImCsvS/mk6+FWiN5LyZJAQ=="],
@@ -362,6 +414,10 @@
"@types/unist": ["@types/unist@2.0.10", "", {}, "sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA=="],
+ "@types/whatwg-mimetype": ["@types/whatwg-mimetype@3.0.2", "", {}, "sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA=="],
+
+ "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="],
+
"@typescript/typescript-aix-ppc64": ["@typescript/typescript-aix-ppc64@7.0.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ=="],
"@typescript/typescript-darwin-arm64": ["@typescript/typescript-darwin-arm64@7.0.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA=="],
@@ -406,6 +462,8 @@
"accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="],
+ "ace-builds": ["ace-builds@1.44.0", "", {}, "sha512-PFNMSYqFdEUkul2Ntud0HvA09AgY+F1ag0UYdpMH60wNI/qOA8cB8tlTgoALMEwIdUPJK2CjrIQ7OnbiSS/ugQ=="],
+
"ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="],
"ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="],
@@ -414,12 +472,14 @@
"ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
- "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
+ "ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="],
"app-root-path": ["app-root-path@3.1.0", "", {}, "sha512-biN3PwB2gUtjaYy/isrU3aNWI5w+fAfvHkSvCKeQGxhmYpwKFUxudR3Yya+KqVRHBmEDYh+/lTozYCFbmzX4nA=="],
"argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
+ "aria-query": ["aria-query@5.3.0", "", { "dependencies": { "dequal": "^2.0.3" } }, "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A=="],
+
"assign-symbols": ["assign-symbols@1.0.0", "", {}, "sha512-Q+JC7Whu8HhmTdBph/Tq59IoRtoy6KAm5zzPv00WdujX82lbAL8K7WVjne7vdCsAmbF4AYaDOPyO3k0kl8qIrw=="],
"astral-regex": ["astral-regex@2.0.0", "", {}, "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ=="],
@@ -436,6 +496,8 @@
"brace-expansion": ["brace-expansion@5.0.5", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ=="],
+ "buffer-image-size": ["buffer-image-size@0.6.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-nEh+kZOPY1w+gcCMobZ6ETUp9WfibndnosbpwB1iJk/8Gt5ZF2bhS6+B6bPYz424KtwsR6Rflc3tCz1/ghX2dQ=="],
+
"bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="],
"bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],
@@ -464,6 +526,8 @@
"clone-regexp": ["clone-regexp@2.2.0", "", { "dependencies": { "is-regexp": "^2.0.0" } }, "sha512-beMpP7BOtTipFuW8hrJvREQ2DrRu3BE7by0ZpibtfBA+qfHYvMGTc2Yb1JMYPKg/JUw0CHYvpg796aNTSW9z7Q=="],
+ "clsx": ["clsx@1.2.1", "", {}, "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg=="],
+
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
@@ -486,10 +550,24 @@
"crypt": ["crypt@0.0.2", "", {}, "sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow=="],
+ "css-selector-tokenizer": ["css-selector-tokenizer@0.8.0", "", { "dependencies": { "cssesc": "^3.0.0", "fastparse": "^1.1.2" } }, "sha512-Jd6Ig3/pe62/qe5SBPTN8h8LeUg/pT4lLgtavPf7updwwHpvFzxvOQBHYj2LZDMjUnBzgvIUSjRcf6oT5HzHFg=="],
+
+ "css.escape": ["css.escape@1.5.1", "", {}, "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg=="],
+
+ "cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="],
+
"csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
+ "d3-path": ["d3-path@1.0.9", "", {}, "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg=="],
+
+ "d3-shape": ["d3-shape@1.3.7", "", { "dependencies": { "d3-path": "1" } }, "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw=="],
+
+ "date-fns": ["date-fns@2.30.0", "", { "dependencies": { "@babel/runtime": "^7.21.0" } }, "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw=="],
+
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
+ "decimal.js": ["decimal.js@10.6.0", "", {}, "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg=="],
+
"deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="],
"define-data-property": ["define-data-property@1.1.1", "", { "dependencies": { "get-intrinsic": "^1.2.1", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.0" } }, "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ=="],
@@ -500,10 +578,16 @@
"depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="],
+ "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="],
+
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
"diff": ["diff@8.0.4", "", {}, "sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw=="],
+ "dom-accessibility-api": ["dom-accessibility-api@0.5.16", "", {}, "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg=="],
+
+ "dom-helpers": ["dom-helpers@5.2.1", "", { "dependencies": { "@babel/runtime": "^7.8.7", "csstype": "^3.0.2" } }, "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA=="],
+
"doublearray": ["doublearray@0.0.2", "", {}, "sha512-aw55FtZzT6AmiamEj2kvmR6BuFqvYgKZUkfQ7teqVRNqD5UE0rw8IeW/3gieHNKQ5sPuDKlljWEn4bzv5+1bHw=="],
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
@@ -514,6 +598,8 @@
"encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="],
+ "entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="],
+
"es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
"es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
@@ -552,6 +638,8 @@
"fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="],
+ "fastparse": ["fastparse@1.1.2", "", {}, "sha512-483XLLxTVIwWK3QTrMGRqUfUpoOs/0hbQrl2oz4J0pAcm3A3bu84wxTFqGqkJzewCLdME38xJLJAxBABfQT8sQ=="],
+
"fault": ["fault@1.0.4", "", { "dependencies": { "format": "^0.2.0" } }, "sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA=="],
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
@@ -588,6 +676,8 @@
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
+ "happy-dom": ["happy-dom@20.10.6", "", { "dependencies": { "@types/node": ">=20.0.0", "@types/whatwg-mimetype": "^3.0.2", "@types/ws": "^8.18.1", "buffer-image-size": "^0.6.4", "entities": "^7.0.1", "whatwg-mimetype": "^3.0.0", "ws": "^8.21.0" } }, "sha512-6QD0ilzDDt93tX44y8tbmZdAcdTRYDhUP+Asgi6pC8Pp5IA3cvaZGyoVN/EGtlq9ziT65iPuBBn3ASLr6hCgVw=="],
+
"has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
"has-property-descriptors": ["has-property-descriptors@1.0.1", "", { "dependencies": { "get-intrinsic": "^1.2.2" } }, "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg=="],
@@ -624,6 +714,8 @@
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
+ "intl-messageformat": ["intl-messageformat@10.7.18", "", { "dependencies": { "@formatjs/ecma402-abstract": "2.3.6", "@formatjs/fast-memoize": "2.2.7", "@formatjs/icu-messageformat-parser": "2.11.4", "tslib": "^2.8.0" } }, "sha512-m3Ofv/X/tV8Y3tHXLohcuVuhWKo7BBq62cqY15etqmLxg2DZ34AGGgQDeR+SCta2+zICb1NX83af0GJmbQ1++g=="],
+
"ip-address": ["ip-address@10.2.0", "", {}, "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA=="],
"ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="],
@@ -718,10 +810,14 @@
"longest-streak": ["longest-streak@2.0.4", "", {}, "sha512-vM6rUVCVUJJt33bnmHiZEvr7wPT78ztX7rojL+LW51bHtLh6HTjx84LA5W4+oa6aKEJA7jJu5LR6vQRBpA5DVg=="],
+ "loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="],
+
"lru-cache": ["lru-cache@11.2.4", "", {}, "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg=="],
"lru_map": ["lru_map@0.4.1", "", {}, "sha512-I+lBvqMMFfqaV8CJCISjI3wbjmwVu/VyOoU7+qtu9d7ioW5klMgsTTiUOUp+DJvfTTzKXoPbyC6YfgkNcyPSOg=="],
+ "lz-string": ["lz-string@1.5.0", "", { "bin": { "lz-string": "bin/bin.js" } }, "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ=="],
+
"markdown-table": ["markdown-table@2.0.0", "", { "dependencies": { "repeat-string": "^1.0.0" } }, "sha512-Ezda85ToJUBhM6WGaG6veasyym+Tbs3cMAw/ZhOPqXiYsr0jgocBV3j3nx+4lk47plLlIqjwuTm/ywVI+zjJ/A=="],
"match-index": ["match-index@1.0.3", "", { "dependencies": { "regexp.prototype.flags": "^1.1.1" } }, "sha512-1XjyBWqCvEFFUDW/MPv0RwbITRD4xQXOvKoPYtLDq8IdZTfdF/cQSo5Yn4qvhfSSZgjgkTFsqJD2wOUG4ovV8Q=="],
@@ -782,6 +878,8 @@
"minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="],
+ "mnth": ["mnth@2.0.0", "", { "dependencies": { "@babel/runtime": "^7.8.0" } }, "sha512-3ZH4UWBGpAwCKdfjynLQpUDVZWMe6vRHwarIpMdGLUp89CVR9hjzgyWERtMyqx+fPEqQ/PsAxFwvwPxLFxW40A=="],
+
"moji": ["moji@0.5.1", "", { "dependencies": { "object-assign": "^3.0.0" } }, "sha512-xYylXOjBS9mE/d690InK3Y74NpE0El0TmAKDmKJveWk9jds/0Tl7MQP4yhavS0U64diEq+5ey2905nhCpIHE+Q=="],
"morpheme-match": ["morpheme-match@2.0.4", "", {}, "sha512-C3U5g2h47dpztGVePLA8w2O1aQEvuJORwIcahWaCG91YPrq+0u7qcPsF9Nqqe8noFvHwgO7b2EEk3iPnYuGTew=="],
@@ -852,8 +950,12 @@
"prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
+ "pretty-format": ["pretty-format@27.5.1", "", { "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", "react-is": "^17.0.1" } }, "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ=="],
+
"prh": ["prh@5.4.4", "", { "dependencies": { "commandpost": "^1.2.1", "diff": "^4.0.1", "js-yaml": "^3.9.1" }, "bin": "bin/prh" }, "sha512-UATF+R/2H8owxwPvF12Knihu9aYGTuZttGHrEEq5NBWz38mREh23+WvCVKX3fhnIZIMV7ye6E1fnqAl+V6WYEw=="],
+ "prop-types": ["prop-types@15.8.1", "", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="],
+
"property-information": ["property-information@5.6.0", "", { "dependencies": { "xtend": "^4.0.0" } }, "sha512-YUHSPk+A30YPv+0Qf8i9Mbfe/C0hdPXk1s1jPVToV8pk8BQtpw10ct89Eo7OWkutrwqvT0eicAxlOg3dOAu8JA=="],
"proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="],
@@ -874,6 +976,10 @@
"react-dom": ["react-dom@19.2.7", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.7" } }, "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ=="],
+ "react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="],
+
+ "react-transition-group": ["react-transition-group@4.4.5", "", { "dependencies": { "@babel/runtime": "^7.5.5", "dom-helpers": "^5.0.1", "loose-envify": "^1.4.0", "prop-types": "^15.6.2" }, "peerDependencies": { "react": ">=16.6.0", "react-dom": ">=16.6.0" } }, "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g=="],
+
"read-package-up": ["read-package-up@11.0.0", "", { "dependencies": { "find-up-simple": "^1.0.0", "read-pkg": "^9.0.0", "type-fest": "^4.6.0" } }, "sha512-MbgfoNPANMdb4oRBNg5eqLbB2t2r+o5Ua1pNt8BqGp4I0FJZhuVSOj3PaBPni4azWuSzEdNn2evevzVmEk1ohQ=="],
"read-pkg": ["read-pkg@9.0.1", "", { "dependencies": { "@types/normalize-package-data": "^2.4.3", "normalize-package-data": "^6.0.0", "parse-json": "^8.0.0", "type-fest": "^4.6.0", "unicorn-magic": "^0.1.0" } }, "sha512-9viLL4/n1BJUCT1NXVTdS1jtm80yDEgR5T4yCelII49Mbj0v1rZdKqj7zCiYdbB0CuCgdrvHcNogAKTFPBocFA=="],
@@ -1104,6 +1210,10 @@
"web-namespaces": ["web-namespaces@1.1.4", "", {}, "sha512-wYxSGajtmoP4WxfejAPIr4l0fVh+jeMXZb08wNc0tMg6xsfZXj3cECqIK0G7ZAqUq0PP8WlMDtaOGVBTAWztNw=="],
+ "weekstart": ["weekstart@1.1.0", "", {}, "sha512-ZO3I7c7J9nwGN1PZKZeBYAsuwWEsCOZi5T68cQoVNYrzrpp5Br0Bgi0OF4l8kH/Ez7nKfxa5mSsXjsgris3+qg=="],
+
+ "whatwg-mimetype": ["whatwg-mimetype@3.0.0", "", {}, "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q=="],
+
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
"word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="],
@@ -1112,6 +1222,8 @@
"write-file-atomic": ["write-file-atomic@1.3.4", "", { "dependencies": { "graceful-fs": "^4.1.11", "imurmurhash": "^0.1.4", "slide": "^1.1.5" } }, "sha512-SdrHoC/yVBPpV0Xq/mUZQIpW2sWXAShb/V4pomcJXh92RuaO+f3UTWItiR3Px+pLnV2PvC2/bfn5cwr5X6Vfxw=="],
+ "ws": ["ws@8.21.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g=="],
+
"xtend": ["xtend@4.0.2", "", {}, "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="],
"yaml": ["yaml@2.8.2", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A=="],
@@ -1124,6 +1236,10 @@
"zwitch": ["zwitch@1.0.5", "", {}, "sha512-V50KMwwzqJV0NpZIZFwfOD5/lyny3WlSzRiXgA0G7VUnRlqttta1L6UQIHzd6EuBY/cHGfwTIck7w1yH6Q5zUw=="],
+ "@cloudscape-design/component-toolkit/weekstart": ["weekstart@2.0.0", "", {}, "sha512-HjYc14IQUwDcnGICuc8tVtqAd6EFpoAQMqgrqcNtWWZB+F1b7iTq44GzwM1qvnH4upFgbhJsaNHuK93NOFheSg=="],
+
+
+
"@tenkacloud/catalog-scanner/typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
"@tenkacloud/simulator-contracts/typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
@@ -1132,12 +1248,15 @@
"async/lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="],
+ "chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
+
"check-ends-with-period/emoji-regex": ["emoji-regex@10.3.0", "", {}, "sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw=="],
"define-data-property/get-intrinsic": ["get-intrinsic@1.2.2", "", { "dependencies": { "function-bind": "^1.1.2", "has-proto": "^1.0.1", "has-symbols": "^1.0.3", "hasown": "^2.0.0" } }, "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA=="],
"define-data-property/gopd": ["gopd@1.0.1", "", { "dependencies": { "get-intrinsic": "^1.1.3" } }, "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA=="],
+
"has-property-descriptors/get-intrinsic": ["get-intrinsic@1.2.2", "", { "dependencies": { "function-bind": "^1.1.2", "has-proto": "^1.0.1", "has-symbols": "^1.0.3", "hasown": "^2.0.0" } }, "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA=="],
"hosted-git-info/lru-cache": ["lru-cache@10.2.0", "", {}, "sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q=="],
@@ -1156,10 +1275,14 @@
"nlcst-pattern-match/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="],
+ "pretty-format/react-is": ["react-is@17.0.2", "", {}, "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="],
+
"prh/diff": ["diff@4.0.2", "", {}, "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A=="],
"prh/js-yaml": ["js-yaml@3.14.1", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": "bin/js-yaml.js" }, "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g=="],
+ "prop-types/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
+
"sentence-splitter/@textlint/ast-node-types": ["@textlint/ast-node-types@13.4.1", "", {}, "sha512-qrZyhCh8Ekk6nwArx3BROybm9BnX6vF7VcZbijetV/OM3yfS4rTYhoMWISmhVEP2H2re0CtWEyMl/XF+WdvVLQ=="],
"set-function-length/define-data-property": ["define-data-property@1.1.4", "", { "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", "gopd": "^1.0.1" } }, "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A=="],
@@ -1170,6 +1293,8 @@
"set-function-name/has-property-descriptors": ["has-property-descriptors@1.0.2", "", { "dependencies": { "es-define-property": "^1.0.0" } }, "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg=="],
+ "slice-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
+
"spellcheck-technical-word/structured-source": ["structured-source@3.0.2", "", { "dependencies": { "boundary": "^1.0.1" } }, "sha512-Ap7JHfKgmH40SUjumqyKTHYHNZ8GvGQskP34ks0ElHCDEig+bYGpmXVksxPSrgcY9rkJqhVMzfeg5GIpZelfpQ=="],
"table/ajv": ["ajv@8.12.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2", "uri-js": "^4.2.2" } }, "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA=="],
diff --git a/bunfig.toml b/bunfig.toml
index cf60bdf..7836397 100644
--- a/bunfig.toml
+++ b/bunfig.toml
@@ -19,7 +19,21 @@ trustedDependencies = []
# 監査時の進捗をログに残し、後追いしやすくする
saveTextLockfile = true
+# 公開から 7 日 (168h) 未満のバージョンを解決しない。.npmrc の
+# min-release-age=168h (npm 系 fallback 用) および CI の Safe Chain
+# minimum package age ゲートと同じ隔離期間を、Bun 自身の解決にも適用する。
+# これが無いと bun install はローカルで最新版を lockfile に固定し、
+# CI の install_ci (Safe Chain) で初めて 403 になる。判断の正本:
+# docs/adr/0014-bun-minimum-release-age.md
+minimumReleaseAge = 604800
+
[test]
+# console の view テスト (Cloudscape + @testing-library/react) が使う DOM 環境。
+# react-dom は load 時に DOM の有無で event system の経路を固定する CommonJS
+# のため、preload で全 module より先に happy-dom を登録する必要がある。
+# setup はネットワーク・ストリーム実装を Bun native へ戻すので、実 HTTP と
+# 実 SQLite を使う他 workspace の behavior テストの経路は変わらない。
+preload = ["./apps/console/test/dom-setup.ts"]
# カバレッジ 100% は Definition of Done (docs/architecture/quality-bar.md / ADR-0003)。
# `bun test --coverage` 実行時に下回ったら失敗させる。閾値は 0-1 の割合で 1 = 100%。
coverageThreshold = 1
diff --git a/docs/adr/0014-bun-minimum-release-age.md b/docs/adr/0014-bun-minimum-release-age.md
new file mode 100644
index 0000000..644ee21
--- /dev/null
+++ b/docs/adr/0014-bun-minimum-release-age.md
@@ -0,0 +1,46 @@
+# ADR-0014: Bun の依存解決に 7 日の minimum release age を適用する
+
+- **Status**: Accepted
+- **Date**: 2026-07-16
+- **Deciders**: Susumu Tomita (@susumutomita)
+
+## Context
+
+[ADR-0001](./0001-supply-chain-hardening.md) の多層防御は、公開直後のパッケージを
+一定期間隔離する「minimum release age」を 2 層で持っている。`.npmrc` の
+`min-release-age=168h` は npm / yarn / pnpm へ fallback した contributor を守り、
+CI の Safe Chain は `make install_ci` 時に公開 7 日未満の tarball ダウンロードを
+403 で遮断する。しかし肝心の Bun 自身の解決には同じ制約が無かったため、
+ローカルの `bun install` は最新版をそのまま lockfile に固定し、CI の Safe Chain で
+初めてブロックされる。実際に console の Cloudscape 移行
+([PR-11](https://github.com/susumutomita/TenkaCloudSimulator/pull/11)) では、
+公開 1〜2 日の `@cloudscape-design/components@3.0.1329` などの 4 パッケージが
+lockfile に入り、CI の install_ci が落ちた。ローカルと CI で解決規則が食い違うと、
+違反は push 後にしか検出できない。
+
+## Decision
+
+root `bunfig.toml` の `[install]` に `minimumReleaseAge = 604800` (168h) を設定し、
+Bun の依存解決自体に隔離期間を適用する。Bun 1.2.20 以降がサポートする公式機能で、
+range 解決 (`^` / `~`) は「公開から 7 日以上経過した最新版」へ自動的に丸められ、
+7 日未満のバージョンを明示 pin した場合は install が失敗して手元で即座に分かる。
+隔離期間は `.npmrc` / Safe Chain と同じ 168h に揃え、3 層が同じ判定を返すようにする。
+緊急のセキュリティパッチ等で例外が必要になった場合は、`minimumReleaseAgeExcludes`
+への追加を ADR で正当化してから行う。
+
+## Consequences
+
+- **Good**: ローカルの `bun install` / `bun update` が CI と同じ判定を返し、
+ 隔離違反が push 前に検出できる。乗っ取り直後の悪性バージョンを掴む窓が閉じる。
+- **Bad**: 新機能・新パッチの取り込みが最大 7 日遅れる。Dependabot が
+ 公開直後のバージョンへ更新する PR は、エージングが済むまで install が失敗する。
+- **Tradeoff**: 隔離無し (従来) は最新版を即座に使えるが、CI で初めて落ちる
+ 非対称性と引き換えだった。隔離期間の長短を再検討するトリガーは、
+ 上流の重大脆弱性修正を 7 日待てないケースが実際に発生したとき。
+
+## References
+
+- 関連コード: `bunfig.toml`
+- 関連 PR / Issue: [PR-11](https://github.com/susumutomita/TenkaCloudSimulator/pull/11)
+- 関連 ADR: [ADR-0001](./0001-supply-chain-hardening.md)
+- 外部資料: [Bun install lifecycle & security 設定](https://bun.com/docs/install/lifecycle)
diff --git a/package.json b/package.json
index fd10c9d..e009761 100644
--- a/package.json
+++ b/package.json
@@ -61,5 +61,10 @@
"textlint-rule-preset-ja-technical-writing": "12.0.2",
"textlint-rule-spellcheck-tech-word": "5.0.0",
"typescript": "7.0.2"
+ },
+ "dependencies": {
+ "@cloudscape-design/collection-hooks": "^1.0.99",
+ "@cloudscape-design/theming-runtime": "^1.0.118",
+ "ws": "^8.21.0"
}
}
diff --git a/tools/workload-runner/test/runner.test.ts b/tools/workload-runner/test/runner.test.ts
index ad54e8d..4a6a219 100644
--- a/tools/workload-runner/test/runner.test.ts
+++ b/tools/workload-runner/test/runner.test.ts
@@ -825,18 +825,23 @@ describe('Docker workload runner', () => {
it.serial(
'起動後に data plane が停止した workload の health probe を typed failure にする',
async () => {
+ // data plane の停止はテスト側から docker exec で指示する。コンテナ内の
+ // 固定 `sleep 2; killall` だと、CI runner の起動レイテンシが 2 秒を超えた
+ // とき start() の health 待ちがレース負けする (Issue 12)。
const started = await runner.start(
workload({
workloadId: 'stopped-data-plane',
command: [
'sh',
'-c',
- 'printf "ready\\n" > /tmp/healthz; httpd -p 8080 -h /tmp; sleep 2; killall httpd; exec sleep 10',
+ 'printf "ready\\n" > /tmp/healthz; httpd -p 8080 -h /tmp; exec sleep 60',
],
})
);
containers.push(started.containerId);
- await Bun.sleep(2200);
+ const killed = docker(['exec', started.containerId, 'killall', 'httpd']);
+ expect(killed.exitCode).toBe(0);
+ await Bun.sleep(250);
await expect(
runner.probe(WORLD_ID, 'stopped-data-plane')
).rejects.toMatchObject({ code: 'WorkloadFailed' });