Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 80 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
name: Test

on:
pull_request:
push:
branches:
- "**"
workflow_call:

permissions:
contents: read

jobs:
checks:
name: Static and unit checks
runs-on: ubuntu-24.04
timeout-minutes: 15

steps:
- name: Check out source
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- name: Set up Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 22
cache: npm
cache-dependency-path: apps/lab-guide/package-lock.json

- name: Set up Python
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
with:
python-version: "3.13"

- name: Install guide dependencies
run: npm ci --prefix apps/lab-guide

- name: Run repository checks
run: npm run check

- name: Check Bash syntax
shell: bash
run: |
set -Eeuo pipefail
checked=0
while IFS= read -r -d '' file; do
if head -n 1 "${file}" | grep -Eq '^#!.*(/|env )(ba)?sh([[:space:]]|$)'; then
bash -n "${file}"
checked=$((checked + 1))
fi
done < <(find labs/open-world-target -type f -print0)
((checked > 0))
printf 'Checked %d shell scripts.\n' "${checked}"

- name: Check PHP syntax
shell: bash
run: |
set -Eeuo pipefail
command -v php >/dev/null
checked=0
while IFS= read -r -d '' file; do
php -l "${file}"
checked=$((checked + 1))
done < <(find labs/open-world-target -type f -name '*.php' -print0)
((checked > 0))
printf 'Checked %d PHP files.\n' "${checked}"

- name: Check C fixture syntax
shell: bash
run: |
set -Eeuo pipefail
checked=0
while IFS= read -r -d '' file; do
cc -Wall -Wextra -Werror -fsyntax-only "${file}"
checked=$((checked + 1))
done < <(find labs/open-world-target -type f -name '*.c' -print0)
((checked > 0))
printf 'Checked %d C files.\n' "${checked}"
9 changes: 9 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
node_modules/
dist/
coverage/
*.log
.DS_Store
__pycache__/
*.py[cod]
.pytest_cache/
.mypy_cache/
180 changes: 180 additions & 0 deletions DESIGN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
---
version: "2.0"
name: "ExamServer Open World — Playable Operations Board"
description: "Three authored visual modes for a beginner cyber range, bound by one learning hierarchy."
colors:
primary: "#007F90"
primary-hover: "#006470"
canvas: "#F1EEE5"
surface: "#FFFDF5"
surface-muted: "#E8E3D8"
text: "#151410"
text-muted: "#625F56"
border: "#171611"
success: "#087B6F"
warning: "#E1A800"
danger: "#DD3F3F"
typography:
display:
fontFamily: "Noto Sans JP"
fontSize: "2rem"
fontWeight: 850
lineHeight: "1.25"
letterSpacing: "-0.03em"
body:
fontFamily: "Noto Sans JP"
fontSize: "1rem"
fontWeight: 400
lineHeight: "1.75"
letterSpacing: "0px"
label:
fontFamily: "Noto Sans JP"
fontSize: "0.875rem"
fontWeight: 700
lineHeight: "1.45"
letterSpacing: "0px"
mono:
fontFamily: "Geist Mono"
fontSize: "0.8125rem"
fontWeight: 600
lineHeight: "1.4"
rounded:
sm: "2px"
md: "4px"
lg: "6px"
spacing:
xs: "0.5rem"
sm: "0.75rem"
md: "1rem"
lg: "1.5rem"
xl: "2rem"
components:
button-primary:
backgroundColor: "{colors.primary}"
textColor: "#FFFFFF"
rounded: "{rounded.sm}"
height: "44px"
button-secondary:
backgroundColor: "{colors.surface}"
textColor: "{colors.text}"
rounded: "{rounded.sm}"
height: "44px"
---

# ExamServer Open World Design Contract

## Design thesis

このサイトは「説明を読む教材」ではなく、攻撃経路を自分で切り開くプレイアブルな作戦盤である。良い見た目の条件は装飾量ではない。2秒で現在地が分かり、10秒で次の仮説を比較でき、選んだ行動と発見の因果が記憶に残ることを品質基準とする。

認知負荷はユーザーが求めた時だけ上げる。初期画面には現在の状況、現在の目標、次の主要操作だけを残し、補助情報は名称と件数が分かる「ラベル付きの引き手」として待機させる。引き手を操作した時だけ一枚のdrawerを開き、別の引き手を操作したら中身を入れ替える。

三つの見た目は配色プリセットではない。情報の意味、操作、状態を共有しながら、異なる構図・書体・境界・動きで同じ世界を演出する。

## Visual targets

- `docs/design/themes/play-mission-deck.png` — PLAY / Mission Deck
- `docs/design/themes/ops-night-circuit.png` — OPS / Night Circuit
- `docs/design/themes/focus-field-notes.png` — FOCUS / Field Notes
- 旧 `docs/design/exploration-map.png` と `docs/design/situation-consultation.png` は情報構造の参考であり、色・密度・雰囲気の正本ではない。

生成画像内のサンプル文言、数値、誤字、架空の機能は仕様ではない。実装は実データ、既存の安全境界、アクセシビリティを優先する。

## What “award-quality” means here

### 1. One memorable idea

主役は常に一本の攻撃経路である。地図、事実、次の選択、最近の発見が一つの因果へ収束し、KPIカードの寄せ集めにはしない。

### 2. Hierarchy before decoration

- 常時表示: 画面名、現在の目標、世界/仮説、次の主要操作
- 引き手: ライブ接続、確定した事実、次の調査/ヒント、最近の発見、見た目
- 開いた時: 選択した一種類だけをdrawerへ表示する
- 緊急表示: 接続断や更新失敗だけは現在の作業を失わせず主画面へ通知する

見出し、選択地点、主ボタンだけが最強のコントラストを持つ。補助文、時刻、接続状態は一段静かにする。件数は「中に何があるか」を予告するために使い、進捗カードやKPI列にはしない。

### 3. Typography is the interface

- PLAY: `Noto Sans JP` の太い見出し。短く、勢いがあり、読み間違えない。
- OPS: `Noto Sans JP` と `Geist Mono`。日本語本文をコード風に崩さず、状態と時刻だけをmonoにする。
- FOCUS: `Noto Serif JP` の見出しと `Noto Sans JP` の本文。余白と行間で読む順を作る。
- 画像化された文字、極小文字、装飾だけの英語を主要情報に使わない。

### 4. Geometry carries state

発見済み、選択中、未発見は色だけで区別しない。実線/強調枠/破線、icon、label、focus ringを必ず併用する。未発見名や答えをDOMへ先出ししない。

### 5. Motion confirms cause

- 150–220ms、`cubic-bezier(0.16, 1, 0.3, 1)` を基本にする。
- hoverは2–3px以内の移動、選択はborderとshadowの変化、経路は控えめなdash移動まで。
- 常時点滅、無目的なparallax、スクロールジャック、Matrix rainを使わない。
- `prefers-reduced-motion` では即時表示にする。

### 6. Empty space is functional

余白は「未実装」を隠すためでなく、事実・地図・選択の境界を明確にするために使う。画面を同寸カードで埋めず、中央の経路へ最大の面積を渡す。

## Three authored modes

### PLAY / Mission Deck

ポップゲーム。黒いインク、温かい紙、electric cyan、acid yellow、coralで、クエスト盤の触感を作る。太い境界、ハードシャドウ、mission番号、選択地点の黒いbriefを使う。成人向けのグラフィックデザインであり、mascot、emoji、子供向けtoy表現、意味のない報酬演出は使わない。初回既定値はPLAY。

### OPS / Night Circuit

ハッカー風。midnight、phosphor mint、amber、ultravioletでライブ運用面を作る。細いgrid、mono metadata、発光を抑えたroute trace、角の小さいsurfaceを使う。green-code壁紙、Matrix rain、skull、過剰なneon、読みにくい全mono本文は使わない。

### FOCUS / Field Notes

シンプル。warm white、黒いink、signal vermilion、quiet cobaltで編集されたフィールドノートを作る。罫線、非対称の余白、明確な文字組みを使い、surface chromeを最小化する。単なるbeige SaaS、巨大な空白、情報を隠すミニマリズムにはしない。

## Shared interaction contract

- Headerの `見た目 PLAY / OPS / FOCUS` という一つの引き手からテーマを即時切替できる。三択を初期表示しない。
- 選択は `aria-pressed` で伝え、44px相当の操作面と明確なfocus ringを持つ。
- 選択テーマはversion付きlocalStorageへ保存する。保存不能でもPLAYで継続する。
- テーマ切替で画面、地図選択、仮説、ヒント、セッション状態を初期化しない。
- 色テーマと公開ガイドはローカルbundleだけで動く。ライブ教材イベントは同一originのクラウドAPIから受け、外部fontや第三者UI SDKは要求しない。
- 補助情報のdrawerは同時に一枚だけ開く。`Escape` と背景操作で閉じ、閉じた後は元の引き手へfocusを戻す。
- drawer内へkeyboard focusを閉じ込め、見えていない補助情報をtab順へ残さない。
- 新しい教材イベントを受けてもdrawerを勝手に開かない。引き手の件数と主画面の目標だけを更新する。

## State language

- Browse: 接続エラーにせず、世界観、遊び方、必要機材、安全境界と接続入口を表示する。
- Waiting: ペアリング済みだがBridge未到着。何を待っているかと再接続方法を示す。
- Loading: 最後の確定状態を残し、更新中と書く。
- Empty: 有線接続と入口確認だけを示す。
- Live: 新しい教材イベントで、地図の経路、現在目標、引き手の件数を更新する。開いているdrawerが該当する場合だけ中身も更新し、閉じている補助情報を勝手に展開しない。
- Reconnecting: 操作を失わせず、自動再接続と手動再読込を出す。
- Selected: border、icon、label、地点briefの4点で示す。
- Locked hint: 開放条件を日常語で示す。
- Success: rootまでの一本の経路と権限変化の振り返りを主役にする。
- Telemetry unavailable: 自動検出不能を明示する。flag手動提出は標的直結のローカル表示だけに出し、クラウド表示には出さない。

## Live transition contract

- 新しい事実では、まずイベント列へ追加し、続いて対応ノードと経路、最後に目標と選択肢を150-220msで更新する。
- ページ全体を再読込せず、現在の画面、選択地点、開いたヒント、テーマを維持する。
- 複数イベントを受けてもrevision順だけを採用し、古いsnapshotで画面を巻き戻さない。
- `prefers-reduced-motion`では同じ情報を即時更新し、色だけに頼らずラベルと形状も変える。
- BrowseからLiveへの切替は同じ世界へ接続したと理解できる連続した遷移にし、別アプリへ飛んだ印象を作らない。

## Responsive contract

- 主基準: 1366×768。検証: 1672×941、1280×720、360×800。
- 900px未満でも初期画面へ補助情報を縦積みしない。引き手は横方向に短く並べ、選んだ情報だけをbottom sheetとして表示する。
- 狭いheaderでは現在テーマだけを残し、三択は `見た目` の引き手を開いた後に表示する。
- 横スクロール、sticky headerによるfocus隠れ、44px未満の主要操作を許さない。

## Rejected directions

- generic dashboard、KPI、ranking、streak、trophy、card soup
- 事実、行動、履歴、接続、設定をすべて常時表示するcontrol-room layout
- glassmorphism、巨大な角丸container、無意味なgradient
- fake terminal、code rain、stock hacker photography
- chat人格、自由文AI prompt、仮説より先に完全commandを見せること
- runtimeで生成画像を背景として読み込み、offline bundleを重くすること
Loading
Loading