From fd330d32ef17ecbbcb90a60239255fbd26ccc2a9 Mon Sep 17 00:00:00 2001 From: huangruiteng <14976749+huangruiteng@users.noreply.github.com> Date: Mon, 21 Sep 2026 03:12:35 +0800 Subject: [PATCH 1/2] fix(periodic-report): classify the coverage caveat as supporting coverage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A calendar report appends a bounded coverage caveat so the reader knows the window reads only this agent's still-readable records. It was labelled content_kind=risk, so the project-progress mapping itemized it under 风险与阻塞 and, when the window had no other risk, the compiled headline said "当前风险:本期证据覆盖范围". Add a typed coverage kind that belongs to supporting evidence, require supporting visibility for it, and build the caveat through one helper the report path calls. Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com> --- .../capabilities/periodic_report/adapters.py | 3 +- .../periodic_report/pending_intent.py | 31 +++++++++++++++---- .../periodic_report/project_progress.py | 3 +- 3 files changed, 29 insertions(+), 8 deletions(-) diff --git a/loopx/capabilities/periodic_report/adapters.py b/loopx/capabilities/periodic_report/adapters.py index f2b56189b9..3b68154f29 100644 --- a/loopx/capabilities/periodic_report/adapters.py +++ b/loopx/capabilities/periodic_report/adapters.py @@ -24,6 +24,7 @@ _SINK_ROLES = {"archive", "delivery"} _ITEM_CONTENT_KINDS = { "capability_change", + "coverage", "decision", "delivery_receipt", "next_action", @@ -33,7 +34,7 @@ "runtime", } _ITEM_VISIBILITIES = {"primary", "supporting"} -_SUPPORTING_CONTENT_KINDS = {"delivery_receipt", "runtime"} +_SUPPORTING_CONTENT_KINDS = {"coverage", "delivery_receipt", "runtime"} _HIGHLIGHT_TONES = {"attention", "neutral", "positive"} _LANGUAGE_RE = re.compile(r"^[A-Za-z]{2,8}(?:-[A-Za-z0-9]{1,8})*$") SourceCollector = Callable[[Mapping[str, Any]], Mapping[str, Any]] diff --git a/loopx/capabilities/periodic_report/pending_intent.py b/loopx/capabilities/periodic_report/pending_intent.py index c13f0f01aa..ce3721f657 100644 --- a/loopx/capabilities/periodic_report/pending_intent.py +++ b/loopx/capabilities/periodic_report/pending_intent.py @@ -655,6 +655,30 @@ def _validated_snapshot_timestamp(value: str, *, observed_at: str) -> str: return value +def _cadence_coverage_fact(cadence_window: Mapping[str, Any]) -> dict[str, Any]: + """Build the bounded coverage caveat for one frozen calendar window. + + A calendar report reads only what this agent's records can still show inside + the window, so the caveat describes the report's own coverage. It used to be + classified as a risk, which rendered a bounded coverage limitation to the + reader under the risks section and let it stand in for the period's headline + risk; supporting coverage evidence is the honest classification. + """ + + return { + "fact_id": "calendar_coverage", + "title": "本期证据覆盖范围", + "summary": ( + "仅核对当前可读的本 Agent 任务记录,并按记录时间筛选本期交付;" + "未验证完整历史和其他 Agent。空结果不能证明本期没有进展。" + ), + "status": "unknown", + "content_kind": "coverage", + "visibility": "supporting", + "source_ref": "cadence:" + str(cadence_window["window_id"]), + } + + def _build_editorial_request( *, intent: Mapping[str, Any], @@ -1186,12 +1210,7 @@ def _consume_pending_periodic_report_intent( facts = [fact for fact in facts if fact.get("status") != "done" or ( start < datetime.fromisoformat(str(fact["completed_at"]).replace("Z", "+00:00")) <= end )] - facts.append({ - "fact_id": "calendar_coverage", "title": "本期证据覆盖范围", - "summary": "仅核对当前可读的本 Agent 任务记录,并按记录时间筛选本期交付;未验证完整历史和其他 Agent。空结果不能证明本期没有进展。", - "status": "unknown", "content_kind": "risk", - "source_ref": "cadence:" + cadence_window["window_id"], - }) + facts.append(_cadence_coverage_fact(cadence_window)) request_path = _editorial_request_path( runtime_root, goal_id, diff --git a/loopx/capabilities/periodic_report/project_progress.py b/loopx/capabilities/periodic_report/project_progress.py index 3189065193..59568f5ca8 100644 --- a/loopx/capabilities/periodic_report/project_progress.py +++ b/loopx/capabilities/periodic_report/project_progress.py @@ -26,6 +26,7 @@ "next_action": ("next_actions", 40), "runtime": ("supporting_evidence", 50), "delivery_receipt": ("supporting_evidence", 50), + "coverage": ("supporting_evidence", 50), } _SECTION_TITLES = { "en": { @@ -44,7 +45,7 @@ }, } _LANGUAGE_RE = re.compile(r"^[A-Za-z]{2,8}(?:-[A-Za-z0-9]{1,8})*$") -_SUPPORTING_CONTENT_KINDS = {"runtime", "delivery_receipt"} +_SUPPORTING_CONTENT_KINDS = {"coverage", "runtime", "delivery_receipt"} _MAX_PRIMARY_ITEMS = 8 _MAX_SUPPORTING_ITEMS = 16 From 3ce392c841ba0b669ea178cee616404db23a3541 Mon Sep 17 00:00:00 2001 From: huangruiteng <14976749+huangruiteng@users.noreply.github.com> Date: Mon, 21 Sep 2026 03:13:02 +0800 Subject: [PATCH 2/2] test(periodic-report): pin the coverage caveat to supporting evidence MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Assert the caveat's classification and supporting visibility, that the adapter routes it to supporting evidence while a genuine risk still reaches 风险与阻塞, that the rendered report keeps it out of the risks block, and that a primary coverage item is refused by the material contract. Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com> --- .../test_periodic_report_coverage_fact.py | 117 ++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100644 tests/capabilities/test_periodic_report_coverage_fact.py diff --git a/tests/capabilities/test_periodic_report_coverage_fact.py b/tests/capabilities/test_periodic_report_coverage_fact.py new file mode 100644 index 0000000000..2749454a4d --- /dev/null +++ b/tests/capabilities/test_periodic_report_coverage_fact.py @@ -0,0 +1,117 @@ +"""A bounded coverage caveat is supporting evidence, not a project risk.""" + +from __future__ import annotations + +import pytest + +from loopx.capabilities.periodic_report import ( + build_periodic_report_document, + build_project_progress_periodic_report_source, +) +from loopx.capabilities.periodic_report.pending_intent import _cadence_coverage_fact +from loopx.presentation.renderers.periodic_report_markdown import ( + render_periodic_report_markdown, +) + +GOAL_ID = "coverage-fact-fixture" +OBSERVED_AT = "2026-09-11T10:00:00Z" +CADENCE_WINDOW = { + "window_id": "cadence-2026-09-11", + "start_at": "2026-09-04T10:00:00Z", + "due_at": OBSERVED_AT, +} +COVERAGE_TITLE = "本期证据覆盖范围" +RISK_TITLE = "Owner gate blocks the next merge" + + +def _coverage_item(**overrides: object) -> dict[str, object]: + item: dict[str, object] = { + "item_id": "calendar_coverage", + "title": COVERAGE_TITLE, + "summary": "仅核对当前可读的本 Agent 任务记录;空结果不能证明本期没有进展。", + "content_kind": "coverage", + "visibility": "supporting", + "value_rank": 90, + "source_ref": "cadence:cadence-2026-09-11", + } + item.update(overrides) + return item + + +def _risk_item() -> dict[str, object]: + return { + "item_id": "risk_owner_gate", + "title": RISK_TITLE, + "summary": "The maintainer decision is still outstanding.", + "content_kind": "risk", + "value_rank": 30, + "source_ref": "todo:todo_owner_gate", + } + + +def _projection(items: list[dict[str, object]]) -> dict[str, object]: + return { + "schema_version": "periodic_report_project_progress_projection_v0", + "goal_id": GOAL_ID, + "observed_at": OBSERVED_AT, + "language": "zh-CN", + "items": items, + } + + +def test_cadence_coverage_fact_is_supporting_coverage_not_a_risk() -> None: + fact = _cadence_coverage_fact(CADENCE_WINDOW) + + assert fact["fact_id"] == "calendar_coverage" + assert fact["content_kind"] == "coverage" + assert fact["visibility"] == "supporting" + assert fact["status"] == "unknown" + assert fact["source_ref"] == "cadence:cadence-2026-09-11" + + +def test_coverage_item_leaves_the_risks_section_and_keeps_a_real_risk() -> None: + source = build_project_progress_periodic_report_source( + _projection([_risk_item(), _coverage_item()]) + ) + sections = {section["section_id"]: section for section in source["sections"]} + + coverage_items = [ + item + for item in sections["supporting_evidence"]["items"] + if item["item_id"] == "calendar_coverage" + ] + risk_items = [item for item in sections["risks"]["items"]] + + assert [item["content_kind"] for item in coverage_items] == ["coverage"] + assert [item["visibility"] for item in coverage_items] == ["supporting"] + assert [item["title"] for item in risk_items] == [RISK_TITLE] + assert [item["content_kind"] for item in risk_items] == ["risk"] + + +def test_rendered_report_keeps_the_caveat_out_of_the_risks_section() -> None: + source = build_project_progress_periodic_report_source( + _projection([_risk_item(), _coverage_item()]) + ) + document = build_periodic_report_document( + title="项目周报", + generated_at=OBSERVED_AT, + period_window={"start_at": "2026-09-04T10:00:00Z", "end_at": OBSERVED_AT}, + profile={"profile_id": "weekly_progress", "profile_version": "v1"}, + sources=[source], + editorial={"language": "zh-CN"}, + ) + content = render_periodic_report_markdown(document)["content"] + + risks_at = content.index("风险与阻塞") + supporting_at = content.index("支撑证据") + coverage_at = content.index(COVERAGE_TITLE) + assert risks_at < supporting_at < coverage_at + assert RISK_TITLE in content[risks_at:supporting_at] + assert COVERAGE_TITLE not in content[risks_at:supporting_at] + + +def test_primary_coverage_item_is_rejected_by_the_material_contract() -> None: + with pytest.raises(ValueError, match="visibility must be supporting for coverage"): + build_project_progress_periodic_report_source( + _projection([_coverage_item(visibility="primary")]) + )