Skip to content
Merged
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
51 changes: 51 additions & 0 deletions .github/aicodingflow-tests/test_product_docs_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
50 changes: 41 additions & 9 deletions .github/scripts/write_product_docs_sync_pr_body.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]:
Expand Down Expand Up @@ -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
Expand All @@ -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:",
Expand All @@ -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:
Expand All @@ -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:",
Expand All @@ -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:
Expand Down
6 changes: 5 additions & 1 deletion docs/product/raw/product-docs-sync-workflow.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
1 change: 1 addition & 0 deletions docs/product/wiki/concepts/product-docs-sync-workflow.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
14 changes: 14 additions & 0 deletions docs/product/wiki/log.md
Original file line number Diff line number Diff line change
@@ -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 中已经确认的新规则:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 并合并后才成为权威产品知识。

## 支持的概念
Expand Down