@kubohiroya/turbowarp-webrtc-qrcode-pairing 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/LICENSE +373 -0
  2. package/README.ja.md +122 -0
  3. package/README.md +414 -0
  4. package/dist/extension-manifest.json +318 -0
  5. package/dist/webrtc-qrcode-pairing.js +4064 -0
  6. package/docs/architecture.ja.md +57 -0
  7. package/docs/architecture.md +65 -0
  8. package/docs/design/qr-pairing/README.md +16 -0
  9. package/docs/design/qr-pairing/architecture.md +191 -0
  10. package/docs/design/qr-pairing/block-api.md +123 -0
  11. package/docs/design/qr-pairing/dataflow.md +202 -0
  12. package/docs/design/qr-pairing/extraction-plan.md +134 -0
  13. package/docs/design/qr-pairing/interfaces.ts +473 -0
  14. package/docs/design/qr-pairing/requirements.md +152 -0
  15. package/docs/design/qr-pairing/test-plan.md +126 -0
  16. package/docs/index.html +34 -0
  17. package/docs/initialization-issue.ja.md +34 -0
  18. package/docs/integration-guide.ja.md +245 -0
  19. package/docs/integration-guide.md +258 -0
  20. package/docs/migration.ja.md +110 -0
  21. package/docs/migration.md +115 -0
  22. package/package.json +90 -0
  23. package/schemas/extension-manifest.schema.json +49 -0
  24. package/src/block-definitions.json +264 -0
  25. package/src/clock.ts +18 -0
  26. package/src/config/feature-flags.ts +18 -0
  27. package/src/config/qr-config.ts +22 -0
  28. package/src/config.ts +12 -0
  29. package/src/errors.ts +51 -0
  30. package/src/extension-manifest.ts +149 -0
  31. package/src/extension.ts +281 -0
  32. package/src/globals.d.ts +54 -0
  33. package/src/index.ts +8 -0
  34. package/src/pairing/controller.ts +836 -0
  35. package/src/pairing/limits.ts +12 -0
  36. package/src/pairing/types.ts +96 -0
  37. package/src/ports/display.ts +136 -0
  38. package/src/ports/qr-scan.ts +151 -0
  39. package/src/ports/webrtc.ts +61 -0
  40. package/src/qr/courier.ts +267 -0
  41. package/src/qr/envelope.ts +0 -0
  42. package/src/qr/hash.ts +13 -0
  43. package/src/qr/limits.ts +44 -0
  44. package/src/qr/svg.ts +34 -0
  45. package/templates/README.ja.md +183 -0
  46. package/templates/README.md +198 -0
@@ -0,0 +1,57 @@
1
+ # アーキテクチャ
2
+
3
+ [English](architecture.md)
4
+
5
+ 使い方: [利用ガイド](integration-guide.ja.md) · [移行と切戻し](migration.ja.md)。
6
+ 設計の記録: [docs/design/qr-pairing](design/qr-pairing/README.md)。
7
+
8
+ ## 実行時の構造
9
+
10
+ ```text
11
+ extension.ts ブロックのファサード。引数のCastと委譲、runtimeリスナの保持
12
+ pairing/ sessionの状態機械。phase、epoch、期限、資源解放
13
+ qr/ 純粋な搬送処理。envelope、分割、再構成、検証、SVG生成
14
+ ports/ アダプタ。WebRTC能力、カメラ走査、スプライトのskin
15
+ errors.ts 搬送層とドメイン層が共有するエラーコード
16
+ ```
17
+
18
+ `qr/`は`qrcode`と`errors.ts`以外に依存しません。DOM、Scratch、WebRTCへの依存が無いため、
19
+ 搬送処理はruntimeなしで、かつフィーチャーフラグと独立して検証できます。`pairing/`は外部へ
20
+ portのinterface経由でのみ到達します。`turbowarp-webrtc`のcapability v3が未公開の間も、
21
+ フェイクを注入して往復をテストできるのはこのためです。
22
+
23
+ 併用拡張は読込み時ではなく呼び出し時に解決します。不在の場合は、読込みの失敗ではなく、それを
24
+ 必要としたブロックのエラーコードとして現れます。
25
+
26
+ ## ビルド出力
27
+
28
+ このプロジェクトは実行時の動作と互換性メタデータを分離し、リポジトリに保存された同じソース定義から両方を生成します。
29
+
30
+ ```text
31
+ src/index.ts + src/extension.ts
32
+ -> vite-plugin-turbowarp-extension
33
+ -> dist/<extension>.js
34
+
35
+ src/config.ts + src/block-definitions.json
36
+ -> extension-api-manifest Viteプラグイン
37
+ -> dist/extension-manifest.json
38
+ ```
39
+
40
+ manifestプラグインはViteのビルド後フェーズで実行されます。これにより、JavaScriptプラグインの単一出力検証を維持しながら、TurboWarpバンドルの完成後にだけmanifestを追加します。
41
+
42
+ ## 拡張機能API manifest v1
43
+
44
+ `schemas/extension-manifest.schema.json`が規範となるJSON Schemaです。`formatVersion`は`1`で、互換性のないmanifest形式を導入するときに変更する必要があります。
45
+
46
+ v1契約は次の情報を含みます。
47
+
48
+ - TurboWarp拡張機能のID
49
+ - 各ブロックのopcodeとブロック種類
50
+ - 各引数のID、引数種類、任意のメニュー参照
51
+ - 各メニューのIDとReporterブロックを受け付けるかどうか
52
+
53
+ ブロック、引数、メニューは、シリアライズ前に識別子で並べ替えられます。テキスト、説明、既定値、静的メニュー項目は、保存済みプロジェクトのAPI参照を識別しないため、意図的に除外しています。そのため互換性チェッカーは、API変更とドキュメントまたはローカライズの変更を区別できます。
54
+
55
+ ## 差分の検出
56
+
57
+ `dist/`はリリース成果物としてコミットされます。`npm run check:dist`は両方のファイルを再ビルドし、`dist/`配下に変更、削除、未追跡ファイルがある場合に失敗します。これにより、ローカル検証とCIの両方でmanifestとバンドルの差分を検出できます。
@@ -0,0 +1,65 @@
1
+ # Architecture
2
+
3
+ [日本語](architecture.ja.md)
4
+
5
+ Using the extension: [Integration guide](integration-guide.md) ·
6
+ [Migration and rollback](migration.md). Design records: [docs/design/qr-pairing](design/qr-pairing/README.md).
7
+
8
+ ## Runtime structure
9
+
10
+ ```text
11
+ extension.ts block facade: casts arguments, delegates, owns runtime listeners
12
+ pairing/ session state machine: phases, epochs, deadlines, resource release
13
+ qr/ pure transport: envelope, splitting, reassembly, verification, SVG
14
+ ports/ adapters: WebRTC capability, camera scanning, sprite skins
15
+ errors.ts error codes shared by the transport and the domain
16
+ ```
17
+
18
+ `qr/` depends on nothing but `qrcode` and `errors.ts`: it has no DOM, Scratch, or WebRTC
19
+ dependency, so the transport can be tested without a runtime and independently of the feature flag.
20
+ `pairing/` reaches the outside world only through the port interfaces, which is what lets the round
21
+ trip be tested with a fake WebRTC capability while `turbowarp-webrtc` capability v3 is unpublished.
22
+
23
+ Companion extensions are resolved at call time, never at load time: a missing one becomes a specific
24
+ error code on the block that needed it, rather than a failure to load.
25
+
26
+ ## Build outputs
27
+
28
+ The project keeps runtime behavior and compatibility metadata separate while generating both from
29
+ the same checked-in source definitions.
30
+
31
+ ```text
32
+ src/index.ts + src/extension.ts
33
+ -> vite-plugin-turbowarp-extension
34
+ -> dist/<extension>.js
35
+
36
+ src/config.ts + src/block-definitions.json
37
+ -> extension-api-manifest Vite plugin
38
+ -> dist/extension-manifest.json
39
+ ```
40
+
41
+ The manifest plugin runs in Vite's post-build phase. This preserves the JavaScript plugin's
42
+ single-output validation and adds the manifest only after the TurboWarp bundle is complete.
43
+
44
+ ## Extension API manifest v1
45
+
46
+ `schemas/extension-manifest.schema.json` is the normative JSON Schema. `formatVersion` is `1` and
47
+ must change when an incompatible manifest shape is introduced.
48
+
49
+ The v1 contract contains:
50
+
51
+ - the TurboWarp extension ID;
52
+ - each block opcode and block type;
53
+ - each argument ID, argument type, and optional menu reference;
54
+ - each menu ID and whether it accepts reporter blocks.
55
+
56
+ Blocks, arguments, and menus are sorted by their identifiers before serialization. Text,
57
+ descriptions, default values, and static menu items are intentionally excluded because they do not
58
+ identify saved-project API references. A compatibility checker can therefore distinguish API
59
+ changes from documentation or localization changes.
60
+
61
+ ## Drift detection
62
+
63
+ `dist/` is committed as a release artifact. `npm run check:dist` rebuilds both files and fails when
64
+ Git reports any modified, deleted, or untracked file below `dist/`. This catches manifest and bundle
65
+ drift in local checks and CI.
@@ -0,0 +1,16 @@
1
+ # QR搬送WebRTCペアリング 設計文書
2
+
3
+ 出典: [Issue #1](https://github.com/kubohiroya/turbowarp-webrtc-qrcode-pairing/issues/1)
4
+
5
+ | 文書 | 内容 |
6
+ |---|---|
7
+ | [requirements.md](requirements.md) | Issue #1の要求仕様の整理(FR/NFR/DoD)と、設計時に判明した制約 |
8
+ | [architecture.md](architecture.md) | モジュール構成、外部依存の契約、peer対応、期限、ライフサイクル、フラグ |
9
+ | [dataflow.md](dataflow.md) | 往復シーケンス、ロール別状態遷移、part受理の判定フロー |
10
+ | [interfaces.ts](interfaces.ts) | 型契約(搬送形式 `twqr/1`、port、状態、エラーコード、上限定数) |
11
+ | [block-api.md](block-api.md) | 公開ブロック契約(26 opcode)とmanifest反映、形式互換性 |
12
+ | [extraction-plan.md](extraction-plan.md) | `turbowarp-realtime-motion-capture`からの移設選別表とPR分割 |
13
+ | [test-plan.md](test-plan.md) | DoDとテストの対応、重点ケース、実機記録テンプレート |
14
+
15
+ 本文書群は`kairo-design`の構成に準拠しつつ、TurboWarp拡張という対象に合わせて
16
+ `database-schema.sql`を省略し、`api-endpoints.md`を`block-api.md`(ブロック契約)に置き換えている。
@@ -0,0 +1,191 @@
1
+ # QR搬送WebRTCペアリング アーキテクチャ設計
2
+
3
+ 対応要求: [requirements.md](requirements.md) / 元Issue: #1
4
+
5
+ ## 1. システム概要
6
+
7
+ 本拡張は「接続情報を光学的に運ぶ運び屋」に徹する。SDPの中身を解釈せず、WebRTCの接続確立そのものも行わない。
8
+ `turbowarp-webrtc`が生成した不透明な文字列(pairing code)を、QRに載る大きさへ分割し、表示・読取り・再構成・検証して、
9
+ 正しい相手・正しい交換に対して一度だけ`turbowarp-webrtc`へ返す。
10
+
11
+ ```
12
+ [hub端末] [camera端末]
13
+ turbowarp-webrtc ──createOffer──▶ offer code
14
+ │ │
15
+ │ 本拡張: 分割・QR化・表示
16
+ │ │ (プロジェクタ投影)
17
+ │ ▼ ~~~ 光学搬送 ~~~
18
+ │ 本拡張: 読取り・再構成・検証 ◀── jsqr + camera-source
19
+ │ │
20
+ │ turbowarp-webrtc ──acceptOffer──▶ answer code
21
+ │ │
22
+ │ 本拡張: 分割・QR化・表示(画面)
23
+ │ ▼ ~~~ スマートフォン撮影による搬送 ~~~
24
+ 本拡張: 読取り・再構成・検証 ◀── jsqr + camera-source
25
+ │
26
+ turbowarp-webrtc ──acceptAnswer──▶ 接続成立
27
+ ```
28
+
29
+ ## 2. アーキテクチャパターン
30
+
31
+ - パターン: **ヘキサゴナル(ports and adapters)+ロール別ステートマシン**
32
+ - 理由:
33
+ - courier(形式・分割・検証)はDOM・Scratch・WebRTCに依存しない純粋関数群にできる。NFR-4(フラグと独立した単体検証)を満たす。
34
+ - WebRTC/QRデコード/カメラ/レンダラは、いずれも他拡張のruntime capabilityという外部依存。portで抽象化すれば、依存が無い環境でもテストでき、依存不足を明示的なエラーとして扱える。
35
+ - 往復・多重session・取消/再試行・期限は状態遷移として定義しないとFR-4を満たせない。
36
+
37
+ ## 3. レイヤとモジュール
38
+
39
+ ```
40
+ src/
41
+ index.ts 登録エントリ(既存)
42
+ config.ts 拡張ID・名前・アイコン(既存)
43
+ block-definitions.json ブロック定義(置換)
44
+ extension.ts ブロックのファサード。引数のCast・翻訳・委譲のみ
45
+ globals.d.ts Scratch/runtime/renderer型(motion-captureから統合)
46
+ errors.ts PairingErrorCode と QrPairingError(依存なしの葉モジュール)
47
+ clock.ts 単調時計(performance.now系)
48
+
49
+ config/
50
+ feature-flags.ts 起動時固定フラグ qrCodePairing(既定OFF)
51
+ qr-config.ts errorCorrectionLevel など起動時固定設定
52
+
53
+ qr/ ── 純粋層(DOM・Scratch非依存) ──
54
+ limits.ts 上限定数と単位・境界条件
55
+ hash.ts SHA-256 base64url(破損検出用)
56
+ envelope.ts envelope v1 (twqr/1) の型・serialize・parse・検証
57
+ courier.ts 分割(createParts)と再構成(PartAssembler)
58
+ svg.ts QRのSVG生成(表示層が使う純粋関数)
59
+
60
+ pairing/ ── ドメイン層 ──
61
+ limits.ts session上限・期限既定値・tick間隔
62
+ types.ts PairingRole / PairingPhase / PairingProgress / session状態の形
63
+ controller.ts session集合の管理、状態遷移、往復手順の進行、資源解放
64
+
65
+ ports/ ── アダプタ層 ──
66
+ webrtc.ts runtime capability v3 の取得と適合検査
67
+ qr-scan.ts jsqr(scanFrame) + camera-source(lease) の取得と走査ループ
68
+ display.ts TemporarySpriteSkinManager(skin退避・復元)
69
+ ```
70
+
71
+ 依存方向は `extension.ts → pairing/ → qr/` と `pairing/ → ports/`(interfaceのみ)。
72
+ `qr/`は`pairing/`・`ports/`を参照しない。エラー型は両層が使うため、依存を持たない葉モジュール
73
+ `src/errors.ts`に置き、`qr/`と`pairing/`の双方がそこだけを参照する。
74
+
75
+ ## 4. 外部依存と契約
76
+
77
+ | 依存 | 取得方法 | 本拡張が使う操作 | 不在時の扱い |
78
+ |---|---|---|---|
79
+ | `turbowarp-webrtc` | `runtime.kubohiroyaWebRtcCapability`、`requireVersion(3)` | `createOffer` / `getOffer` / `acceptOffer` / `getAnswer` / `acceptAnswer` / `connectionState` / `closePeer` | `webrtc-capability-missing` で失敗。部分機能での縮退はしない |
80
+ | `turbowarp-jsqr` | `runtime.ext_kubohiroyajsqr` | `scanFrame(frameSource)` | カメラ走査ブロックのみ`qr-decoder-missing`。テキスト投入経路は動作する |
81
+ | `turbowarp-camera-source` | `runtime.ext_kubohiroyacamerasource` | `acquireCamera({owner, cameraId})` → `lease.getFrameSource()` / `lease.release()` | 同上(`camera-unavailable`) |
82
+ | TurboWarp renderer | `runtime.renderer` | `createSVGSkin` / `updateDrawableSkinId` / `destroySkin` | スプライト表示ブロックのみ`renderer-unavailable`。SVG取得ブロックは動作する |
83
+
84
+ > **前提作業(ブロッカー)**: `turbowarp-webrtc`のruntime capabilityは現在v2で、`createOffer`/`getOffer`しか公開していない。
85
+ > `acceptOffer`/`getAnswer`/`acceptAnswer`/`connectionState`/`closePeer`は`PeerSessionPort`には存在するがブロック経由でしか呼べない。
86
+ > 本拡張のFR-1(対称な搬送)はこれらを必要とするため、**`turbowarp-webrtc`側にcapability v3を追加するPRが先行して必要**。
87
+ > 詳細は [extraction-plan.md](extraction-plan.md) の「前提となる他リポジトリの変更」。
88
+
89
+ ## 5. peer識別子の対応(FR-1.3)
90
+
91
+ `turbowarp-webrtc`のpeer名は各端末ローカルのキーでしかなく、両端で一致している必要はない。
92
+ 本拡張はこれを利用し、**envelopeに送信側の自称と受信側への呼称の両方を載せる**ことで対応関係を明示する。
93
+
94
+ | 端末 | 自分の呼称 `localPeerId` | 相手の呼称 `remotePeerId` | WebRTCに渡すpeerキー |
95
+ |---|---|---|---|
96
+ | hub | `hub`(アプリが指定) | `camera-1`(アプリが指定) | `camera-1` |
97
+ | camera | `camera-1`(Offerの`targetPeerId`から受領) | `hub`(Offerの`senderPeerId`) | `hub` |
98
+
99
+ - Offer envelope: `senderPeerId = "hub"`, `targetPeerId = "camera-1"`
100
+ - Answer envelope: `senderPeerId = "camera-1"`, `targetPeerId = "hub"`, `replyTo = <Offerのmessage ID>`
101
+ - camera側は`startAnswerPairing`で期待する自称を任意指定できる。空なら`targetPeerId`をそのまま採用し、
102
+ 非空で不一致なら`peer-mismatch`で拒否する(誤った投影を読んだ場合の検出)。
103
+ - hub側はAnswerの`senderPeerId`/`targetPeerId`が自分のsessionの`remotePeerId`/`localPeerId`と一致することを検証する。
104
+
105
+ これにより「両端で同じ識別子が設定されていること」を要求せず、かつ2台以上のcameraを順に接続しても
106
+ Answerが別peerへ適用されない(DoD 6)。
107
+
108
+ ## 6. 交換の同一性と鮮度(FR-1.4 / FR-4.4 / FR-4.5)
109
+
110
+ 3層で守る。
111
+
112
+ 1. **exchangeId(=`sessionId`)**: hubがOffer生成時にUUIDで採番。Answerは同じ`sessionId`を反映する。
113
+ 再試行では必ず新しい`sessionId`になるので、古いQRは`unknown-session`/`stale-exchange`で弾かれる。
114
+ 2. **`replyTo`**: Answerが応答するOfferの`messageId`。hubのsessionが保持する`messageId`と一致しなければ拒否。
115
+ 3. **epoch(世代番号)**: sessionごとの単調増加カウンタ。`await`から復帰した非同期処理は、
116
+ 再開時に`session.epoch`が自分の取得時と同じかを確認し、異なれば結果を捨てる(motion-capture `this.operation`の一般化)。
117
+ 取消・再試行・期限切れ・破棄はいずれもepochを進める。
118
+
119
+ `messageId`は既存実装と同じく`<sessionId>.<messageHash先頭12文字>`で、内容が変われば必ず変わる。
120
+
121
+ ## 7. 期限(FR-4.6)
122
+
123
+ - 期限は**単調時計**(`performance.now()`、無ければ`Date.now()`のフォールバックを明示)で、
124
+ session開始時刻からの経過で判定する。envelopeの`createdAt`は診断・表示用であり期限判定には使わない。
125
+ これにより未同期端末間の壁時計差に依存しない。
126
+ - 既定値: 600秒(人手搬送を想定)。`setPairingTimeout`で1〜3600秒に設定可能。
127
+ - 期限到来時は`expired`へ遷移し、表示・lease・タイマを解放する。**成立済み接続は切断しない**(NFR-6と同じ方針)。
128
+ 未成立のRTCPeerConnectionのみ`closePeer`する。
129
+
130
+ ## 8. 状態と資源のライフサイクル(FR-4.8)
131
+
132
+ | イベント | 動作 |
133
+ |---|---|
134
+ | `cancelPairing` | epoch++、表示復元、lease解放、タイマ停止、受信バッファ破棄、未成立ならclosePeer、phase=`cancelled` |
135
+ | `retryPairing` | cancel相当の後、新`sessionId`で同じロールの交換を開始 |
136
+ | `PROJECT_RUN_STOP` | 表示復元とlease解放(進行中sessionは`cancelled`)。成立済み接続は維持 |
137
+ | `PROJECT_STOP_ALL` | 同上。停止ボタンでは成立済み接続を切断しない。FR-4.8の「切断は所有関係と明示操作に従う」に従い、sessionも保持する |
138
+ | `PROJECT_LOADED` | 全sessionを破棄し、未成立・成立済みを問わずpeerを閉じる。旧プロジェクトのsessionは新しいプロジェクトから到達できないため。**runtimeリスナは維持する**(拡張インスタンスは読込みをまたいで生き残り、以降の停止やスプライト削除に反応し続ける必要がある) |
139
+ | `targetWasRemoved` | そのスプライトへの表示のみ終了 |
140
+ | `RUNTIME_DISPOSED` | 全解放+runtimeリスナ解除 |
141
+
142
+ 共有カメラのleaseは本拡張が取得したものだけを解放する(camera-sourceは参照カウント方式なので、
143
+ アプリ側が別途leaseを持っていればカメラは止まらない、FR-3.4)。
144
+
145
+ ## 9. 表示(FR-3.5)
146
+
147
+ - `TemporarySpriteSkinManager`(motion-captureから移設)でスプライトの元skin IDを退避し、
148
+ 表示終了・取消・停止・スプライト削除時に復元、一時skinは破棄する。
149
+ - 併せて`pairingQrPartSvg`/`pairingQrPartDataUri`リポーターを提供し、
150
+ アプリが独自にHTML/コスチューム等で表示する経路を塞がない。
151
+ - 静止画撮影での確実な取得のため、自動アニメーションは行わず**手動切替**を基本とする(FR-3.2)。
152
+ 現在part番号・総part数をリポーターで提供し、アプリが「1/3」などの案内を出せるようにする。
153
+
154
+ ## 10. 読取り(FR-3.3)
155
+
156
+ - 正規の入口は`ingestPairingQrText(TEXT, SESSION)`。デコード済み文字列を受け取るだけで、画像処理はしない。
157
+ - 利便のため`scanPairingQrFromCamera(SESSION, CAMERA_ID)`を提供するが、実装は
158
+ `camera-source`から**session中1つだけ**leaseを取得し、`jsqr.scanFrame(frameSource)`を間隔ポーリングで呼ぶ。
159
+ jsqrの`waitForQrText`はpartごとにlease取得・解放を繰り返すため使わない([requirements.md](requirements.md) C-4)。
160
+ - 走査は`AbortController`で中断可能。取消・期限・全part受信で停止する。
161
+ - 同一partの重複読取りは正常系として黙って無視する(カメラは同じQRを何度も読む)。
162
+
163
+ ## 11. フィーチャーフラグとロールバック(NFR-3/5/6)
164
+
165
+ ```ts
166
+ // src/config/feature-flags.ts
167
+ (globalThis as FeatureFlagGlobal).__TWQP_FEATURE_FLAGS__?.qrCodePairing === true
168
+ ```
169
+
170
+ - 起動時に1回だけ読み、以後変化しない(`Object.freeze`)。既定OFF。
171
+ - OFF時: `getInfo()`はペアリングブロックを返さない。直接呼ばれた場合は`feature-disabled`で失敗し、
172
+ `pairingPhase`は`disabled`を返す。
173
+ - OFF化は経路選択の切戻しであり、成立済み接続を切断しない。
174
+ - 切戻し手順: フラグをOFF(または設定しない)にし、アプリは`turbowarp-webrtc`の
175
+ `create offer code` / `accept offer code` / `answer code` / `accept answer code` ブロックによる
176
+ 従来のmanual交換に戻す。motion-capture側の既存QR経路は本Issueでは削除しない。
177
+ - `qr/`配下の純粋処理はフラグを参照しないので、単体テストはフラグと独立に動く。
178
+
179
+ ## 12. 診断とプライバシー(FR-4.9)
180
+
181
+ - 既定でSDP・ICE・pairing code・QR payloadを一切ログ出力しない。例外メッセージにも含めない
182
+ (courierのエラーは「長さ」「index」「code」のみを述べる)。
183
+ - 診断用に公開するのは、phase、part進捗(received/required/missing)、エラーコード、
184
+ そしてサニタイズ済みエラーメッセージのみ。
185
+ - 長さ・hash・peer IDは診断に出してよい情報として扱う(hashは破損検出用であり、本人性の保証ではない、FR-2.6)。
186
+
187
+ ## 13. 想定外として明示すること
188
+
189
+ - QRを見た第三者は接続情報を取得できる。QR搬送は認証ではない。投影の可視範囲は運用で管理する(FR-2.6の帰結)。
190
+ - `twqr/1`と`twmp-qr/1`は非互換。混在は`unsupported-protocol`で検出される([block-api.md](block-api.md) 参照)。
191
+ - 接続の成立可否はネットワーク条件次第であり、QR搬送の成功は接続成功を意味しない(NFR-2)。
@@ -0,0 +1,123 @@
1
+ # 公開契約(ブロックAPI)と形式互換性
2
+
3
+ 対応要求: FR-4.3(opcodeと引数を確定しmanifestへ反映)/ FR-2.8(形式互換性)
4
+
5
+ `kairo-design`標準の`api-endpoints.md`に相当する。本拡張の公開面はRESTではなくTurboWarpブロックなので、
6
+ opcodeとその引数をAPI契約として定義する。
7
+
8
+ ## 0. 契約の生成経路
9
+
10
+ `src/block-definitions.json` と `src/config.ts` から `dist/extension-manifest.json`(`formatVersion: 1`)が生成される。
11
+ manifestはopcode・blockType・引数ID・引数型・menu参照のみを含み、テキストや説明は含まない
12
+ ([docs/architecture.md](../../architecture.md))。本設計の変更はmanifestの中身を全面的に差し替えるが、
13
+ `formatVersion`は1のまま(manifest形式自体は変えない)。
14
+
15
+ `hello`ブロックは削除する。初期scaffold専用の動作確認ブロックであり、保存済みプロジェクトからの参照はない。
16
+
17
+ ## 1. 共通引数
18
+
19
+ | 引数ID | 型 | 既定値 | 意味 |
20
+ |---|---|---|---|
21
+ | `SESSION` | STRING | `pairing-1` | アプリが決めるsessionキー。同時に`MAX_ACTIVE_SESSIONS`(8)まで |
22
+ | `LOCAL_PEER` | STRING | `hub` / 空 | 自分の呼称 |
23
+ | `REMOTE_PEER` | STRING | `camera-1` | 相手の呼称 |
24
+ | `INDEX` | NUMBER | `1` | 1始まりのpart番号 |
25
+ | `TEXT` | STRING | 空 | デコード済みQR文字列 |
26
+ | `CAMERA_ID` | STRING | `default` | camera-sourceのカメラID |
27
+ | `SECONDS` | NUMBER | `600` | 期限(1〜3600) |
28
+
29
+ ## 2. hub(統合側)ブロック
30
+
31
+ | opcode | 種類 | テキスト | 効果・戻り値 |
32
+ |---|---|---|---|
33
+ | `startOfferPairing` | COMMAND | `start offer pairing [SESSION] as [LOCAL_PEER] to [REMOTE_PEER]` | `createOffer`→分割→`offer-ready`。既存の同名sessionがあれば`session-exists` |
34
+ | `acceptAnswerFromQr` | — | (明示ブロックなし) | 全part検証完了時に`ingestPairingQrText`/`scanPairingQrFromCamera`の内部で自動的に1回だけ`acceptAnswer`を呼ぶ(FR-1.2) |
35
+
36
+ ## 3. camera(カメラ側)ブロック
37
+
38
+ | opcode | 種類 | テキスト | 効果・戻り値 |
39
+ |---|---|---|---|
40
+ | `startAnswerPairing` | COMMAND | `start answer pairing [SESSION] as [LOCAL_PEER]` | `awaiting-offer`へ。`LOCAL_PEER`が空ならOfferの`targetPeerId`を採用、非空なら照合し不一致で`peer-mismatch` |
41
+
42
+ Offer全part受信後の`acceptOffer`とAnswer分割は内部で自動実行し、`answer-ready`へ遷移する。
43
+
44
+ ## 4. 受信(両ロール共通)
45
+
46
+ | opcode | 種類 | テキスト | 備考 |
47
+ |---|---|---|---|
48
+ | `ingestPairingQrText` | COMMAND | `receive pairing QR text [TEXT] for [SESSION]` | 正規入口。重複partは成功扱い。不正partは対応するエラーコードで失敗 |
49
+ | `scanPairingQrFromCamera` | COMMAND | `scan pairing QR for [SESSION] from camera [CAMERA_ID]` | 全part受信・取消・期限までブロックする。session中1つのleaseを保持 |
50
+ | `pairingReceivedParts` | REPORTER | `received parts of [SESSION]` | 取得済み数 |
51
+ | `pairingRequiredParts` | REPORTER | `required parts of [SESSION]` | 必要数。最初のpart受信前は0 |
52
+ | `pairingMissingParts` | REPORTER | `missing parts of [SESSION]` | 未取得の1始まり番号をカンマ区切りで返す |
53
+
54
+ ## 5. 表示(両ロール共通)
55
+
56
+ | opcode | 種類 | テキスト | 備考 |
57
+ |---|---|---|---|
58
+ | `showPairingQrPart` | COMMAND | `show pairing QR part [INDEX] of [SESSION] on this sprite` | 一時skinへ差し替え。範囲外は`invalid-argument` |
59
+ | `showNextPairingQrPart` | COMMAND | `show next pairing QR part of [SESSION] on this sprite` | 最終partから先頭へ巡回 |
60
+ | `pairingQrPartCount` | REPORTER | `pairing QR part count of [SESSION]` | 未準備は0 |
61
+ | `pairingQrCurrentPart` | REPORTER | `current pairing QR part of [SESSION]` | 1始まり。未表示は0 |
62
+ | `pairingQrPartSvg` | REPORTER | `pairing QR part [INDEX] of [SESSION] as SVG` | アプリ独自表示用(FR-3.5) |
63
+ | `pairingQrPartDataUri` | REPORTER | `pairing QR part [INDEX] of [SESSION] as data URI` | コスチューム差替え等に使える形 |
64
+ | `endPairingQrDisplay` | COMMAND | `end pairing QR display of [SESSION]` | 元skinへ復元。sessionは維持する |
65
+
66
+ ## 6. 進行・状態・診断
67
+
68
+ | opcode | 種類 | テキスト | 戻り値 |
69
+ |---|---|---|---|
70
+ | `pairingPhase` | REPORTER | `pairing phase of [SESSION]` | `PairingPhase`の文字列。未知のsessionは`idle`、フラグOFFは`disabled` |
71
+ | `pairingConnectionState` | REPORTER | `pairing connection state of [SESSION]` | `turbowarp-webrtc`の接続状態そのまま |
72
+ | `isPairingConnected` | BOOLEAN | `pairing [SESSION] connected?` | `phase === 'connected'` |
73
+ | `waitUntilPairingConnected` | COMMAND | `wait until pairing [SESSION] is connected` | 接続成立・失敗・取消・期限まで待つ |
74
+ | `pairingError` | REPORTER | `pairing error code of [SESSION]` | `PairingErrorCode`または空 |
75
+ | `pairingErrorMessage` | REPORTER | `pairing error message of [SESSION]` | サニタイズ済み文言(SDP・payloadを含まない) |
76
+ | `pairingLocalPeer` | REPORTER | `local peer of [SESSION]` | 自称 |
77
+ | `pairingRemotePeer` | REPORTER | `remote peer of [SESSION]` | 相手の呼称 |
78
+ | `pairingExchangeId` | REPORTER | `exchange id of [SESSION]` | 現在のexchangeId(診断・実機記録用) |
79
+ | `pairingRemainingSeconds` | REPORTER | `remaining seconds of [SESSION]` | 期限までの残り |
80
+ | `pairingSessions` | REPORTER | `pairing sessions` | 有効なsessionキーをカンマ区切り |
81
+
82
+ ## 7. 制御
83
+
84
+ | opcode | 種類 | テキスト | 備考 |
85
+ |---|---|---|---|
86
+ | `cancelPairing` | COMMAND | `cancel pairing [SESSION]` | epoch++、資源解放、`cancelled`へ。成立済み接続は切らない |
87
+ | `retryPairing` | COMMAND | `retry pairing [SESSION]` | 取消後に新しい`exchangeId`で同ロールの交換を再開 |
88
+ | `setPairingTimeout` | COMMAND | `set pairing timeout of [SESSION] to [SECONDS] seconds` | 1〜3600。範囲外は`invalid-argument` |
89
+
90
+ 合計 28 opcode(COMMAND 11 / REPORTER 16 / BOOLEAN 1)。実装時に `pairingQrPartDataUri` と `pairingRemainingSeconds` を加え、表示選択と表示適用を分けた結果。
91
+
92
+ ## 8. エラーの返し方
93
+
94
+ - COMMANDは失敗時に例外を投げる(TurboWarpはスクリプトを停止する)。同時に`pairingError`/`pairingErrorMessage`へ記録する。
95
+ - REPORTERは例外を投げず、既定値(空文字・0)を返す。状態はphaseとエラーコードで観測する。
96
+ - 重複partの受理、未接続時の`connectionState`照会などの正常系はエラーにしない。
97
+
98
+ ## 9. 形式互換性(FR-2.8)
99
+
100
+ | 形式 | 発行元 | 本拡張の扱い |
101
+ |---|---|---|
102
+ | `twqr/1` | 本拡張 | 受理する |
103
+ | `twmp-qr/1` | `turbowarp-realtime-motion-capture`(Offerのみ) | 受理しない。`unsupported-protocol`で明示的に失敗する |
104
+
105
+ **非互換にした理由**: `twmp-qr/1`は`peerId`が1つだけで「送信側の自称」と「受信側への呼称」を区別できず、
106
+ `replyTo`も持たないため、FR-1.3(両端で同じ識別子を要求しない)とFR-1.4(Answerの応答先追跡)を満たせない。
107
+ これらは必須フィールドの追加にあたるので、後方互換の拡張ではなくversion変更とした。
108
+
109
+ **移行方法**:
110
+
111
+ 1. 本拡張は`twqr/1`のみを発行・受理する。旧形式の読取り互換モードは設けない
112
+ (ambiguousな受理より、明示的な`unsupported-protocol`のほうが誤接続を防ぐ)。
113
+ 2. `turbowarp-realtime-motion-capture`は当面`twmp-qr/1`のまま維持する(NFR-5)。
114
+ 両者を同一会場で使うと相互に読めないが、`unsupported-protocol`として検出されるので誤接続にはならない。
115
+ 3. motion-capture側を本拡張へ委譲する変更は、当該リポジトリの別Issue/PRとして記録する(Issue #1の方針どおり)。
116
+ 4. 将来`twqr/2`が必要になった場合も、受理側は`protocol`完全一致で判定し、未知versionは常に拒否する。
117
+
118
+ ## 10. manifest反映時の注意
119
+
120
+ - ブロック・引数・menuは識別子でソートされて出力されるので、`block-definitions.json`の記述順は契約に影響しない。
121
+ - menuは使わない(`SESSION`等はすべて自由入力のSTRING)。動的menuを将来入れる場合はmanifestに`menu`参照が現れるため、契約変更として扱う。
122
+ - フィーチャーフラグOFF時は`getInfo()`がペアリングブロックを返さないが、**manifestは常に全ブロックを出力する**
123
+ (manifestはビルド時の静的契約であり、実行時のフラグ状態ではない)。この点はREADMEに明記する。
@@ -0,0 +1,202 @@
1
+ # データフローと状態遷移
2
+
3
+ 対応要求: FR-1 / FR-3 / FR-4 — [requirements.md](requirements.md)
4
+
5
+ ## 1. 全体シーケンス(正常系・DoD 1)
6
+
7
+ ```mermaid
8
+ sequenceDiagram
9
+ autonumber
10
+ participant App1 as 利用アプリ(hub)
11
+ participant Ext1 as 本拡張(hub)
12
+ participant W1 as turbowarp-webrtc(hub)
13
+ participant Human as 担当者/光学搬送
14
+ participant Ext2 as 本拡張(camera)
15
+ participant W2 as turbowarp-webrtc(camera)
16
+ participant App2 as 利用アプリ(camera)
17
+
18
+ App2->>Ext2: startAnswerPairing(s, localPeer="")
19
+ Note over Ext2: phase=awaiting-offer
20
+
21
+ App1->>Ext1: startOfferPairing(s, "hub", "camera-1")
22
+ Ext1->>W1: createOffer("camera-1")
23
+ W1-->>Ext1: offer code (ICE収集完了済み)
24
+ Ext1->>Ext1: createParts() 分割・hash・QR化
25
+ Note over Ext1: phase=offer-ready
26
+ App1->>Ext1: showPairingQrPart(1..n) 投影
27
+
28
+ loop 各part
29
+ Human-->>Ext2: カメラでQR読取り → scanFrame
30
+ Ext2->>Ext2: parse → assembler.add()
31
+ Note over Ext2: phase=receiving (received/required)
32
+ end
33
+
34
+ Ext2->>Ext2: assemble() 長さ・hash検証
35
+ Note over Ext2: phase=offer-received
36
+ Ext2->>W2: acceptOffer("hub", offerCode)
37
+ W2-->>Ext2: answer code (ICE収集完了済み)
38
+ Ext2->>Ext2: createParts(kind=answer, replyTo=offerMessageId)
39
+ Note over Ext2: phase=answer-ready
40
+ App2->>Ext2: showPairingQrPart(1..m) 画面表示
41
+
42
+ Human->>Human: スマートフォンで全partを撮影し hubへ運ぶ
43
+ loop 各part
44
+ Human-->>Ext1: スマホ画面をhubのカメラへ → scanFrame
45
+ Ext1->>Ext1: parse → sessionId/replyTo/peer検証 → assembler.add()
46
+ end
47
+
48
+ Ext1->>Ext1: assemble() 長さ・hash検証
49
+ Note over Ext1: phase=answer-received
50
+ Ext1->>W1: acceptAnswer("camera-1", answerCode)
51
+ Note over Ext1: phase=connecting
52
+ loop connectionStateをポーリング
53
+ Ext1->>W1: connectionState("camera-1")
54
+ end
55
+ Note over Ext1: phase=connected
56
+ Ext1-->>App1: isPairingConnected = true
57
+ Note over Ext2: phase=connected(W2のconnectionStateで確認)
58
+ App1->>W1: sendEvent/sendLatestData で疎通確認 (DoD 2)
59
+ ```
60
+
61
+ ## 2. 状態遷移(hub)
62
+
63
+ ```mermaid
64
+ stateDiagram-v2
65
+ [*] --> idle
66
+ idle --> creating_offer: startOfferPairing
67
+ creating_offer --> offer_ready: offer code 取得・分割成功
68
+ creating_offer --> failed: webrtc失敗 / 分割失敗
69
+ offer_ready --> awaiting_answer: 最初のAnswer partを受理
70
+ offer_ready --> answer_received: 単一partで完結
71
+ awaiting_answer --> answer_received: 全part検証成功
72
+ answer_received --> connecting: acceptAnswer 呼出し
73
+ connecting --> connected: connectionState=connected
74
+ connecting --> failed: connectionState=failed / closed
75
+ offer_ready --> expired: 期限超過
76
+ awaiting_answer --> expired: 期限超過
77
+ connecting --> expired: 期限超過
78
+ creating_offer --> cancelled: cancelPairing
79
+ offer_ready --> cancelled: cancelPairing
80
+ awaiting_answer --> cancelled: cancelPairing
81
+ connecting --> cancelled: cancelPairing
82
+ cancelled --> creating_offer: retryPairing(新sessionId)
83
+ expired --> creating_offer: retryPairing(新sessionId)
84
+ failed --> creating_offer: retryPairing(新sessionId)
85
+ connected --> [*]
86
+ ```
87
+
88
+ `offer_ready`/`awaiting_answer`は「Offerを表示しながらAnswerを待つ」段階で、
89
+ 表示の有無は`pairingQrPartCount`/`pairingQrCurrentPart`、受信進捗は
90
+ `pairingReceivedParts`/`pairingRequiredParts`/`pairingMissingParts`で別々に観測する。
91
+
92
+ ## 3. 状態遷移(camera)
93
+
94
+ ```mermaid
95
+ stateDiagram-v2
96
+ [*] --> idle
97
+ idle --> awaiting_offer: startAnswerPairing
98
+ awaiting_offer --> receiving: 最初のOffer partを受理
99
+ receiving --> receiving: 追加part / 重複part
100
+ awaiting_offer --> offer_received: 単一partで完結
101
+ receiving --> offer_received: 全part検証成功
102
+ offer_received --> creating_answer: acceptOffer 呼出し
103
+ creating_answer --> answer_ready: answer code 取得・分割成功
104
+ creating_answer --> failed: webrtc失敗 / 分割失敗
105
+ answer_ready --> connecting: 表示完了後もconnectionStateを監視
106
+ connecting --> connected: connectionState=connected
107
+ connecting --> failed: connectionState=failed / closed
108
+ awaiting_offer --> expired: 期限超過
109
+ receiving --> expired: 期限超過
110
+ answer_ready --> expired: 期限超過
111
+ awaiting_offer --> cancelled: cancelPairing
112
+ receiving --> cancelled: cancelPairing
113
+ answer_ready --> cancelled: cancelPairing
114
+ cancelled --> awaiting_offer: retryPairing
115
+ expired --> awaiting_offer: retryPairing
116
+ failed --> awaiting_offer: retryPairing
117
+ connected --> [*]
118
+ ```
119
+
120
+ **FR-4.2の分離**: QR読取り完了は`offer_received`/`answer_received`、
121
+ WebRTC接続成立は`connected`。両者は必ず別の状態として観測できる。
122
+
123
+ ## 4. part受理の判定フロー(FR-2.3 / FR-2.5)
124
+
125
+ ```mermaid
126
+ flowchart TD
127
+ A[QRテキスト到着] --> B{長さ <= MAX_PART_TEXT}
128
+ B -- No --> E1[part-too-large]
129
+ B -- Yes --> C{JSON parse 可}
130
+ C -- No --> E2[invalid-json]
131
+ C -- Yes --> D{protocol == twqr/1}
132
+ D -- No --> E3[unsupported-protocol]
133
+ D -- Yes --> F{envelope各フィールドが範囲内}
134
+ F -- No --> E4[invalid-envelope / index-out-of-range / message-too-large]
135
+ F -- Yes --> G{sessionId が対象sessionのもの}
136
+ G -- No --> E5[unknown-session / stale-exchange]
137
+ G -- Yes --> H{kind と role の組合せが正しい}
138
+ H -- No --> E6[unexpected-kind]
139
+ H -- Yes --> I{peer対応が一致 / Answerならreply-to一致}
140
+ I -- No --> E7[peer-mismatch / reply-mismatch]
141
+ I -- Yes --> J{既存partと同一identity}
142
+ J -- No --> E8[message-mismatch]
143
+ J -- Yes --> K{同indexの既存partと payload一致}
144
+ K -- No --> E9[conflicting-part]
145
+ K -- Yes --> L[assembler へ格納]
146
+ L --> M{received == partCount}
147
+ M -- No --> N[phase=receiving を維持]
148
+ M -- Yes --> O{結合長 == messageLength}
149
+ O -- No --> E10[length-mismatch]
150
+ O -- Yes --> P{SHA-256 == messageHash}
151
+ P -- No --> E11[hash-mismatch]
152
+ P -- Yes --> Q[検証済みメッセージ確定 → WebRTCへ1回だけ引渡し]
153
+ ```
154
+
155
+ 重複partで`payload`が一致する場合は`duplicate=true`を返し、エラーにはしない(正常系)。
156
+
157
+ ## 5. 古い非同期結果の破棄(FR-4.4)
158
+
159
+ ```mermaid
160
+ sequenceDiagram
161
+ participant App
162
+ participant Ctl as PairingController
163
+ participant W as turbowarp-webrtc
164
+
165
+ App->>Ctl: startOfferPairing(s)
166
+ Note over Ctl: epoch=1 を捕捉
167
+ Ctl->>W: createOffer("camera-1") (時間がかかる)
168
+ App->>Ctl: cancelPairing(s)
169
+ Note over Ctl: epoch=2、表示・lease解放、phase=cancelled
170
+ App->>Ctl: retryPairing(s)
171
+ Note over Ctl: epoch=3、新 sessionId
172
+ W-->>Ctl: 旧createOfferの解決 (epoch=1で開始)
173
+ Note over Ctl: epoch不一致 → 結果を破棄。状態は変更しない
174
+ ```
175
+
176
+ 同じ仕組みを`acceptOffer`/`acceptAnswer`/分割処理/connectionStateポーリングにも適用する。
177
+
178
+ ## 6. 資源の取得と解放
179
+
180
+ ```mermaid
181
+ flowchart LR
182
+ S[session開始] --> T[タイマ登録]
183
+ S --> L[camera lease 取得<br/>scanブロック使用時のみ]
184
+ S --> K[一時skin 生成<br/>表示ブロック使用時のみ]
185
+ S --> P[connectionState ポーリング開始<br/>connecting以降]
186
+ subgraph 終了契機
187
+ C1[cancel]
188
+ C2[expire]
189
+ C3[connected]
190
+ C4[PROJECT_RUN_STOP]
191
+ C5[RUNTIME_DISPOSED]
192
+ end
193
+ C1 --> R
194
+ C2 --> R
195
+ C3 --> R
196
+ C4 --> R
197
+ C5 --> R
198
+ R[解放: タイマ停止 / lease.release /<br/>元skin復元・一時skin破棄 /<br/>ポーリング停止 / assembler破棄]
199
+ ```
200
+
201
+ `connected`到達時も走査・表示・タイマは解放するが、RTCPeerConnectionは維持する。
202
+ `RUNTIME_DISPOSED`ではruntimeイベントリスナも解除する。