Skip to content
Open
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
81 changes: 80 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,82 @@
# Kysymys

Kysymys is a platform for training programming.
Kysymys はプログラミング学習・研修のためのプラットフォームです。学習者が問題に向きあい、解答を誰かに見てもらい、その過程を講師が伴走します。

## 何を解決するのか

プログラミングの研修や勉強会では、教材を配って「あとは各自やってみて」で終わりがちです。すると二つのことが見えなくなります。**解答そのもの**と、**解いている途中の様子**です。

提出された解答だけを見ても、その人がどこで何時間つまずいていたかは分かりません。逆に、机を回って様子を見ても、20人が同時に手を動かしていれば誰が本当に手助けを要しているかは埋もれます。Kysymys はこの両方を扱います。解答を submit して受け取り、レビューコメントで返す仕組みと、submit を待たずに作業状況をリアルタイムに集める仕組みを、一つのプラットフォームに収めています。

## 学びのループ

Kysymys の中心には、問題と解答をめぐる循環があります。

先生が Git リポジトリ上に問題を用意し、Kysymys に登録します。学習者は自分の手元でその問題に取り組み、書けたところで手元の Maven プラグインから解答を submit します。submit された解答には先生や仲間がレビューコメントを付けられ、コメントは学習者に通知として届きます。

一人で閉じないのがこのプラットフォームの狙いです。学習者どうしはフォローし合え、フォローしている相手の解答は互いに見えます。他人がどう書いたかを読むこと自体が学びになる、という前提でモデルを組んでいます。誰の解答が誰に見えてよいかはフォロー関係で決まるので、この判定にはユーザ間のつながりの知識が要ります。

問題には生成からアーカイブまでの一生があり、その節目 (作成・更新・アーカイブ) をイベントとして記録します。今の状態を上書きで持つのではなく、状態を変えた出来事を時系列に残す作りなので、「この問題はいつ誰が作り、いつアーカイブされたか」を後から辿れます。

## 研修中の作業状況を見る

解答の submit は結果でしかありません。研修で本当に知りたいのは、今まさに詰まっている人が誰か、です。

Kysymys は参加者のマシンで動く軽量な仕掛けから、作業中の信号を集めます。`mvn` を回すたびのビルド成否、ファイルを編集し続けているかどうかの heartbeat、そして本人からの「詰まった」「解消した」の申告です。これらは参加者ごとに配られたトークンで識別され、Kysymys サーバに送られます。

先生はダッシュボードを開くだけで、参加者が「手助けが要りそうな順」に並んだ一覧をリアルタイムに見られます。ビルドが失敗し続けている人、しばらく手が止まっている人、自分から助けを求めた人が上に来て、色で危険度が分かります。机を回る代わりに、画面を見て声をかけに行けます。

参加者側の準備は演習リポジトリに設定ファイルを二つ置くだけで、常駐プロセスやバイナリの手動配置は要りません。仕組みの詳細と配布手順は [`doc/training-activity-setup.md`](doc/training-activity-setup.md) にまとめています。

## ドメインの区切り

Kysymys は四つの領域に分けて設計しています。将来それぞれを別サービスに切り出せるよう、境界を最初から引いています。

| 領域 | 役割 |
| --- | --- |
| Lesson | 問題の作成、解答の submit、レビューコメント。プラットフォームの核 |
| User | ユーザ登録、プロフィール、フォロー申請などのつながり |
| Avatar | ユーザのアバター画像の生成と表示 |
| Notification | コメントや更新を本人に届ける通知 |

ユーザの情報はどの領域からも必要になりますが、User 以外の領域はユーザの ID しか持ちません。名前やメールアドレスが要るとき (通知メールなど) は、発火元がイベントに必要な情報を載せて渡します。領域どうしを直接参照させず、出来事を介してつなぐ設計です。判断の記録は [`doc/adr/`](doc/adr/) に番号付きで残しています。

## 何でできているか

Kysymys は複数のモジュールの集まりです。サーバ本体に加えて、参加者の手元で動く小さな道具がいくつかあります。

| モジュール | 役割 |
| --- | --- |
| `kysymys-app` | サーバ本体。JSON API として全機能を露出する |
| `kysymys-frontend` | React/TypeScript の SPA。学習者向け画面と講師ダッシュボード |
| `kysymys-maven-plugin` | 学習者が手元から解答を submit するための Maven プラグイン |
| `kysymys-activity-agent` | ビルド成否を送る Maven 拡張。作業状況テレメトリの入口 |
| `kysymys-agent` | ファイル編集の heartbeat を送る常駐ウォッチャー (Go 単一バイナリ) |
| `kysymys-scorer-java` | Java 解答をテストで採点するロジック |

このリポジトリは、kawasima が開発しているフレームワーク群 (Enkan / Kotowari / Raoh / Bouncr) のドッグフーディングの場も兼ねています。教材プラットフォームとして役立てながら、同じコードベースでフレームワークを実際の要件にさらします。技術構成の詳細は [`CLAUDE.md`](CLAUDE.md) と [`doc/adr/004_replatform_to_enkan.md`](doc/adr/004_replatform_to_enkan.md) を参照してください。

## はじめかた

開発用サーバは H2 のインメモリ DB で、追加のセットアップなしに起動できます。

```bash
# サーバ本体を開発モードで起動 (ポート 3000)
cd kysymys-app && mvn exec:java

# フロントエンド (別ターミナル、ポート 5173)
cd kysymys-frontend && npm install && npm run dev
```

研修で作業状況テレメトリまで動かす手順 (トークンの配り方、参加者側の設定) は [`doc/training-activity-setup.md`](doc/training-activity-setup.md) にあります。

## ドキュメント

- [`doc/adr/`](doc/adr/) — 設計判断の記録 (Bounded Context の分割、Problem のライフサイクル、Enkan への全面移行 ほか)
- [`doc/usecase/`](doc/usecase/) — 各操作のユースケース記述
- [`doc/training-activity-setup.md`](doc/training-activity-setup.md) — 研修での作業状況リアルタイム収集の使い方
- [`examples/`](examples/) — 動く演習リポジトリのサンプルと、ループ全体を実コマンドで通す検証スクリプト

## ライセンス

[LICENSE](LICENSE) を参照してください。
65 changes: 65 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# examples — 動く演習と検証

Kysymys の学びのループを、実際に動く演習リポジトリで通すためのサンプルです。二つの役割を
兼ねています。

- **見本**: 研修の講師・学習者が「演習リポジトリはこう作る」とコピーして使える参照実装
- **検証**: `verify.sh` が submit → コメント → 通知 → 採点 → テレメトリ → ダッシュボードまでを
本物のコマンドで一気通貫に通し、README のコンセプトが現状のコードで回ることを確かめる

## 構成

```
examples/
├── fizzbuzz/ 演習問題プロジェクト (研修参加者が clone するもの)
├── bin/mint-jwt dev 用の HS256 JWT 発行ヘルパ
└── verify.sh ループ全体を実コマンドで駆動する検証スクリプト
```

`fizzbuzz/` 単体の使い方 (実装・自己採点・提出) は [`fizzbuzz/README.md`](fizzbuzz/README.md) にあります。
これがそのまま問題文で、kysymys に問題を登録するときの `readmePath` になります。

## 検証を回す

```bash
# 依存する kysymys モジュールを ~/.m2 に入れておく (初回のみ)
mvn -N install -DskipTests
mvn -pl kysymys-scorer-java,kysymys-maven-plugin,kysymys-activity-agent install -DskipTests

# ループを通す (dev サーバが未起動なら verify.sh が mvn exec:java で起動・停止する)
./examples/verify.sh
```

`verify.sh` が通す流れ:

1. dev サーバの応答を待つ (`GET /health`)
2. dev JWT を発行 (teacher / student)
3. 遅延サインアップ (`GET /users`)
4. teacher が問題を登録 (`POST /problems`)
5. student が `mvn kysymys:submit` で解答を提出 (git リモート URL + commit を送る)
6. teacher がレビューコメントを投稿 (`POST /answers/:id/comments`)
7. 通知機構を確認 (フォロー申請 → 対象者へ通知)
8. 手元で自己採点 (`mvn exec:java` → `100`)
9. `mvn kysymys:stuck` / `:resolved` とビルドイベントを送信
10. 講師ダッシュボードへの反映を確認 (`GET /activity/status`)

## スコープ境界と、検証で分かった現状との差

README のコンセプトと今のコードには、いくつか差があります。verify.sh はこれを踏まえて、
実装済みの範囲をグリーンにしつつ差分を出力します。

- **コメントは通知を生成しない (README との差)**。README は「コメントは学習者に通知として
届きます」と述べますが、`PostComment` はイベントを発火せず、`CommentPostedEvent` 自体が
ありません。通知を作る `RecordWhatsNew` は `SubmittedAnswerEvent` (フォロワー宛て) と
`OfferedToFollowEvent` (対象者宛て) にしか反応しません。verify.sh はこの差分を警告として
出し、通知機構そのものはフォロー申請の経路で動作確認します。README どおりにするには、
コメント投稿時にイベントを発火し、`RecordWhatsNew` にコメント通知の分岐を加える必要が
あります。
- **サーバ採点は存在しない**。採点は `kysymys-scorer-java` が学習者の手元で走り、点数を
stdout に出すだけです。submit は解答リポジトリの URL とコミットを記録するだけで、採点は
返しません (`KysymysTestLauncher` の javadoc に明記)。
- **本物の Bouncr スタックは使わない**。`mint-jwt` が dev の HMAC 固定鍵で自前署名します。
本番の Envoy + bouncr-proxy 接続 (移行計画の Sub-C 後半) は未達です。
- **Go 常駐ウォッチャーの heartbeat は best-effort**。verify.sh は Maven 拡張のビルドイベントと
`stuck`/`resolved` の到達までを必須検証とし、Go バイナリ配信 (`GET /agent/<os-arch>`、
サーバの `mvn package` 時にクロスコンパイル) は任意扱いです。
67 changes: 67 additions & 0 deletions examples/bin/mint-jwt
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
#!/usr/bin/env python3
"""dev 用の HS256 JWT を発行する。

kysymys の BouncrBackend は x-bouncr-credential ヘッダの JWT を KYSYMYS_JWT_SECRET
(dev 既定 "kysymys-dev-jwt-secret-not-for-production") で検証し、claims から
UserPermissionPrincipal を作る。読む claim は:

sub -> principal 名 = UserId (必須)
permissions -> 権限の配列 (例 ["TEACHER"] / ["STUDENT"])
uid -> 数値 id (任意)
その他 -> profiles (name / email など)

本物の Bouncr スタックの代わりに研修 dev / verify.sh で使う。本番トークンではない。

使い方:
mint-jwt --sub alice --perm STUDENT --name Alice --email alice@example.com
mint-jwt --sub teacher1 --perm TEACHER
KYSYMYS_JWT_SECRET=... mint-jwt --sub bob # 秘密鍵を環境変数で上書き
"""
import argparse
import base64
import hashlib
import hmac
import json
import os
import sys

DEV_SECRET = "kysymys-dev-jwt-secret-not-for-production"


def b64url(raw: bytes) -> str:
return base64.urlsafe_b64encode(raw).rstrip(b"=").decode("ascii")


def main() -> int:
p = argparse.ArgumentParser(description="Mint an HS256 dev JWT for kysymys.")
p.add_argument("--sub", required=True, help="principal id (sub claim = UserId)")
p.add_argument("--perm", action="append", default=[],
help="permission, repeatable (e.g. --perm TEACHER)")
p.add_argument("--uid", type=int, default=None, help="numeric uid claim (optional)")
p.add_argument("--name", default=None, help="display name (profile claim)")
p.add_argument("--email", default=None, help="email (profile claim)")
p.add_argument("--secret", default=os.environ.get("KYSYMYS_JWT_SECRET", DEV_SECRET),
help="HMAC secret (default: env KYSYMYS_JWT_SECRET or dev secret)")
args = p.parse_args()

header = {"alg": "HS256", "typ": "JWT"}
claims: dict = {"sub": args.sub, "permissions": args.perm}
if args.uid is not None:
claims["uid"] = args.uid
if args.name is not None:
claims["name"] = args.name
if args.email is not None:
claims["email"] = args.email

signing_input = (
b64url(json.dumps(header, separators=(",", ":")).encode())
+ "."
+ b64url(json.dumps(claims, separators=(",", ":")).encode())
)
sig = hmac.new(args.secret.encode(), signing_input.encode(), hashlib.sha256).digest()
print(signing_input + "." + b64url(sig))
return 0


if __name__ == "__main__":
sys.exit(main())
2 changes: 2 additions & 0 deletions examples/fizzbuzz/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
target/
kysymys.properties
13 changes: 13 additions & 0 deletions examples/fizzbuzz/.mvn/extensions.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
研修参加者の演習リポジトリに .mvn/extensions.xml として置く。
これだけで mvn 実行のたびにビルド成否が、ファイル編集のたびに heartbeat が
講師の kysymys サーバに送られる。接続先と本人トークンは隣の kysymys.properties で渡す。
-->
<extensions>
<extension>
<groupId>net.unit8.kysymys</groupId>
<artifactId>kysymys-activity-agent</artifactId>
<version>0.2.0-SNAPSHOT</version>
</extension>
</extensions>
49 changes: 49 additions & 0 deletions examples/fizzbuzz/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# 演習: FizzBuzz

Kysymys の演習問題プロジェクトのサンプルです。この README がそのまま問題文になります
(kysymys に問題を登録するとき `readmePath` に指定します)。

## 課題

`src/main/java/net/unit8/kysymys/example/fizzbuzz/FizzBuzz.java` の `convert(int n)` を実装してください。
1 以上の整数 `n` を受け取り、次の規則で文字列を返します。

- 3 の倍数なら `Fizz`
- 5 の倍数なら `Buzz`
- 3 と 5 の両方の倍数なら `FizzBuzz`
- それ以外はその数の 10 進表記 (例: `1` → `"1"`)

## 自分でテスト・採点する

```bash
# テストを走らせる
mvn test

# 点数 (通ったテストの割合 0〜100) を表示する
mvn -q test-compile exec:java
```

採点はあなたの手元で走ります。kysymys サーバは採点をしません
(提出は解答リポジトリの URL とコミットを記録するだけです)。

## 提出する

`kysymys.properties` に接続先・問題 ID・自分のトークンを記入し、コミットしてから:

```bash
mvn net.unit8.kysymys:kysymys-maven-plugin:submit
```

提出は「今の git リモートの URL + HEAD コミット」をサーバに送ります。事前に
リモートへ push しておいてください。

## 作業状況の共有 (任意)

`.mvn/extensions.xml` が置いてあると、`mvn` を回すたびにビルドの成否が、ファイルを
編集するたびに heartbeat が講師の kysymys サーバに送られます。詰まったとき・解消した
ときは自分から知らせられます。

```bash
mvn net.unit8.kysymys:kysymys-maven-plugin:stuck # 「詰まった」
mvn net.unit8.kysymys:kysymys-maven-plugin:resolved # 「解消した」
```
11 changes: 11 additions & 0 deletions examples/fizzbuzz/kysymys.properties.template
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# 演習リポジトリのルート (pom.xml と同じ階層) に kysymys.properties として置く。
# 値は研修主催者から配布される。

# 講師の kysymys サーバ
kysymys.url=http://INSTRUCTOR_HOST:3000

# 取り組む問題の ID
kysymys.problemId=PROBLEM_ID

# 参加者ごとに配布される Bouncr JWT (これで本人として識別される)
kysymys.token=YOUR_JWT_HERE
69 changes: 69 additions & 0 deletions examples/fizzbuzz/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!--
演習問題プロジェクト (FizzBuzz)。

研修参加者はこのリポジトリを clone し、src/main の FizzBuzz を実装して
`mvn net.unit8.kysymys:kysymys-maven-plugin:submit` (省略形 `mvn kysymys:submit`)
で kysymys サーバに提出する。ビルドのたびに .mvn/extensions.xml の
kysymys-activity-agent が作業状況を送る。

自己完結の standalone プロジェクトで、kysymys-parent の子ではない (研修参加者は
kysymys 本体を clone しない)。依存する SNAPSHOT (scorer / plugin / activity-agent) は
研修主催者が事前に配布した ~/.m2 のものを解決する。
-->
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>net.unit8.kysymys.example</groupId>
<artifactId>fizzbuzz</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>

<name>kysymys-example-fizzbuzz</name>
<description>Kysymys 演習問題のサンプル: FizzBuzz</description>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.release>17</maven.compiler.release>
<!-- 研修主催者が配布した kysymys モジュール群のバージョン -->
<kysymys.version>0.2.0-SNAPSHOT</kysymys.version>
<junit.version>5.10.2</junit.version>
</properties>

<dependencies>
<!-- 採点ロジック。KysymysTestLauncher が junit-platform を走らせて点数を stdout に出す -->
<dependency>
<groupId>net.unit8.kysymys</groupId>
<artifactId>kysymys-scorer-java</artifactId>
<version>${kysymys.version}</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.2.5</version>
</plugin>
<!-- `mvn -Pscore test-compile exec:java` で自己採点 (点数を stdout に出力) -->
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>3.2.0</version>
<configuration>
<mainClass>net.unit8.kysymys.example.fizzbuzz.Scoring</mainClass>
<classpathScope>test</classpathScope>
</configuration>
</plugin>
</plugins>
</build>
</project>
Loading