diff --git a/.github/aicodingflow-tests/test_product_docs_sync.py b/.github/aicodingflow-tests/test_product_docs_sync.py index 348aa1bd..69ece102 100644 --- a/.github/aicodingflow-tests/test_product_docs_sync.py +++ b/.github/aicodingflow-tests/test_product_docs_sync.py @@ -342,6 +342,57 @@ def test_pr_body_includes_decision_source_pr_and_processed_entries(self) -> None self.assertIn("PR #86: Add first flow", body) self.assertIn("Document first flow.", body) + def test_pr_body_truncates_large_processed_decision_history(self) -> None: + entries = [] + for number in range(1, 35): + entries.append( + { + "pr": number, + "title": f"PR {number}", + "url": f"https://example.test/pull/{number}", + "merged_at": f"2026-05-25T01:{number:02d}:00Z", + "docs_update": "required", + "reason": "Changed product behavior. " * 400, + "affected_docs": [f"docs/product/raw/{number}.md"], + "proposed_patch": "Document the product behavior in detail. " * 400, + } + ) + + body = body_writer.build_body( + pr_number="99", + pr_url="https://example.test/pull/99", + result={ + "docs_update": "required", + "reason": "Product behavior changed.", + "affected_docs": ["docs/product/raw/flow.md"], + "source_context": ["PR #99"], + "proposed_patch": "Document the flow.", + }, + ledger={"version": 1, "entries": entries}, + ) + + self.assertLessEqual(len(body), body_writer.MAX_GITHUB_MARKDOWN_CHARS) + self.assertIn("Omitted 14 older processed decisions", body) + self.assertNotIn("PR #1: PR 1", body) + self.assertIn("PR #34: PR 34", body) + self.assertIn("Truncated change summary", body) + + def test_pr_comment_truncates_large_patch_summary(self) -> None: + comment = body_writer.build_comment( + pr_number="87", + pr_url="https://example.test/pull/87", + result={ + "docs_update": "required", + "reason": "Docs changed.", + "affected_docs": ["docs/product/raw/flow.md"], + "source_context": ["PR #87"], + "proposed_patch": "Long patch summary. " * 1000, + }, + ) + + self.assertLessEqual(len(comment), body_writer.MAX_GITHUB_MARKDOWN_CHARS) + self.assertIn("Truncated patch summary", comment) + def test_pr_comment_includes_current_run_decision(self) -> None: comment = body_writer.build_comment( pr_number="87", diff --git a/.github/scripts/write_product_docs_sync_pr_body.py b/.github/scripts/write_product_docs_sync_pr_body.py index 9f3132e6..545b6790 100644 --- a/.github/scripts/write_product_docs_sync_pr_body.py +++ b/.github/scripts/write_product_docs_sync_pr_body.py @@ -11,6 +11,25 @@ DEFAULT_LEDGER_PATH = "docs/product/.product-docs-sync-ledger.json" +MAX_GITHUB_MARKDOWN_CHARS = 60_000 +MAX_FIELD_CHARS = 4_000 +MAX_LEDGER_FIELD_CHARS = 800 +MAX_LEDGER_ENTRIES_IN_BODY = 20 + + +def truncate_text(value: Any, max_chars: int, *, label: str = "content") -> str: + text = str(value or "").strip() + if len(text) <= max_chars: + return text + suffix = f"\n\n[Truncated {label}; full content is available in workflow artifacts.]" + return text[: max(0, max_chars - len(suffix))].rstrip() + suffix + + +def enforce_markdown_limit(markdown: str, max_chars: int = MAX_GITHUB_MARKDOWN_CHARS) -> str: + if len(markdown) <= max_chars: + return markdown + suffix = "\n\n[Truncated to stay within GitHub body length limits. See workflow artifacts for full details.]\n" + return markdown[: max(0, max_chars - len(suffix))].rstrip() + suffix def load_ledger(path: Path) -> dict[str, Any]: @@ -41,12 +60,16 @@ def format_decision(entry: dict[str, Any]) -> list[str]: lines = [ heading, f" - docs update: `{entry.get('docs_update')}`", - f" - reason: {entry.get('reason') or ''}", + f" - reason: {truncate_text(entry.get('reason') or '', MAX_LEDGER_FIELD_CHARS, label='reason')}", f" - url: {entry.get('url') or ''}", " - affected docs:", *format_list(entry.get("affected_docs") or []), ] - proposed_patch = str(entry.get("proposed_patch") or "").strip() + proposed_patch = truncate_text( + entry.get("proposed_patch") or "", + MAX_LEDGER_FIELD_CHARS, + label="change summary", + ) if proposed_patch: lines.extend([" - change summary:", *[f" {line}" for line in proposed_patch.splitlines()]]) return lines @@ -61,14 +84,21 @@ def build_body(pr_number: str, pr_url: str, result: dict[str, Any], ledger: dict affected_docs = result.get("affected_docs") or [] source_context = result.get("source_context") or [] processed_entries = ledger_entries(ledger or {}) - return "\n".join( + shown_entries = processed_entries[-MAX_LEDGER_ENTRIES_IN_BODY:] + omitted_count = max(0, len(processed_entries) - len(shown_entries)) + omitted_lines = ( + [f"- Omitted {omitted_count} older processed decisions to keep the PR body within GitHub limits.", ""] + if omitted_count + else [] + ) + body = "\n".join( [ "Synchronizes long-term product docs from merged implementation pull requests.", "", "Latest decision:", f"- source PR: #{pr_number}", f"- docs update: `{result.get('docs_update')}`", - f"- reason: {result.get('reason')}", + f"- reason: {truncate_text(result.get('reason'), MAX_FIELD_CHARS, label='reason')}", f"- source URL: {pr_url}", "", "Affected docs:", @@ -78,18 +108,20 @@ def build_body(pr_number: str, pr_url: str, result: dict[str, Any], ledger: dict *(f"- {item}" for item in source_context), "", "Patch summary:", - str(result.get("proposed_patch") or ""), + truncate_text(result.get("proposed_patch"), MAX_FIELD_CHARS, label="patch summary"), "", "Processed decisions in this PR:", + *omitted_lines, *( line - for entry in processed_entries + for entry in shown_entries for line in [*format_decision(entry), ""] ), "This PR may accumulate multiple product docs sync decisions until it is reviewed and merged.", "", ] ) + return enforce_markdown_limit(body) def build_comment(pr_number: str, pr_url: str, result: dict[str, Any]) -> str: @@ -99,7 +131,7 @@ def build_comment(pr_number: str, pr_url: str, result: dict[str, Any]) -> str: "", f"- source PR: #{pr_number}", f"- docs update: `{result.get('docs_update')}`", - f"- reason: {result.get('reason') or ''}", + f"- reason: {truncate_text(result.get('reason'), MAX_FIELD_CHARS, label='reason')}", f"- source URL: {pr_url}", "", "Affected docs:", @@ -108,12 +140,12 @@ def build_comment(pr_number: str, pr_url: str, result: dict[str, Any]) -> str: if not affected_docs: lines.append("- none") - proposed_patch = str(result.get("proposed_patch") or "").strip() + proposed_patch = truncate_text(result.get("proposed_patch") or "", MAX_FIELD_CHARS, label="patch summary") lines.extend(["", "Patch summary:", proposed_patch or "None."]) if result.get("docs_update") == "uncertain": lines.extend(["", "This docs update is uncertain and needs maintainer confirmation."]) lines.append("") - return "\n".join(lines) + return enforce_markdown_limit("\n".join(lines)) def main() -> int: diff --git a/docs/product/raw/product-docs-sync-workflow.md b/docs/product/raw/product-docs-sync-workflow.md index 2a8e719d..2ba8d58c 100644 --- a/docs/product/raw/product-docs-sync-workflow.md +++ b/docs/product/raw/product-docs-sync-workflow.md @@ -91,6 +91,10 @@ PR comment,记录本次 run 的 source PR、`docs_update` 决策、原因、 如果最新决策是 `uncertain`,追加 comment 也应明确提示需要维护者确认。workflow 不会编辑旧的 bot comment 来替代追加记录。 +生成 PR body 和 comment 时,workflow 会保守控制输出长度,避免超过 GitHub body/comment +长度限制。最新同步决策会保留较完整摘要;历史 ledger 决策只展示最近一批,并对过长的 reason +或 patch summary 做截断,完整上下文仍可从 workflow artifacts 和 ledger 文件中追溯。 + 长期产品文档只有在同步 PR 经过 review 并合并后才成为权威产品知识。 -来源:PR #179,https://github.com/Terry-Mao/AICodingFlow/pull/179;PR #184,https://github.com/Terry-Mao/AICodingFlow/pull/184;PR #187,https://github.com/Terry-Mao/AICodingFlow/pull/187;PR #188,https://github.com/Terry-Mao/AICodingFlow/pull/188;PR #194,https://github.com/Terry-Mao/AICodingFlow/pull/194;PR #215,Issue #214;PR #217,Issue #216。 +来源:PR #179,https://github.com/Terry-Mao/AICodingFlow/pull/179;PR #184,https://github.com/Terry-Mao/AICodingFlow/pull/184;PR #187,https://github.com/Terry-Mao/AICodingFlow/pull/187;PR #188,https://github.com/Terry-Mao/AICodingFlow/pull/188;PR #194,https://github.com/Terry-Mao/AICodingFlow/pull/194;PR #215,Issue #214;PR #217,Issue #216;Issue #257。 diff --git a/docs/product/wiki/concepts/product-docs-sync-workflow.md b/docs/product/wiki/concepts/product-docs-sync-workflow.md index 9042344e..ee4f7f7c 100644 --- a/docs/product/wiki/concepts/product-docs-sync-workflow.md +++ b/docs/product/wiki/concepts/product-docs-sync-workflow.md @@ -57,6 +57,7 @@ Agent 写入 `product-docs-sync-result.json`,其中 `docs_update` 只能是: - 普通同步 PR title 是 `Update product docs`;最新决策为 `uncertain` 时使用 draft PR 和 `Draft: Update product docs` title。 - `not-needed` 不修改权威 markdown 文档,但 ledger 更新仍会创建或更新只记录同步决策的 PR。 - 每次创建或更新同步 PR 后,workflow 都会追加一条 conversation comment 记录本次 run;旧 bot comment 不会被编辑替代。 +- PR body 和 comment 会保守控制长度;历史 ledger 决策只展示最近一批,过长字段会被截断,完整上下文保留在 workflow artifacts 和 ledger 中。 - 长期产品文档只有同步 PR 经 review 并合并后才成为权威知识。 ## Related Concepts diff --git a/docs/product/wiki/log.md b/docs/product/wiki/log.md index abc24935..d3deacf5 100644 --- a/docs/product/wiki/log.md +++ b/docs/product/wiki/log.md @@ -1,5 +1,19 @@ # Compile Log +## 2026-06-10 + +按 `docs/product/raw/` 作为权威来源同步 Product LLM Wiki 的产品文档同步行为: + +- 更新 [产品文档同步 workflow 摘要](summaries/product-docs-sync-workflow.md) 与 [产品文档同步 workflow](concepts/product-docs-sync-workflow.md):记录产品文档同步 PR body/comment 的长度保护、历史 ledger 摘要和超长字段截断规则。 + +## 待确认 / 开放问题 + +- 待确认:raw source 未详细描述 spec agent 的全部输出文件契约,只说明外层 workflow 创建或更新 spec PR。 + +## 冲突 + +- 未发现 raw source 之间的明确冲突。 + ## 2026-06-09 按 `docs/product/raw/` 作为权威来源复核 Product LLM Wiki,并校准近期 raw source 中已经确认的新规则: diff --git a/docs/product/wiki/summaries/product-docs-sync-workflow.md b/docs/product/wiki/summaries/product-docs-sync-workflow.md index 65bb14be..983eaf5d 100644 --- a/docs/product/wiki/summaries/product-docs-sync-workflow.md +++ b/docs/product/wiki/summaries/product-docs-sync-workflow.md @@ -45,6 +45,7 @@ Source: [docs/product/raw/product-docs-sync-workflow.md](../../raw/product-docs- - 普通同步 PR title 是 `Update product docs`;最新决策为 `uncertain` 时使用 draft PR 和 `Draft: Update product docs` title。 - `not-needed` 不修改权威 markdown 文档,但 ledger 更新仍会创建或更新只记录同步决策的 PR。 - 每次创建或更新同步 PR 后,workflow 都会追加一条 conversation comment,记录本次 run 的 source PR、决策、原因、受影响文档和 patch summary;旧 bot comment 不会被编辑替代。 +- PR body 和 comment 会保守控制长度;历史 ledger 决策只展示最近一批,过长 reason 或 patch summary 会被截断,完整上下文保留在 workflow artifacts 和 ledger 中。 - 长期产品文档只有在同步 PR review 并合并后才成为权威产品知识。 ## 支持的概念