# Transable 教材連携 — LLM 実装仕様（このファイルだけで完了）

| 項目 | 内容 |
|------|------|
| **更新** | 2026-08-12（G3 · english-word-order G3-app 承認後の実装正本） |
| **読者** | 実装を行う LLM / コーディングエージェント（人間の先生は読まない） |
| **ゴール** | 対象 Web アプリが Transable の iframe 子として学習イベントを送れ、末尾チェックリストがすべて ✅ になる |
| **参考実装（任意）** | [PICKS](https://lifelonglearning777.github.io/picks/)（第1号）· `english-word-order`（第2号・手元更新可）· `claude-word-order-quiz`（第3号・受け口準備済み · Bridge 差し替え待ち） |
| **人間向け規格整理** | Author Profile 0.5（アーキテクチャ・Transable 内正本）。**本ファイルがアプリ実装の正本（wire v1）** |

この Markdown **1 本に実装に必要な契約はすべて含まれる。追加資料は不要。**  
チェックリストがすべて ✅ なら、アプリ側の連携タスクは完了である。Transable へのカタログ登録やクラス配布は本仕様の対象外（別オペレーション）。

---

## 0. 作業指示（エージェント向け）

1. 本ファイル全文を読む（必要なら対象アプリの既存コードも読む）。  
2. 最小差分で Bridge・必須イベント・マニフェストを実装する。UI / 採点 / SRS の大規模リファクタはしない。  
3. 末尾の受け入れチェックリストを自己確認し、未達があれば直す。  
4. 最終回答でチェックリストの各項目を ✅ / ❌ で報告する。

人間側の依頼文の例（コピー用）:

```text
添付の仕様を読んで、このアプリを Transable 教材連携できるようにしてください。
終わったら「受け入れチェックリスト」を一つずつ確認し、
未達があれば直して、全部チェックできる状態にしてください。
```

---

## 0.1 位置づけ（G1 · 人間向け注記）

アプリが送るのは **Author Event（wire）** であり、xAPI Statement / Caliper Event ではない。  
Identity・Session・Consent・標準形式への変換は **Transable 親**が行う。子に学習者 ID・LRS・Caliper JSON-LD を持たせない。

実装面では本ファイルの `postMessage` 契約で足りる。将来、Typed Author API（`startActivity` / `recordAttempt` / `completeActivity`）へ寄せる場合も、**wire の `event_type` 名と `schema_version` 1 は維持**する想定である（G3-app で個別承認）。

やってはいけないこと（要約）:

- Author Event を「xAPI 互換」「Caliper 準拠」と呼ぶ  
- 氏名・メール・learner_id を送る・保存する  
- Activity 完了時に「全体 success」を勝手に定義して送る（必須は 1 問ごとの `result.success`）  
- キー逐次ログや正式成績確定のための送信  

---

## 1. 役割分担

| 担当 | 内容 |
|------|------|
| Transable（親） | ログイン・名簿・iframe 起動・学習記録の保存 |
| 対象アプリ（子） | 問題・採点・画面。学習の「事実」だけを `postMessage` で親へ送る |

```text
利用者は Transable にログイン
        │
        ▼
Transable が対象アプリを iframe で開く
        │
        ▼
アプリは「何問やった・どれができた・どこでつまずいた」だけを送る
        │
        ▼
記録は Transable のアカウントに残る
```

採点結果はブラウザからの報告であり、正式成績の根拠にはしない前提で設計する。

---

## 2. やってよいこと / やってはいけないこと

### やってよい

- 問題データ・採点・ヒント・自信度・タイマーなどの教材 UI  
- 単独でも動く（GitHub Pages 等やローカル静的ホスト）。連携は embed 時だけ有効  
- 鍵・MASTER・ストリーク・SRS などを端末の localStorage に持つ（任意・Transable には送らない）  
- 1 問が終わったときの集約値（正誤・所要時間など）  

### やってはいけない

| 禁止 | 理由 |
|------|------|
| アプリに学校 SSO や独自のログインを足す | 認証は常に Transable |
| 氏名・メール・生徒 ID を受け取る・送る・保存する | 個人情報は親だけ |
| Transable の REST API を直接叩く | 親が代行する |
| `postMessage(..., "*")` | 危険 |
| キー入力の一文字ずつログを送る | 過剰収集 |
| 「Transable 上の mastery」を送る | ゲーム状態 ≠ 共通熟達度 |
| 正式成績の唯一の根拠にする前提で設計する | ブラウザ報告は改ざんされうる |
| 一覧用サムネ画像を必須にする | 定型カードが既定 |

---

## 3. 最小契約（これだけ実装すればつながる）

### 3.1 起動

```text
https://アプリの起動URL/?embed=1&parent_origin=https://www.transable.net
```

- `embed=1` または `window.parent !== window` のときだけ連携を有効にする  
- 単独で開いたときは従来どおり（連携コードは動かさない）  
- `parent_origin` は親が付与する。子は **許可リストに一致したときだけ** その値を `postMessage` の targetOrigin に使う  

許可する親 origin（`"*"` 禁止）:

| origin | 備考 |
|--------|------|
| `https://www.transable.net` | 本番（必須） |
| `https://transable.net` | 本番（www なし。両方許可すること） |
| `http://localhost:3000` | Lab 確認用（推奨。無いとローカルで handshake しない） |

上記以外の `parent_origin` では連携イベントを送らない（それでも `"*"` は使わない）。  
**www なしだけ許可すると、本番で連携が開始されない。**

### 3.1.1 第3号（`claude-word-order-quiz`）を直すとき

- イベント / マニフェストの `provider` / `provider_key` は **必ず `claude-word-order-quiz`**  
- Claude 公開 Artifact だけでは Transable から iframe できない（CSP）。納品は静的 HTML + `transable-app.manifest.json`（同一ディレクトリ）  
- Transable 側の差し替え先: `public/external-apps/claude-word-order-quiz/`（または別 HTTPS ホスト）  
- 親の受け口（カタログ・Event・sync-manifest）は準備済み。チェックリスト ✅ 後、差し替えのみで Lab 記録が動く

### 3.2 Handshake（`bridge.init` は無い）

1. 子 → 親: `bridge.ready`  
2. 親 → 子: `bridge.ack`  
3. 以降、学習イベントを送る  

### 3.3 必須イベント 3 種

| いつ | `event_type` |
|------|----------------|
| 出題セットと mode が決まった直後 | `session.started` |
| 1 問の結果が確定したとき | `item.attempt.completed` |
| 完了・提出完了 | `session.completed` |

任意: `item.hint.used` · `item.confidence.answered` · `session.abandoned`  
任意推奨: 親の `bridge.practice_ended` を受けたら「続きをやる」→ `ui.continue_requested`  
（**継続** = 新 Session。途中の同一 Session **再開**は契約未整備のため送らない。）

Manifest `capability.event_types` には **学習 Event のみ**を書く。`bridge.ready` / `ui.continue_requested` 等の Bridge 制御は `capability.bridge_messages`（任意）へ。同じ一覧に混ぜない。

### 3.4 メッセージ共通形

```json
{
  "channel": "transable.external_learning",
  "schema_version": 1,
  "event_type": "item.attempt.completed",
  "occurred_at": "2026-08-10T02:15:30.000Z",
  "idempotency_key": "（UUID）",
  "provider": "your-app-id",
  "content_version": "1",
  "activity": {
    "external_activity_id": "unit-1",
    "title": "任意の単元名"
  },
  "payload": {}
}
```

- `provider` は英小文字・ハイフン可（例: `picks`）  
- **`learner_id` / `user_id` / `email` を入れない**  
- `channel` は必ず `"transable.external_learning"`（変更しない）  
- `schema_version` は `1`

### 3.5 `item.attempt.completed` の payload 例

```json
{
  "item_id": "we-enjoyed-talking-in-english",
  "legacy_item_id": "L1-U1-3",
  "mode": "write",
  "result": {
    "success": false,
    "response": "We enjoyed talk in English.",
    "canonical_answer": "We enjoyed talking in English.",
    "error_types": ["grammar"],
    "hint_level": 1,
    "confidence": null,
    "duration_sec": 42
  }
}
```

### 3.6 問題 ID（最重要）

配列位置の ID（`L1-U1-1`）を主キーにしない。  
英文などに基づく**不変** `item_id` を使う。旧 ID は `legacy_item_id` で併記可。

### 3.7 iframe

`X-Frame-Options: DENY` / `SAMEORIGIN` や CSP `frame-ancestors 'none'` で埋め込みを拒否しない。

### 3.8 表示用マニフェスト（一覧カード用）

起動 URL と同じディレクトリに `transable-app.manifest.json` を置く。  
**サムネ画像は不要**（Transable が定型カードを描く）。

```json
{
  "schema_version": 1,
  "provider_key": "your-app-id",
  "name": "アプリの短い名前",
  "activity_title": "利用者が取り組む活動の名前",
  "description": "このアプリで何をするか。学習のようすが Transable に残る旨を短く。",
  "locale": "ja"
}
```

任意: `author.display_name` · `thumbnail_url`（実画像で上書きしたいときだけ）  
`provider_key` はイベントの `provider` と同じ値にする。

既存アプリで Bridge が既にある場合は、マニフェスト追加だけで一覧表示用メタが足りる。

> **Manifest v2（任意）:** `schema_version: 2` + `capability` / `data_declaration` は親が受理できるが、**現状のチェックリスト完了条件ではない**（v1 で足りる）。必須化は個別アプリの G3-app 承認後。

---

## 4. ホスト・配信（開発中の前提）

本仕様の完了条件に「すでに本番公開済み」「Transable カタログ登録済み」は含まれない。

実装中は次のいずれかでよい。

- ローカル静的サーバ  
- プレビュー URL / GitHub Pages 等  

HTTPS で配信できると本番と同条件で試しやすいが、**チェックリスト完了はコードとマニフェストの充足で判定する。**  
Transable 側への登録・クラス配布は別オペレーションであり、本ファイルのタスク完了とは切り離す。

---

## 5. 受け入れチェックリスト（これがゴール）

- [ ] 単独起動で従来どおり動く（連携オフ）  
- [ ] `embed=1`（または iframe 内）のときだけ Bridge が動く  
- [ ] Handshake: `bridge.ready` → `bridge.ack`（`bridge.init` なし）  
- [ ] `session.started` / `item.attempt.completed` / `session.completed` を送る  
- [ ] 各問題に不変 `item_id` がある  
- [ ] 各イベントに `idempotency_key`（UUID）がある  
- [ ] `channel` が `"transable.external_learning"`、`schema_version` が `1`  
- [ ] `postMessage` の第2引数が `"*"` ではない（許可 origin のみ）  
- [ ] 親 origin 許可に `https://www.transable.net` と `https://transable.net` の両方を含めている（Lab 用に `http://localhost:3000` も推奨）  
- [ ] 氏名・メール・learner_id / user_id を送っていない・保存していない  
- [ ] キー逐次ログを送っていない  
- [ ] Transable API を子から直接呼んでいない  
- [ ] iframe 埋め込みを拒否していない  
- [ ] `transable-app.manifest.json` がある（`name` / `activity_title` / `description`。サムネ不要）  
- [ ] `provider` と `provider_key` が一致（英小文字）  
- [ ] （任意）ヒント・自信度・中断を送っている  

**全部 ✅ ＝ アプリ側の連携準備は完了。追加資料は不要。**

---

## 6. 実装時の注意（よくある失敗）

- ホスト専用のスコア表を仮定しない  
- Transable Mastery を自前判定して送らない  
- UI・採点・SRS の大規模リファクタをしない（結果ログの集中点に最小フック）  
- `description` に HTML を入れない  
- マニフェストを起動 HTML と別パスに置かない（同じディレクトリ）  
