Skip to content

Commit c3ca686

Browse files
authored
feat(periodic-report): add bounded segment milestone trigger (#3527)
* feat(periodic-report): add bounded segment milestones Signed-off-by: cmyk-labs <niangxizhuang01@gmail.com> * test(periodic-report): cover bounded segment triggers Signed-off-by: cmyk-labs <niangxizhuang01@gmail.com> * docs(periodic-report): document segment milestone contract Signed-off-by: cmyk-labs <niangxizhuang01@gmail.com> --------- Signed-off-by: cmyk-labs <niangxizhuang01@gmail.com>
1 parent e7dbfd1 commit c3ca686

7 files changed

Lines changed: 168 additions & 0 deletions

File tree

‎docs/reference/protocols/periodic-report-v0.md‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,8 @@ candidate facts. The built-in kinds are:
159159
and a successor is established or the goal is terminal;
160160
- `primary_goal_outcome`: the primary delivery outcome is validated and has a
161161
durable writeback;
162+
- `bounded_segment_milestone`: a named, bounded work segment completed or
163+
entered replan and was durably written back, even when open todos remain;
162164
- `material_decision`: an approved, rejected, or cancelled decision changed
163165
the execution route and was durably recorded;
164166
- `material_blocker`: a new or escalated P0 blocker stops the primary path;
@@ -179,6 +181,16 @@ the selected and coalesced ids, every suppression reason, cooldown state, and
179181
the report kind (`cadence_digest`, `milestone_update`, `exception_update`, or
180182
`manual_update`).
181183

184+
`bounded_segment_milestone` accepts only `segment_ref`, `transition`,
185+
`delivered_count`, `remaining_todo_count`, and `durable_writeback` facts.
186+
`segment_ref` and `transition` are required. Counts are non-negative and
187+
default to zero when omitted. A `segment_completed` or `replan_entered`
188+
transition is material only after `durable_writeback=true`;
189+
`vision_checkpoint` remains non-material. Remaining todos do not suppress an
190+
otherwise material segment milestone. The trigger has priority 40, between
191+
`material_decision` and `cadence_due`, maps to `milestone_update`, and obeys
192+
the normal profile cooldown.
193+
182194
An eligible decision may be embedded as `trigger_receipt` in a
183195
`periodic_report_run_request_v0`. Its `report_key` and `report_kind` then
184196
participate in run identity, so a milestone update and a scheduled digest over

‎examples/fixtures/periodic-report-product-profiles.public.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
"trigger_policy": {
1515
"enabled_kinds": [
1616
"manual",
17+
"bounded_segment_milestone",
1718
"cadence_due",
1819
"primary_goal_outcome",
1920
"vision_closed",

‎loopx/capabilities/periodic_report/core.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
"milestone_update",
2525
}
2626
_REPORTABLE_TRIGGER_KINDS = {
27+
"bounded_segment_milestone",
2728
"cadence_due",
2829
"manual",
2930
"material_blocker",

‎loopx/capabilities/periodic_report/presets.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
"trigger_policy": {
3131
"enabled_kinds": [
3232
"manual",
33+
"bounded_segment_milestone",
3334
"cadence_due",
3435
"primary_goal_outcome",
3536
"vision_closed",

‎loopx/capabilities/periodic_report/triggers.py‎

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
TRIGGER_DECISION_SCHEMA = "periodic_report_trigger_decision_v0"
2323

2424
_REPORTABLE_KINDS = {
25+
"bounded_segment_milestone",
2526
"cadence_due",
2627
"manual",
2728
"material_blocker",
@@ -44,9 +45,11 @@
4445
"material_blocker": 70,
4546
"material_recovery": 60,
4647
"material_decision": 50,
48+
"bounded_segment_milestone": 40,
4749
"cadence_due": 10,
4850
}
4951
_REPORT_KIND = {
52+
"bounded_segment_milestone": "milestone_update",
5053
"cadence_due": "cadence_digest",
5154
"manual": "manual_update",
5255
"material_blocker": "exception_update",
@@ -147,6 +150,39 @@ def _materiality(kind: str, facts: Mapping[str, Any], label: str) -> tuple[bool,
147150
_boolean(facts.get("due"), f"{label}.due"),
148151
"cadence_due" if facts.get("due") is True else "cadence_not_due",
149152
)
153+
if kind == "bounded_segment_milestone":
154+
_reject_unknown_facts(
155+
facts,
156+
allowed={
157+
"delivered_count",
158+
"durable_writeback",
159+
"remaining_todo_count",
160+
"segment_ref",
161+
"transition",
162+
},
163+
label=label,
164+
)
165+
_token(facts.get("segment_ref"), f"{label}.segment_ref")
166+
transition = _token(facts.get("transition"), f"{label}.transition")
167+
if transition not in {
168+
"replan_entered",
169+
"segment_completed",
170+
"vision_checkpoint",
171+
}:
172+
raise ValueError(f"{label}.transition is invalid")
173+
_integer(facts.get("delivered_count", 0), f"{label}.delivered_count")
174+
_integer(
175+
facts.get("remaining_todo_count", 0),
176+
f"{label}.remaining_todo_count",
177+
)
178+
durable_writeback = _boolean(
179+
facts.get("durable_writeback"), f"{label}.durable_writeback"
180+
)
181+
if transition not in {"replan_entered", "segment_completed"}:
182+
return False, "segment_checkpoint_only"
183+
if not durable_writeback:
184+
return False, "segment_writeback_pending"
185+
return True, "bounded_segment_advanced"
150186
if kind == "vision_closed":
151187
_reject_unknown_facts(
152188
facts,

‎tests/capabilities/test_periodic_report_profile.py‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,9 @@ def test_weekly_preset_is_portable_domain_neutral_and_aliasable() -> None:
116116
assert {
117117
item["adapter_id"] for item in activation["profile"]["renderer_bindings"]
118118
} == {"markdown_v0", "html_artifact_v0"}
119+
assert "bounded_segment_milestone" in activation["profile"]["trigger_policy"][
120+
"enabled_kinds"
121+
]
119122
serialized = json.dumps(activation, ensure_ascii=False).lower()
120123
assert "issue_fix" not in serialized and "pull_request" not in serialized
121124

‎tests/capabilities/test_periodic_report_triggers.py‎

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ def _trigger_request(*candidates: dict[str, object]) -> dict[str, object]:
4040
},
4141
"trigger_policy": {
4242
"enabled_kinds": [
43+
"bounded_segment_milestone",
4344
"cadence_due",
4445
"manual",
4546
"material_blocker",
@@ -242,6 +243,119 @@ def test_vision_requires_validated_closure_and_continuation() -> None:
242243
assert payload["report_kind"] == "milestone_update"
243244

244245

246+
@pytest.mark.parametrize("transition", ["segment_completed", "replan_entered"])
247+
def test_bounded_segment_with_remaining_todos_triggers_milestone_update(
248+
transition: str,
249+
) -> None:
250+
request = _trigger_request(
251+
_candidate(
252+
"cadence_due",
253+
source_ref="scheduler:weekly-window/2026-w29",
254+
evidence_digest="sha256:weekly-due",
255+
facts={"due": True},
256+
),
257+
_candidate(
258+
"bounded_segment_milestone",
259+
source_ref="segment:2026-w29",
260+
evidence_digest=f"sha256:{transition}",
261+
facts={
262+
"segment_ref": "week-2026-w29",
263+
"transition": transition,
264+
"delivered_count": 4,
265+
"remaining_todo_count": 12,
266+
"durable_writeback": True,
267+
},
268+
),
269+
)
270+
271+
payload = build_periodic_report_trigger_decision(request)
272+
run = build_periodic_report_run(_run_request(payload))
273+
274+
assert payload["eligible"] is True
275+
assert payload["selected_trigger_kind"] == "bounded_segment_milestone"
276+
assert payload["report_kind"] == "milestone_update"
277+
assert len(payload["coalesced_trigger_ids"]) == 2
278+
assert run["trigger_receipt"]["report_kind"] == "milestone_update"
279+
280+
281+
@pytest.mark.parametrize(
282+
("transition", "durable_writeback", "reason"),
283+
[
284+
("vision_checkpoint", True, "segment_checkpoint_only"),
285+
("replan_entered", False, "segment_writeback_pending"),
286+
],
287+
)
288+
def test_bounded_segment_requires_material_transition_and_writeback(
289+
transition: str,
290+
durable_writeback: bool,
291+
reason: str,
292+
) -> None:
293+
payload = build_periodic_report_trigger_decision(
294+
_trigger_request(
295+
_candidate(
296+
"bounded_segment_milestone",
297+
source_ref="segment:2026-w29",
298+
evidence_digest=f"sha256:{transition}",
299+
facts={
300+
"segment_ref": "week-2026-w29",
301+
"transition": transition,
302+
"delivered_count": 4,
303+
"remaining_todo_count": 12,
304+
"durable_writeback": durable_writeback,
305+
},
306+
)
307+
)
308+
)
309+
310+
assert payload["eligible"] is False
311+
assert payload["suppressed_triggers"][0]["reason"] == reason
312+
313+
314+
@pytest.mark.parametrize(
315+
"facts, error",
316+
[
317+
(
318+
{
319+
"transition": "segment_completed",
320+
"durable_writeback": True,
321+
},
322+
"segment_ref is required",
323+
),
324+
(
325+
{
326+
"segment_ref": "week-2026-w29",
327+
"transition": "unknown_transition",
328+
"durable_writeback": True,
329+
},
330+
"transition is invalid",
331+
),
332+
(
333+
{
334+
"segment_ref": "week-2026-w29",
335+
"transition": "segment_completed",
336+
"remaining_todo_count": -1,
337+
"durable_writeback": True,
338+
},
339+
"remaining_todo_count must be between",
340+
),
341+
],
342+
)
343+
def test_bounded_segment_facts_are_strictly_validated(
344+
facts: dict[str, object], error: str
345+
) -> None:
346+
with pytest.raises(ValueError, match=error):
347+
build_periodic_report_trigger_decision(
348+
_trigger_request(
349+
_candidate(
350+
"bounded_segment_milestone",
351+
source_ref="segment:2026-w29",
352+
evidence_digest="sha256:invalid-segment",
353+
facts=facts,
354+
)
355+
)
356+
)
357+
358+
245359
def test_urgent_blocker_bypasses_cooldown_and_coalesces_material_decision() -> None:
246360
request = _trigger_request(
247361
_candidate(

0 commit comments

Comments
 (0)