バージョン: 1.0 最終更新: 2026-05-29 ステータス: Active — このブリーフに従って実装を進める
このドキュメントは tablecodec を SPEC.md に厳密に準拠した実装 に到達させるための作業指示書です。
| 文書 | 役割 | 優先度 |
|---|---|---|
SPEC.md |
仕様の唯一の正典。挙動・契約・不変条件はここで定義 | 最高 |
IMPLEMENTATION_BRIEF.md (本書) |
実装順序、技術選定、品質基準 | 高 |
CLAUDE.md / グローバルガイドライン |
コーディング標準、コミット規約、共通 lint ルール | 高 |
矛盾が生じた場合、優先順位は SPEC.md > IMPLEMENTATION_BRIEF.md > CLAUDE.md(グローバルガイドライン) とする。SPEC を破る変更を提案する場合は、先に SPEC への PR を出して合意を取ること。
SPEC.md §1〜§2 を参照。実装側の追加ミッションとして:
- 各マイルストーン完了時点で
mainブランチが常にリリース可能な状態であること - 第三者(Docling、PaddleOCR、in-house パイプライン)が読まずに動かせるドキュメント密度を維持すること
- パフォーマンス回帰をコミット単位で検知できる体制(M3 以降)
Kent Beck の流儀に従う。例外なし:
- Red: 失敗するテストを 1 件書く。コミットメッセージ
test(scope): describe failing case - Green: 最小実装でテストを通す。コミットメッセージ
feat(scope): minimal impl - Refactor: 振る舞いを変えずに構造を整える。コミットメッセージ
refactor(scope): tidy
1 ステップ = 1 コミットを基本とする。複数ステップをまとめたコミットは PR レビューで reject 対象。
構造変更と振る舞い変更を同じコミットに混ぜない。リネーム、ファイル分割、import 整理は独立した tidy コミットとして提出。コミットメッセージは refactor(scope): tidy first - <description>。
type(scope): subject 形式を厳守。scope は概ね SPEC の章番号またはモジュール名 (ir, codec, cli, ci など)。
許可された type: feat, fix, refactor, test, docs, chore, build, ci, perf.
PR は M0/M1/... 単位、または同マイルストーン内の論理的に分離可能な単位で出す。1 PR に複数マイルストーンを詰め込むのは reject。
グローバルガイドライン(CLAUDE.md)で定義された Semgrep meta-rules、justfile 規約、Conventional Commits フォーマッタ、.yaml 拡張子(.yml ではない)の規律はそのまま継承する。本ブリーフはそれらに追加で課す制約のみを記述する。
| 項目 | 選定 | 根拠 |
|---|---|---|
| Python 最小バージョン | 3.11 | PEP 604 (int | None), dataclass(slots=True), Self 型 |
| パッケージマネージャ | uv | プロジェクト標準 |
| ビルドバックエンド | hatchling | uv との相性、シンプル |
| Lint | ruff | グローバルガイドライン準拠 |
| 型チェック | pyright (strict) | mypy より速く、推論が強い |
| テストランナー | pytest | デファクト |
| Property-based test | hypothesis | M1 の IR 不変条件テストで必須 |
| カバレッジ | coverage.py | pytest-cov 経由 |
| CLI フレームワーク | click | M6、[cli] extra でのみ依存 |
| タスクランナー | just | プロジェクト標準 |
| CI | GitHub Actions | OSS デファクト |
| ライセンス | MIT | SPEC §16 で確定済み |
コアパッケージは上記のうち ruff、pyright、pytest、hypothesis、coverage のみを 開発依存として持つ。ランタイム依存はゼロ(SPEC §13)。
最終的に到達すべきレイアウト(M8 時点):
tablecodec/
├── .github/
│ └── workflows/
│ ├── ci.yaml # lint + type + test on push/PR
│ ├── benchmark.yaml # M3 以降、パフォーマンス回帰検知
│ └── release.yaml # tag push で PyPI 公開
├── docs/
│ ├── loss_matrix.md # M5 で自動生成
│ └── format_support.md # 各 codec の対応表
├── src/
│ └── tablecodec/
│ ├── __init__.py # public API のみ re-export
│ ├── ir.py # GridCell, TableSample, BBox
│ ├── _invariants.py # I-01〜I-07 (private)
│ ├── validate.py # validate(), profiles
│ ├── io.py # open(), detect(), streaming utilities
│ ├── loss.py # analyze_loss()
│ ├── cli.py # click app (only in [cli] extra)
│ └── codecs/
│ ├── __init__.py # registry: get(), register(), detect()
│ ├── _base.py # Codec protocol
│ ├── pubtabnet.py # pubtabnet-1.0.0, pubtabnet-2.0.0
│ ├── fintabnet.py
│ ├── otsl.py # otsl-1.0.0
│ ├── tableformer.py
│ ├── doctags.py # doctags-tables
│ ├── pubtables1m.py # read-only
│ └── tablebank.py
├── tests/
│ ├── conftest.py
│ ├── strategies.py # hypothesis strategies
│ ├── test_ir.py
│ ├── test_invariants.py
│ ├── test_invariants_hypothesis.py
│ ├── test_validate.py
│ ├── test_io_streaming.py
│ ├── test_loss.py
│ ├── test_cli.py
│ ├── codecs/
│ │ ├── test_pubtabnet.py
│ │ ├── test_otsl.py
│ │ └── ...
│ └── fixtures/
│ ├── pubtabnet/
│ ├── otsl/
│ └── ...
├── pyproject.toml # ruff [tool.ruff] / pyright [tool.pyright] もここに統一
├── justfile
├── .gitignore
├── .pre-commit-config.yaml
├── .semgrep/rules/ # 「コアに3rd party import 禁止」等を強制(+ co-located test)
├── LICENSE # MIT
├── README.md
├── SPEC.md
├── IMPLEMENTATION_BRIEF.md
└── CHANGELOG.md # Keep a Changelog 形式
各マイルストーンは独立した PR として提出する。Acceptance Criteria がすべて緑になるまで次に進まない。
Goal: リポジトリの土台を整え、just ci が空のテスト一式に対してグリーンになる状態を作る。
Deliverables:
pyproject.toml(hatchling, Python 3.11+, extras:teds,cli,hf,all,dev)justfile(targets:install,hooks,test,lint,type,fmt,cov,semgrep,semgrep-test,ci,clean)- ruff (
[tool.ruff]) と pyright ([tool.pyright]) はpyproject.tomlに統一 .github/workflows/ci.yaml(matrix: Python 3.11, 3.12, 3.13 / Ubuntu, macOS).gitignore,.pre-commit-config.yaml.semgrep/rules/: ルール「src/tablecodec/配下で許可された stdlib モジュール以外の import を禁止」等を含む(各ルールは co-located なsemgrep testfixture 付き)LICENSE(MIT)README.md(最小限のあいさつと SPEC へのリンクのみ)CHANGELOG.md(Unreleased セクションのみ)src/tablecodec/__init__.py:__version__ = "0.0.1"のみtests/test_smoke.py: import と version assertion
Acceptance Criteria:
-
just ciがローカルで成功 - CI が GitHub で緑
-
pip install -e .が成功 -
semgrep --config .semgrep/rules/ src/がノイズなく走り、semgrep test .semgrep/rules/が通る
TDD ノート: M0 は土台整備なので Red-Green-Refactor は test_smoke.py のみに適用。それ以外は構造変更コミット。
Goal: SPEC §5 の IR と §5.2 の不変条件 I-01〜I-07 を実装し、property-based testing で網羅する。
Deliverables:
src/tablecodec/ir.py:BBox,GridCell,TableSamplesrc/tablecodec/_invariants.py: 各 I-XX を独立した関数として実装src/tablecodec/validate.py:validate(sample, profile=...),profiles.{LENIENT,DEFAULT,PUBTABNET_2_0,TABLEFORMER,STRICT}tests/strategies.py:gridcell_st,tablesample_st,valid_tablesample_sttests/test_ir.py: dataclass の frozen / slots / hashable 性質tests/test_invariants.py: I-01〜I-07 を 1 関数 1 テストで網羅tests/test_invariants_hypothesis.py: 「valid な TableSample を生成 → すべての invariant がパス」「特定の invariant を壊した時、その invariant のみが失敗を報告」
Acceptance Criteria:
- I-01〜I-07 のすべてに positive / negative テストあり
- hypothesis が 10,000 ケース回って fail なし
-
TableSampleが pickle 可能、hashable、__slots__持ち - coverage 100%(
_invariants.py,ir.py) - pyright strict で warning ゼロ
TDD 順序の指針:
test_ir.py::test_gridcell_is_frozenを Red →GridCellの最小定義で Green- 同様に
TableSampleを進める - I-01 から順に Red → Green → Refactor を繰り返す
- 全 invariant が個別関数で揃ってから
validate.pyを組み立てる - 最後に hypothesis テストで網羅
Goal: SPEC §6 の Codec 契約を確定し、最初の codec として pubtabnet-2.0.0 を実装。
Deliverables:
src/tablecodec/codecs/_base.py:CodecProtocolsrc/tablecodec/codecs/__init__.py: registry (register,get,detect,list_codecs)src/tablecodec/codecs/pubtabnet.py:PubTabNet20Codec(read + write)tests/codecs/test_pubtabnet.py: 単体テスト + round-trip + 損失申告検証tests/fixtures/pubtabnet/: 公式サンプル数件(PMC license 上問題なし、exploring_PubTabNet_dataset.ipynbの examples フォルダから借用、出典明記)
Acceptance Criteria:
-
codecs.get("pubtabnet-2.0.0").read(f)がストリームでTableSampleを yield - read → write → read の往復で
lossy_read()申告以外のフィールドが完全一致 - HTML トークン列の rowspan/colspan パースが正しく
GridCell.rowspan/colspanに反映 - 空セル(
bbox欠落)がbbox=Noneで表現される - 巨大ファイル(10,000 件以上)で peak memory が定数(M3 で正式測定するが、M2 でも観察)
-
lossy_read()とlossy_write()の申告が round-trip テストと一致
TDD 順序:
CodecProtocol を_base.pyに置く- registry の
register/getを Red → Green PubTabNet20Codecのシンプルな単一サンプル read を Red → Green- rowspan/colspan のあるサンプルを追加 Red → Green
- write を追加、round-trip テスト
- lossy_* を実装、整合性テスト
Goal: SPEC §10 のストリーミング保証を担保し、レガシー codec を追加。
Deliverables:
src/tablecodec/io.py:open(path, codec=None),detect(source)src/tablecodec/codecs/pubtabnet.py:PubTabNet10Codecを追加(bbox なし)tests/test_io_streaming.py: メモリ使用量がデータサイズに依存しないことを assert(tracemallocで確認).github/workflows/benchmark.yaml: pytest-benchmark で baseline 計測docs/format_support.md: 対応 codec 表(自動生成スクリプト含む)
Acceptance Criteria:
- 100,000 件の jsonl をストリーミング処理しても peak memory < 50MB
-
detect()が先頭 5 行で codec 名を返す - benchmark の baseline が
mainに commit される - 1.0.0 と 2.0.0 が同じ jsonl で誤判定なく区別される
Goal: 異なる token language である OTSL を実装し、Codec 契約の汎用性を実証する。
Deliverables:
src/tablecodec/codecs/otsl.py:OTSL10Codectests/codecs/test_otsl.py: round-trip + IBM 公式 reference(docling-ibm-models/tableformer/otsl.py)との挙動一致テスト(テスト時のみ optional 依存)- IBM の OTSL リファレンスを
[dev]extra でインストールし、cross-validation テストを optional に実行
Acceptance Criteria:
- OTSL の 5 トークン語彙(
fcel,ecel,lcel,ucel,xcel+nl)すべてが正しく解釈される - square table assumption の検証ロジックを持つ
- OTSL → IR → OTSL のラウンドトリップが完全一致
- OTSL → IR → PubTabNet HTML の変換が SPEC §9 の loss 申告どおりに動作
重要(2026-05-28 更新, ADR 0005 で改訂): 当初はクリーンルーム方針(IBM の otsl.py を逐語コピーせず論文から自前実装)だった。しかし live e2e で SynthTabNet の複雑な span 構造に対し自前 reconstruction が誤動作することが判明し(HTML 経路は同一テーブルを正しく解釈)、docling の otsl_to_html アルゴリズムを 帰属付きで適応移植する方針に改めた。docling-ibm-models は私たち同様 MIT(Copyright (c) 2024 IBM)なので、著作権表示と MIT 文を保持すれば再利用可。帰属は _otslgrid.py ヘッダ・THIRD_PARTY_NOTICES.md・ADR 0005 に記録。ライセンスは MIT のまま(Apache 化は不要)。クリーンルームは依然デフォルトで、許諾ライセンス + 帰属記録 + core invariant 維持を満たす場合のみ適応移植を許す。
Goal: SPEC §9 を実装。tablecodec の独自価値の核を完成させる。
Deliverables:
src/tablecodec/loss.py:analyze_loss(source: str, target: str) -> LossReporttests/test_loss.py: 全 codec 組み合わせの分析が安定して動作docs/loss_matrix.md: スクリプトで自動生成、CI で更新が必須
Acceptance Criteria:
-
analyze_lossが静的に動作(実データを読まずに codec のlossy_*メタから判定) -
round_trip_classificationが"lossless"/"structure-preserving"/"lossy"を正しく返す - loss_matrix.md がコミット差分なしで再生成可能
Goal: SPEC §12 の CLI サブコマンドを実装。
Deliverables:
src/tablecodec/cli.py: click ベース、[cli]extrapyproject.tomlの[project.scripts]にtablecodec = "tablecodec.cli:main"tests/test_cli.py: click のCliRunnerで各サブコマンドを検証README.mdに CLI セクション追加
Acceptance Criteria:
-
tablecodec validateが validation failure で非ゼロ exit code -
tablecodec convert --dry-runがデータを読まずに loss レポートのみ返す -
tablecodec statsがストリーミングで動作 - CLI なしでも core が動く(
[cli]extra が optional であることを CI で確認)
Goal: 別リポジトリ tablecodec/conformance を立ち上げ、SPEC §11 の初期 fixtures を配置。
Deliverables:
- 別リポジトリ作成:
github.com/hironow/tablecodec-conformance(個人 org 配下、後でtablecodecorg に移行可能) INDEX.jsonの JSON Schema 定義- 初期 fixtures: pubtabnet-2.0.0 と otsl-1.0.0 各 3 件以上
tablecodec本体側にtests/test_conformance.py: conformance リポジトリを git submodule または HF dataset として取得して通す
Acceptance Criteria:
- Conformance リポジトリが MIT で公開
-
INDEX.jsonSchema が dereferenceable - 本体側のテストが Conformance を取得して PASS
方針変更(実行時の決定): 当初は M1-M8 を経て v0.1.0 で公開する計画だったが、
codec を 1 つずつ 0.0.x の patch bump として出荷する方針に切り替えた(現在
0.0.19、9 codec + [teds] + §8 STRICT 出荷済み、docling bridge は別 package
0.0.2)。2026-06-07 に PyPI へ初公開(v0.0.18) — OIDC Trusted Publishing で
トークンレス、PEP 740 attestations + SLSA build provenance 付き。一回限りの
人間側設定(PyPI pending publisher / GitHub Environment・Ruleset)手順は
gitignore 下の private/PYPI_RELEASE_STEPS.md。
Goal: PyPI 公開、GitHub Release、告知(設定完了後)。
Deliverables:
- version は 0.0.x のまま(codec 追加 = patch bump、
pyproject.toml+src/tablecodec/__init__.pyを同期) - リリース時に
CHANGELOG.mdの[Unreleased]を[0.0.N]へ昇格 .github/workflows/release.yaml(v*タグで発火、Trusted Publishing / OIDC) — サプライチェーン硬化済み(全 action full-SHA pin / SLSA build provenance / PEP 740 attestations / skip-existing / Takumi Guard 経由の screened build / Dependabot 7日 cooldown、ADR 0014)。v0.0.18 で初公開・全ジョブ稼働確認済み- README に installation / basic usage / SPEC リンク(済)
- (任意)GitHub Discussions / Issues テンプレート
Acceptance Criteria(公開を実施する場合):
-
pip install tablecodecが動作(core は zero-dep) -
pip install "tablecodec[cli]"が動作 -
import tablecodec; tablecodec.__version__が現行 0.0.x と一致 - PyPI ページに README が正しく表示される
- GitHub の "About" 欄に SPEC リンクと一文の説明
以下を含む PR は内容によらず reject、または該当箇所の修正を要求する:
| アンチパターン | 検出方法 |
|---|---|
src/tablecodec/ 配下に stdlib 以外の import(CLI/loss/io 除く) |
semgrep meta-rule |
ir.py または _invariants.py に Pydantic を持ち込む |
semgrep |
pytest.mark.skip をコメントなしで使う |
ruff custom rule |
# type: ignore をコメントなしで使う |
pyright |
| 構造変更と振る舞い変更が同一コミットに含まれる | コードレビュー |
| 1 PR に複数マイルストーン | PR テンプレ + レビュー |
| Conventional Commits 違反 | commit lint |
| テストなしのバグ修正 | レビュー |
jsonl 全体を f.read() でメモリにロード |
semgrep meta-rule + コードレビュー |
numpy, pillow, opencv のいずれかを core にコミット |
semgrep meta-rule |
| 公式コード(IBM otsl.py 等)からの逐語コピー | ライセンスチェッカ + 人間レビュー |
マイルストーン PR を merge する前に、以下をすべて満たすこと:
- そのマイルストーンの Acceptance Criteria が全て✓
-
just ciがローカルでグリーン - GitHub Actions がグリーン(matrix 全組み合わせ)
- coverage が 95% 以上(M1 以降の新規モジュール)
- ruff、pyright strict が clean
- semgrep がノイズなく走る
- CHANGELOG.md に Unreleased エントリ追加
- Conventional Commits 違反なし
- PR description に SPEC のどのセクションを実装したか明記
- 公開 API を追加した場合、docstring と型ヒント完備
実績(0.0.x で達成済み): 当初は codec を 0.2.0〜0.4.0 の minor で追加する計画
だったが、実際には全 9 codec(pubtabnet 1.0/2.0、otsl、fintabnet、fintabnet-otsl、
tableformer、tablebank、pubtables-1m、doctags-tables)を 0.0.x の patch bump で
出荷した(現在 0.0.19)。TEDS extra は 0.0.16 で実装済み(tablecodec.teds、
ADR 0011)。§8 STRICT は 0.0.17(ADR 0012)。docling bridge は read+write を
packages/tablecodec-docling/(own 0.0.2)に in-repo monorepo で実装済み
(ADR 0013、別 repo 抽出は publish 前)。
未来事項はすべてこの §8 に集約します。docs/spec.md は現状の契約のみ、
docs/handover.md はアクティブなセッション状態のみを書き、未来の roadmap/TODO は
重複させずにここを参照します。
| バージョン | 内容 |
|---|---|
| 0.0.x(継続) | 下記「抽出・公開」「機能フォローアップ」「Open Questions」を順次 |
| 0.9.0 | Public API freeze、RC1 |
| 1.0.0 | API frozen、3 年 LTS スタート |
- docling bridge を別 repo へ抽出(publish 前、ADR 0013)。現状は
packages/tablecodec-docling/の in-repo monorepo。 - Conformance Suite を別 vendor-neutral repo へ抽出(v1.0 前、ADR 0001)。 corpus は全 9 codec が expected-IR を持つ(pubtabnet-2.0 / otsl は各3ケース、 他7 codec は各1ケース)。各 codec のケース数をさらに増やす余地あり。
- PyPI 公開 — 完了(v0.0.18, 2026-06-07)。OIDC Trusted Publishing で
トークンレス公開、PEP 740 attestations + SLSA build provenance 付き(ADR 0014)。
人間側の一回限り設定(PyPI pending publisher / GitHub Environment
release/ Ruleset)も適用済み。以降の steady-state リリース手順は gitignore 下のprivate/PYPI_RELEASE_STEPS.md§C。
- codec への image-dims populate: pubtables-1m が PASCAL VOC の
<size>から width/height を読めば、§8 STRICT が実データで発火する(docling bridge が page size →image_width/heightで先行実証済み、ADR 0012 follow-up)。当該 codec の payload・round-trip・lossy_writeに波及。 - docling の e2e(docling-core 直読み): 現状は意図的な non-gap(bridge の round-trip + 30 テストがカバレッジ)。別 repo 抽出のタイミングで再検討。
- OQ-1:
TableSample.cellsを ordered(現行 spec)/ unordered(set)どちらに するか。ordering は直列化を単純化するが正規化要件を生む。 - OQ-2: セル内多行テキストの tokenization。per-character(PubTabNet)か per-word(PubTables-1M)か。
- OQ-3:
bboxの float 対応。現状 integer-only だが PubTables-1M は float。 なお §8 STRICT の bbox-in-image は containment 判定で int/float に非依存ゆえ、 OQ-3 の解決に依存しない(ADR 0012)。 - OQ-4: IR の JSON Schema を dataclass 定義と別に公開するか(cross-language 用途)。
実装中に SPEC で曖昧な点や、本ブリーフのルールを破る必要が生じた場合:
- 小規模な疑義: issue tracker のチケットコメントで議論し、決定を SPEC または本書の更新 PR に反映
- SPEC を変更する必要がある場合: 先に
SPEC.mdへの PR を出し、merge 後に実装に着手 - 本ブリーフのルールを一時的に緩める必要: PR description に明記し、レビュアの明示的な承認を得る(例:M1 で型ヒントの一部省略を許可、など)
「黙って迂回する」「コメントなしで例外を作る」は禁止。
# Prerequisites
brew install just uv # mise でも可
# Repo clone & setup
git clone https://github.com/hironow/tablecodec
cd tablecodec
uv venv --python 3.11
source .venv/bin/activate
uv pip install -e ".[dev,cli,teds]"
# git hooks(prek、.pre-commit-config.yaml を使用)
just hooks # = prek install(prek は brew install prek 等で導入)
# 動作確認
just cijust ci が緑になれば着手可能。
- 本書と SPEC.md を熟読 — 不明点を issue tracker にチケット化
- M0 を着手 — リポジトリの土台を確実に
- M0 の PR を merge してから M1 — 早期に CI 緑を実現
- M1 → M2 → ... と順番に
- 各マイルストーン完了時に issue tracker のチケットをクローズし、CHANGELOG を更新
並行作業は M5 と M6 以降のみ許容(依存関係が独立しているため)。M0〜M4 は厳密に逐次。
default:
@just --list
install:
uv pip install -e ".[dev,cli,teds]"
test:
pytest tests/ -v
lint:
ruff check src/ tests/
ruff format --check src/ tests/
type:
pyright src/
fmt:
ruff format src/ tests/
ruff check --fix src/ tests/
cov:
pytest tests/ --cov=tablecodec --cov-report=term-missing --cov-report=html
semgrep:
semgrep --config .semgrep/rules/ --error src/
semgrep-test:
semgrep test .semgrep/rules/
ci: lint type test semgrep semgrep-test
@echo "✓ All checks passed"
clean:
rm -rf .pytest_cache .ruff_cache htmlcov .coverage dist build *.egg-info
find . -type d -name __pycache__ -exec rm -rf {} +ルールは実装済みで .semgrep/rules/<category>/<rule-id>.yaml に1ファイル1ルールで
存在する(各ルールは co-located な <rule-id>.py の semgrep test fixture 付き):
core-deps/tablecodec-no-third-party-imports-in-core.yaml— コアは stdlib のみ (cli.py/teds.pyは core-external として除外、loss.pyは 含む)。streaming/tablecodec-no-full-file-read.yaml—$F.read()/$F.readlines()禁止。typing/tablecodec-no-untagged-type-ignore.yaml— 無印# type: ignore禁止。
just semgrep で scan、just semgrep-test でルール正しさを検証(両方 just ci)。
詳細は .semgrep/README.md を参照。
chore: initial empty repository # README, LICENSE
build: pyproject.toml, hatchling, Python 3.11+ # 設定のみ
build(ci): justfile and GitHub Actions skeleton # CI 土台
chore(lint): ruff, pyright, semgrep configurations # lint 設定
test(smoke): verify package imports and exposes version # 最初のテスト
これで M0 完了。M1 への着手はここから。
このブリーフは生きたドキュメントです。マイルストーン完了時にレビューし、必要に応じて更新の PR を出してください。