# データセットと受け渡し (Datasets & Delivery)

データセットの定義・版の作成・バンドル（manifest ＋ファイル）の書き出し・計算サーバへの受け渡し・CLI・出力の規則を定める．表の定義は [SPEC_02](SPEC_02_データモデル.md)．

## データセットの定義 (Dataset Definition)

`datasets/<名前>.toml` に置く（リポジトリで管理し，PR で変更する）．

```toml
name = "tongue_color"
description = "舌色 5 分類＋程度（TIASJudgeModel の学習データ）"

[select]
asset_kind = "tongue_crop_masked"      # 集めるファイルの種類
require = ["tongue_color"]             # この所見を持つものだけ（撮影に付いた所見を含む）

[columns]                              # manifest に出す所見の列
tongue_color = ["label", "severity"]
```

- 条件は「ファイルの種類」と「必須の所見」から始め，必要になったら所見の値による絞り込み等を足す
- 1 つのファイル・撮影に同じ種類の有効な所見（訂正されていないもの）が 2 つ以上ある場合は，版の作成を止める（どちらを使うかを黙って決めない）

## 版の作成 (Creating a Version)

`tiasdata dataset freeze <名前>` で，その時点の目録に定義を当てはめて新しい版を作る．

1. 定義を検証し，条件に合うファイルと採用する所見を集める
2. 直前の版と構成（ファイルと所見）が同じなら，版を作らずにその旨を表示する
3. 構成・定義の内容・本リポジトリのコミット・`manifest.csv` の SHA-256 を記録する（以後変更しない）

- 本リポジトリに未コミットの変更がある状態では版を作らない（定義とコードの版を記録できなくなるため）
- 行の順序は `subject_id`・`capture_id`・`sha256` の順に並べる（同じ構成なら同じ `manifest.csv` になるようにする）
- 未コミットの変更の確認は，目録を見る前に行う（`git status --porcelain --untracked-files=all` が空でなければ停止）．`hub_commit` は `git rev-parse HEAD`
- **構成の比較**: 定義の全文が同じで，かつ採用したファイルと所見（所見の行の ID）の組が同じなら版を作らない．そのため，同じ値への訂正でも所見の行が変わるので新しい版になる（manifest の SHA-256 は直前の版と同じになる）．定義は説明だけの変更でも新しい版になる
- 条件に合うファイルが 0 件なら停止する．撮影にひも付かないファイルが入る場合も停止する（`subject_id`・`capture_id` を決められないため）
- 所見の列は定義の `[columns]` に書いた順に並べる．値は文字列ならそのまま，無ければ空，それ以外は JSON の表記
- 定義の `description` は必須．目録の `datasets.description` は最初の版のときの値のままにする（版ごとの定義の全文は `dataset_versions.definition` に残る）
- 種類との照合（定義の種類・項目が kinds と値の型にあるか）は版を作るときだけ行う．過去の版の manifest の作り直しには使わない（種類の定義が後で変わっても作り直せるようにするため）
- 版の備考（`--note`）は `dataset show` で表示する．**備考に患者 ID 等の個人情報を書かない**

## バンドル (Bundle)

データセットの 1 つの版を，計算サーバ・Python 以外のツールに渡す形にまとめたもの．

```text
<名前>/v<版>/
├── dataset.json          # 名前・版・作成日時・hub_commit・件数・manifest.csv の SHA-256
├── manifest.csv          # 1 行 1 ファイル
├── SHA256SUMS            # files/ の照合用（sha256sum -c で検証できる形式）
└── files/<sha256>.<ext>  # ストアからのコピー
```

- `dataset.json` の項目: `format`（形式の版．今は 1．読み込み側は知らない形式を拒否し，未知のキーは無視する）・`name`・`version`・`created_at`（版を作った日時）・`hub_commit`・`files`（件数）・`manifest_sha256`．**書き出した日時は入れない**（同じ版から常に同じバイト列にし，既存のバンドルをバイト単位で照合できるようにするため）
- `SHA256SUMS` の 1 行は `<sha256>  files/<sha256>.<ext>`（空白 2 つ・LF・パスの順）
- バンドルのファイルは読み取り専用にしない（作り直せて，受け渡した後に消す前提のため．2026-10-08 のユーザー判断）．改変は `verify()` と書き出し直しの照合で見つける

### 書き出し (Export)

`tiasdata export <名前> --version <版>`（`src/tiasdata/export.py`）．

1. 版の記録から manifest を作り直し，記録した SHA-256 と一致しなければ停止する（目録が書き換えられた版を書き出さない）
2. `bundles/<名前>/.tmp-v<版>-…/` に `files/`（ストアからコピーし，読み直して照合）・`manifest.csv`・`SHA256SUMS`・`dataset.json` を書き，開き直して形を確かめてから `v<版>` に名前を変える（書きかけを正式な名前で残さない．失敗したら一時フォルダを消す）
3. 既に `v<版>` があれば，`dataset.json`・`manifest.csv`・`SHA256SUMS` を版の記録から作ったものとバイト比較し，`verify()` を通して使う．食い違えば上書きせずに停止する（消して書き出し直すのは人間の作業）

### manifest.csv の列

| 列 | 内容 |
| --- | --- |
| `path` | バンドルのルートからの相対パス（`files/<sha256>.<ext>`．区切りは `/`） |
| `sha256` | ファイルの SHA-256 |
| `kind` | ファイルの種類 |
| `subject_id` | 研究用の症例 ID（分割の単位にする） |
| `capture_id` | 撮影 ID |
| `<所見>.<項目>` | 定義の `[columns]` で指定した所見の値（例: `tongue_color.label`・`tongue_color.severity`） |

- 文字コードは UTF-8（BOM なし），改行は LF
- 実際の患者 ID・元のファイル名は出さない

### TIASJudgeModel との互換 (Compatibility)

TIASJudgeModel の manifest（`image_path`・`patient_id`・`label`・`severity`・`note`）は，次の対応で同じ情報を持つ．

| TIASJudgeModel | 本基盤 |
| --- | --- |
| `image_path` | `path` |
| `patient_id` | `subject_id` |
| `label` | `tongue_color.label` |
| `severity` | `tongue_color.severity` |

- TIASJudgeModel 側は `tiasdata.load()` でバンドルを読み，自分の manifest の形に変換して使う（変換は TIASJudgeModel 側の実装）．基盤側は使う側ごとの形式を持たない
- `subject_id` は患者単位である．TIASJudgeModel の現行の `patient_id` は画像 ID（「1 患者 1 枚」という誤った前提）のため，**切り替えると分割が患者単位に変わり，交差検証の成績が変わりうる**（同じ患者の画像が学習と評価に分かれなくなる）

## 計算サーバへの受け渡し (Delivery)

`tiasdata deliver <名前> --version <版> --to <サーバ名>` を研究室 PC で実行する（手元 PC からは `ssh labpc` 経由．データは手元 PC を通らない）．

1. バンドルを書き出す（`bundles/<名前>/v<版>/`．既にあれば中身を照合して使う）
2. tar にまとめ，設定ファイルの鍵で `scp` する
3. サーバで `<root>/<名前>/v<版>/` に展開し，`chmod -R u+rwX,go-rwx` で自分だけに絞る（Windows で作った tar は `tar --delay-directory-restore -xf` で展開する．TIASJudgeModel の ENV_03 の実測）
4. サーバで `sha256sum -c SHA256SUMS` を実行して照合する（サーバに `tiasdata` が無くても検証できる）
5. tar ファイルを両側で削除し，`deliveries` に記録する

- 受け渡し先に同じ版が既にあれば，照合だけ行って記録する（上書きしない）
- 版は凍結されているため，一度渡した版の中身が変わることはない．新しいデータは新しい版として渡す

## ライブラリの API (Library API)

```python
import tiasdata

bundle = tiasdata.load("tongue_color", version=1)   # TIASDATA_BUNDLES/<名前>/v<版>/ を開く
bundle.verify()                                     # ハッシュを照合する（任意．時間がかかる）
for row in bundle.rows:                             # manifest の行（pydantic の型）
    row.path, row.subject_id, row.annotations["tongue_color"]["label"]
bundle.fingerprint                                  # {"name", "version", "manifest_sha256"}．実験記録に残す
```

- `load(name, *, version)`: 版の指定は**キーワードで必須**（`load("tongue_color", 1)` は誤り）．置き場所は環境変数 `TIASDATA_BUNDLES`，無ければ `~/data/tiasdata`．`dataset.json` の名前・版が指定と違えば停止する
- `bundle.rows` は `BundleRow`（`path`・`sha256`・`kind`・`subject_id`・`capture_id`・`annotations`）の並び．`annotations` は manifest の列 `<所見>.<項目>` を所見ごとにまとめたもので，値は文字列か None（空欄）．文字列以外の型には戻さない
- `bundle.file_path(row)` が画像等の絶対パスを返す．`bundle.info`（`BundleInfo`）・`bundle.root`・`bundle.name`・`bundle.version`・`bundle.manifest_sha256` も使える
- `bundle.verify()` は問題が無ければ None を返し，欠け・改変・`files/` の余分・`SHA256SUMS` と manifest の食い違い・開いた後の `manifest.csv` の書き換えを ValueError で知らせる
- 読み込み側（`tiasdata.bundle`）は標準ライブラリ・pydantic・`tiasdata.manifest` だけを使い，目録（catalog・store・config）を読み込まない．`import tiasdata` で読み込まれるのは `tiasdata`・`tiasdata.bundle`・`tiasdata.manifest` だけ
- エラーメッセージにバンドルの場所（パス）を出さない（研究室 PC・計算サーバの出力も Claude が読むため．名前・版・`<名前>/v<版>/` の形・環境変数名で示す）
- `tiasdata.open_bundle(path)` でパスを直接指定して開くこともできる（テスト・Python 以外との突き合わせ用）
- 読み込み時に `manifest.csv` の SHA-256 が `dataset.json` と一致しなければ停止する

## CLI (Commands)

研究室 PC で使う．パスは引数に取らず，名前で指定する（[SPEC_01「設定ファイル」](SPEC_01_アーキテクチャ設計.md)）．

| コマンド | 役割 |
| --- | --- |
| `tiasdata init` | 設定ファイルに従って目録・ストアを作り，マイグレーションを適用する |
| `tiasdata import <取り込み元>` | 取り込み元を読み，ファイルをストアにコピーして症例・撮影・ファイル・所見・由来を登録する（取り込み元の形ごとに importer を用意する．最初は舌色の 9 フォルダ `tongue_color_folders`）．保留リストの番号は飛ばし，件数を報告する |
| `tiasdata dataset list` / `show <名前>` | データセットと版の一覧・件数．定義だけあって版の無いものは「版なし」，定義ファイルを消したものは「（定義ファイルなし）」．show は版ごとの件数・作成日時・hub_commit・manifest の SHA-256・備考 |
| `tiasdata dataset freeze <名前> [--note TEXT]` | 新しい版を作る（作った版の番号と件数，または「構成・定義が同じなので版を作らなかった」） |
| `tiasdata export <名前> --version <版>` | バンドルを `bundles/` に書き出す（上記「書き出し」）．書き出した件数・合計の大きさ・manifest の SHA-256，または「既にあったバンドルを照合して使った」を表示する |
| `tiasdata deliver <名前> --version <版> --to <サーバ名>` | 計算サーバへ渡す |
| `tiasdata check` | 点検: ストアのファイルのハッシュの照合，目録にあってストアに無いファイル・その逆，所見の参照切れ，凍結した版の構成の欠け，凍結した版の manifest の不一致（全版について記録した定義と構成から manifest を作り直し，記録した SHA-256 と照合する．ファイルの種類や撮影の症例が書き換えられた場合に見つかる），読み取り専用が外れたストアのファイル，書きかけの一時ファイル（`.tmp-`），ストアの想定外のファイル・フォルダ．件数だけを表示し，問題が 1 件以上なら終了コード 1．対応表は開かない |
| `tiasdata stats` | 症例数（撮影が 2 つ以上の症例数を含む）・撮影数，種類ごとのファイル数，訂正されていない所見の項目ごと・組み合わせごとの件数，訂正された所見の数．対応表は開かない |
| `tiasdata backup` | 目録を `backups/catalog-<日時>.sqlite` に複製する（SQLite のバックアップ API を使う） |

- 取り込みは何度実行しても同じ結果になるようにする（既に登録済みのファイルは飛ばす）
- 登録・版の作成は 1 つのトランザクションで行い，途中で失敗したら目録・対応表に何も残さない（取り込みでは対応表を目録の接続に ATTACH して 1 つのトランザクションにする）．ストアへのコピーはトランザクションの外なので，失敗時にコピー済みのファイルはストアに残る．これは次に取り込みが成功するまで `check` が「ストアにあって目録に無いファイル」として報告し，次の取り込みで再利用される
- 取り込みは，書き込む前に取り込み元の構成・保留リスト・対応表・同じ中身の画像が複数の番号に無いかをすべて検証する
- `import` の出力: 取り込み元の画像の件数，保留で飛ばした件数，対応表で使わない行の件数，登録済みで飛ばした件数，登録した件数（症例・撮影・ファイル・所見・由来），ストアにコピーした件数
- `--show-source-names`（`import`）を付けたときだけ，問題のあったファイルの元の名前・番号と，対応表で使わない行の行番号を最大 20 件まで表示する．患者 ID はどの場合も表示しない
- 取り込みはリポジトリの clone から実行する（保留リスト `holds/` をパッケージの位置からリポジトリのルートを辿って読むため）．openpyxl が要る（研究室 PC では `uv sync --no-dev --extra import`）
- 問題を見つけたら黙って飛ばさず，件数と理由を表示して停止する（`check`・`stats` は止めずに件数を報告する）

## 保留リスト (Hold List)

取り込み元のデータに，基盤では正解を決められない問題（同じ写真の重複・患者 ID やラベルの食い違い等）があるとき，確かめられるまで取り込まずに**保留**にする．推測で片方を選んで版に固定しないため．

- `holds/<取り込み元の名前>.toml` に置く（リポジトリで管理し，PR で変更する）．患者 ID は書かない
- 1 件ごとに `number`（取り込み元での番号）・`reason`（理由）・`resolve`（確かめる方法）・`since`（保留にした日）を書く
- `tiasdata import` は保留の番号を飛ばし，保留の件数を報告する（黙って外さない）．保留リストにあるのに取り込み元に無い番号は停止する（リストの誤りを見逃さないため）
- **保留リストのファイルが無い取り込み元は停止する**（設定で取り込み元の名前を誤ったときに保留が効かずに取り込まれるのを防ぐため．2026-10-06 のユーザー判断）．保留が無い取り込み元には，空の保留リスト（`[[hold]]` が 0 件）を PR で置く
- 形の誤り（未知のキー・必須キーの欠落・番号が 1 未満・番号の重複・`since` が日付でない）は停止する
- 確かめ終わったら行を消して取り込み直し，データセットの新しい版を作る．過去の版は変わらない
- 最初の保留リストは `holds/tiasjudgemodel.toml`（舌色データセットの重複 5 組のうち 9 枚．PLAN_02「舌色データセットの重複 5 組の扱い」）

## 出力の規則 (Output Rules)

CLI の出力・ログ・エラーメッセージに出してよいもの・出さないもの．Claude が SSH 経由で CLI を実行し，その出力を読むことを前提にする．

| 出してよい | 出さない |
| --- | --- |
| 件数・割合・サイズの合計 | 実際の患者 ID・対応表の中身 |
| 研究用 ID（`S…`・`C…`）・SHA-256 | 画像の画素値・画像そのもの |
| 種類名・所見の値（舌色の slug 等）・版 | 取り込み元のファイルの中身 |
| 設定ファイルで付けた名前（取り込み元・サーバ） | — |

- 元のファイル名（`source_name`）は個人をたどれる情報を含まないが，既定では表示しない（エラーの特定に必要な場合だけ表示する）
- 目録・ストア・対応表・取り込み元のパスは表示しない．例外として，「設定ファイルが見つからない」エラーでは設定ファイル自身のパスを表示する（データの場所ではなく，原因の特定に必要なため）
- 設定ファイルの検証エラーは「キーの位置: 理由」だけを 1 行で示し，入力された値（パス等）は表示しない
- 出力は常に UTF-8 で書き出す（Windows でパイプ・SSH 越しに cp932 で書き出されて手元で文字化けするのを防ぐため）
