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
54 changes: 54 additions & 0 deletions docs/todo/20260119-rm000-overflow-autofix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
---
目標: テキスト溢れ時の自動対処ルールを適用して超過行を抑制する
関連ブランチ: feat/overflow-autofix
関連Issue: #542
roadmap_item: RM-000 例: RMなしIssue C
---

- [x] ブランチ作成・初期コミット・push
- メモ: ブランチ名: feat/overflow-autofix / 初期コミット: docs(todo): add rm000 overflow autofix / push: 済み
- 必ず main からブランチを切る
- [x] 計画策定(スコープ・前提の整理)
- メモ: 承認済み Plan をそのまま転記する。以下の項目を含めること
- 対象スコープ: MappingStep で body 超過時に自動短縮し末尾に "..." を付与
- 対象ファイル: src/pptx_generator/pipeline/mapping/processor.py, tests/pipeline/mapping/test_mapping_step_layout_assignment.py
- 前提: max_lines は layout の text_hint を使用
- ドキュメント/コード修正方針: body を短縮し warnings に記録
- 確認・共有方法: ToDo 更新、Issue コメント
- 想定影響ファイル: generate_ready.json の body 出力
- リスク: 内容の末尾が削られる
- テスト方針: PYTHONPATH=src python -m pytest tests/pipeline/mapping/test_mapping_step_layout_assignment.py
- ロールバック方針: 追加処理を revert
- 承認メッセージ ID/リンク: ユーザー OK
- [x] 設計・実装方針の確定
- メモ: max_lines 超過時に body を短縮し、末尾行へ "..." を付与する
- [x] 設計・実装方針メモの共有(不要)
- [x] 方針メモを更新するまで以降の stage へ進まないこと
- [x] 実装
- メモ: src/pptx_generator/pipeline/mapping/processor.py, tests/pipeline/mapping/test_mapping_step_layout_assignment.py
- [x] テスト・検証
- メモ: PYTHONPATH=src python -m pytest -n 0 tests/pipeline/mapping/test_mapping_step_layout_assignment.py / 10 passed / coverage.xml / diff-cover: python -m diff_cover.diff_cover_tool coverage.xml --compare-branch origin/main / Coverage 100%(26 lines)
- [x] ドキュメント更新
- メモ: docs/todo/20260119-rm000-overflow-autofix.md, C:\PPT_test_textyabai\実施事項概要.md
- メモ: 対象外: docs/roadmap 配下, docs/requirements 配下, docs/design 配下, docs/runbook 配下, README.md / AGENTS.md
- [x] docs/roadmap 配下
- [x] docs/requirements 配下(実装結果との整合を確認)
- [x] docs/design 配下(実装結果との整合を確認)
- [x] docs/runbook 配下
- [x] README.md / AGENTS.md
- [x] 関連Issue 行動更新
- メモ: 関連Issue: #542
- [x] チェックリスト整合確認
- メモ: 親タスクと子タスクのチェックを整合
- [x] PR 作成
- メモ: PR #543 https://github.com/yurake/pptx_generator/pull/543

## メモ
- 連続性メモ(短文で上書き)
- 前提/制約: max_lines は layout の text_hint を使用
- 決定と根拠: 超過時は末尾行に "..." を付けて短縮
- リスク(UNCONFIRMED): 内容の末尾が削られる
- Now/Next: Now=レビュー待ち / Next=マージ対応
- テスト実績/抜け: PYTHONPATH=src python -m pytest -n 0 tests/pipeline/mapping/test_mapping_step_layout_assignment.py / 10 passed / diff-cover 100%
- テスト実績/抜け:
- 計画のみで完了する場合は、判断者・判断日・次アクション条件を記載する
57 changes: 42 additions & 15 deletions src/pptx_generator/pipeline/mapping/processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -398,25 +398,52 @@ def _apply_capacity_controls(

max_lines = layout.max_lines()
body = elements.get("body")
if max_lines is not None and isinstance(body, list) and len(body) > max_lines:
warnings.append(
f"body が許容行数 {max_lines} を超過しています(現在 {len(body)} 行)"
)
capacity_warnings.append(
MappingLogCapacityWarning(
slide_id=slide_id,
element="body",
max_lines=max_lines,
actual_lines=len(body),
layout_id=layout.layout_id,
if isinstance(body, list):
if max_lines is not None and len(body) > max_lines:
actual_lines = len(body)
trimmed_body, trimmed = self._trim_body_lines(body, max_lines)
if trimmed:
elements["body"] = trimmed_body
warnings.append(
"body が許容行数 {max} を超過していたため {max} 行に短縮しました(元 {actual} 行)".format(
max=max_lines,
actual=actual_lines,
)
)
capacity_warnings.append(
MappingLogCapacityWarning(
slide_id=slide_id,
element="body",
max_lines=max_lines,
actual_lines=actual_lines,
layout_id=layout.layout_id,
)
)
)

if isinstance(body, list) and not body:
warnings.append("body が空です")
if not body:
warnings.append("body が空です")

return fallback, ai_patches, warnings, capacity_warnings

@staticmethod
def _trim_body_lines(body: list[str], max_lines: int) -> tuple[list[str], bool]:
if max_lines <= 0:
return ([], bool(body))
if len(body) <= max_lines:
return (list(body), False)
trimmed = list(body[:max_lines])
if trimmed:
trimmed[-1] = MappingSlideProcessor._append_ellipsis(trimmed[-1])
return (trimmed, True)

@staticmethod
def _append_ellipsis(text: str) -> str:
stripped = text.rstrip()
if not stripped:
return "..."
if stripped.endswith("..."):
return stripped
return f"{stripped}..."

@staticmethod
def _build_auto_draw_payload(spec_slide: Slide | None) -> list[dict[str, float]]:
if spec_slide is None:
Expand Down
45 changes: 43 additions & 2 deletions tests/pipeline/mapping/test_mapping_step_layout_assignment.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
from pptx_generator.pipeline.base import PipelineContext
from pptx_generator.pipeline.mapping import MappingOptions, MappingStep
from pptx_generator.pipeline.mapping.processor import MappingSlideProcessor
from pptx_generator.pipeline.mapping.types import LayoutProfile
from pptx_generator.prepare import (
PrepareBodyBlock,
PrepareCard,
Expand Down Expand Up @@ -242,7 +243,7 @@ def test_mapping_step_applies_fallback_when_body_overflow(tmp_path: Path) -> Non

generate_ready_payload = json.loads(generate_ready_path.read_text(encoding="utf-8"))
body = generate_ready_payload["slides"][0]["elements"]["body"]
assert body == ["1行目", "2行目", "3行目"], "オーバーフロー時でも本文は維持されること"
assert body == ["1行目", "2行目..."], "オーバーフロー時は本文を短縮すること"
assert generate_ready_payload["slides"][0]["meta"]["fallback"] == "none"
assert generate_ready_payload["meta"]["template_path"] == template_path.name

Expand All @@ -255,7 +256,7 @@ def test_mapping_step_applies_fallback_when_body_overflow(tmp_path: Path) -> Non
assert mapping_payload["meta"]["ai_patch_count"] == 0
assert mapping_payload["meta"]["analyzer_issue_count"] == 0
assert slide_log["warnings"] == [
"body が許容行数 2 を超過しています(現在 3 行)"
"body が許容行数 2 を超過していたため 2 行に短縮しました(元 3 行)"
]
assert slide_log["capacity_warnings"] == [
{
Expand Down Expand Up @@ -356,6 +357,46 @@ def test_mapping_step_assigns_table_anchor(tmp_path: Path) -> None:
assert "table" not in slide_elements


def test_mapping_capacity_controls_warn_on_empty_body(tmp_path: Path) -> None:
processor = MappingSlideProcessor(
options=MappingOptions(output_dir=tmp_path),
layout_catalog={},
)
layout = LayoutProfile(
layout_id="layout_basic",
layout_name="Basic",
usage_tags=(),
text_hint={"max_lines": 2},
media_hint={},
)
elements = {"body": []}

fallback, ai_patches, warnings, capacity_warnings = processor._apply_capacity_controls(
slide_id="s01",
layout=layout,
elements=elements,
)

assert fallback.applied is False
assert ai_patches == []
assert warnings == ["body が空です"]
assert capacity_warnings == []
assert elements["body"] == []


def test_mapping_capacity_controls_helpers() -> None:
trimmed, changed = MappingSlideProcessor._trim_body_lines(["本文"], 0)
assert trimmed == []
assert changed is True

trimmed, changed = MappingSlideProcessor._trim_body_lines(["本文"], 2)
assert trimmed == ["本文"]
assert changed is False

assert MappingSlideProcessor._append_ellipsis("") == "..."
assert MappingSlideProcessor._append_ellipsis("本文...") == "本文..."


def test_mapping_step_errors_on_invalid_content_artifact(tmp_path: Path, caplog) -> None:
spec = _build_spec(["本文"])
context = PipelineContext(spec=spec, workdir=tmp_path)
Expand Down
Loading