#844 only caught epic-shaped child-only records. Live allocation still selected product vision (#652), phased roadmap (#653), and umbrella (#655) as implement targets. Extend pre-rank semantic classification with body markers and container labels for those coordination records, keep title- only and incidental mentions eligible, and add a live-equivalent canary. Closes #854
379 lines
13 KiB
Python
379 lines
13 KiB
Python
"""Allocator semantic container exclusion for vision/roadmap/umbrella (#854).
|
|
|
|
#844 excluded epic / child-only containers (live #631) but product-vision
|
|
(#652), phased-roadmap (#653), and umbrella (#655) coordination records still
|
|
ranked as implementable work. This module is the live-equivalent canary:
|
|
|
|
* #631 / #652 / #653 / #655-shaped records are all excluded in one inventory.
|
|
* Independently executable children remain eligible and can be selected.
|
|
* Ordinary issues that merely mention vision / roadmap / umbrella stay eligible.
|
|
* Excluded containers never receive assignments or workflow leases.
|
|
* Structured skip reason ``epic_or_child_only_container`` is reported.
|
|
* Candidate-set fingerprint remains stable after exclusions.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import tempfile
|
|
import unittest
|
|
|
|
from allocator_service import (
|
|
OUTCOME_ASSIGNED,
|
|
OUTCOME_PREVIEW,
|
|
SKIP_EPIC_OR_CHILD_ONLY_CONTAINER,
|
|
WorkCandidate,
|
|
allocate_next_work,
|
|
candidate_set_fingerprint,
|
|
classify_epic_or_child_only_container,
|
|
)
|
|
from control_plane_db import ControlPlaneDB
|
|
|
|
REMOTE = "prgs"
|
|
ORG = "Scaled-Tech-Consulting"
|
|
REPO = "Gitea-Tools"
|
|
|
|
# Minimal bodies mirroring live coordination records (not full issue text).
|
|
_EPIC_631_BODY = """
|
|
## Scope (umbrella)
|
|
|
|
This epic owns the **product roadmap and linkage** for the Web Console.
|
|
Implementation is delivered via child issues only.
|
|
|
|
## Explicit non-goals
|
|
|
|
* Do not implement product features in this epic issue itself.
|
|
* No product feature implementation is claimed complete solely on this epic.
|
|
"""
|
|
|
|
_VISION_652_BODY = """
|
|
## Canonical product vision — enduring source of truth
|
|
|
|
**This issue is the enduring source of truth for the MCP Control Plane Web Console product vision.**
|
|
|
|
## Implementation linkage
|
|
|
|
* **Do not implement features on this issue.**
|
|
* Sequencing: roadmap issue + #631 children.
|
|
|
|
## Canonical issue state
|
|
|
|
```text
|
|
STATE: vision-active
|
|
WHO_IS_NEXT: controller (triage/ordering) / author (implementation of linked children only)
|
|
```
|
|
"""
|
|
|
|
_ROADMAP_653_BODY = """
|
|
## Purpose
|
|
|
|
This issue is the **phased delivery roadmap and epic sequencing** for the MCP Control Plane Web Console.
|
|
|
|
## Non-goals
|
|
|
|
* Implementing features on this roadmap issue.
|
|
* Deleting vision items by omitting them from phases without #652 change log.
|
|
|
|
## Canonical issue state
|
|
|
|
```text
|
|
STATE: roadmap-active
|
|
WHO_IS_NEXT: author
|
|
```
|
|
"""
|
|
|
|
_UMBRELLA_655_BODY = """
|
|
## Scope (umbrella)
|
|
|
|
This issue owns the **canonical restart-governance program**. Implementation is via linked children only.
|
|
|
|
## Acceptance criteria (umbrella)
|
|
|
|
6. No product feature claimed complete on this issue alone.
|
|
"""
|
|
|
|
_CHILD_BODY = """
|
|
## Problem
|
|
|
|
Operators need a workflow-event timeline model for Phase 1.
|
|
|
|
## Acceptance criteria
|
|
|
|
- [ ] Timeline model API exists
|
|
"""
|
|
|
|
|
|
def _issue(
|
|
number: int,
|
|
*,
|
|
title: str = "",
|
|
body: str = "",
|
|
labels: tuple[str, ...] = ("status:ready", "type:feature"),
|
|
priority: int = 20,
|
|
) -> WorkCandidate:
|
|
return WorkCandidate(
|
|
kind="issue",
|
|
number=number,
|
|
state="open",
|
|
labels=labels,
|
|
title=title or f"issue {number}",
|
|
body=body,
|
|
priority=priority,
|
|
)
|
|
|
|
|
|
def _live_shaped_containers() -> list[WorkCandidate]:
|
|
return [
|
|
_issue(
|
|
631,
|
|
title="Epic: MCP Control Plane Web Console",
|
|
body=_EPIC_631_BODY,
|
|
),
|
|
_issue(
|
|
652,
|
|
title="Product vision: MCP Control Plane Web Console (canonical)",
|
|
body=_VISION_652_BODY,
|
|
),
|
|
_issue(
|
|
653,
|
|
title="Roadmap: MCP Control Plane Web Console (phased delivery)",
|
|
body=_ROADMAP_653_BODY,
|
|
),
|
|
_issue(
|
|
655,
|
|
title="Umbrella: Governed MCP restart coordination and zero-disruption recovery",
|
|
body=_UMBRELLA_655_BODY,
|
|
),
|
|
]
|
|
|
|
|
|
class ClassifySemanticContainersTest(unittest.TestCase):
|
|
def test_652_vision_is_container(self) -> None:
|
|
c = _issue(
|
|
652,
|
|
title="Product vision: MCP Control Plane Web Console (canonical)",
|
|
body=_VISION_652_BODY,
|
|
)
|
|
is_c, detail = classify_epic_or_child_only_container(c)
|
|
self.assertTrue(is_c)
|
|
self.assertIsNotNone(detail)
|
|
self.assertIn("body_marker", detail or "")
|
|
|
|
def test_653_roadmap_is_container(self) -> None:
|
|
c = _issue(
|
|
653,
|
|
title="Roadmap: MCP Control Plane Web Console (phased delivery)",
|
|
body=_ROADMAP_653_BODY,
|
|
)
|
|
is_c, detail = classify_epic_or_child_only_container(c)
|
|
self.assertTrue(is_c)
|
|
self.assertIn("body_marker", detail or "")
|
|
|
|
def test_655_umbrella_is_container(self) -> None:
|
|
c = _issue(
|
|
655,
|
|
title="Umbrella: Governed MCP restart coordination and zero-disruption recovery",
|
|
body=_UMBRELLA_655_BODY,
|
|
)
|
|
is_c, detail = classify_epic_or_child_only_container(c)
|
|
self.assertTrue(is_c)
|
|
self.assertIn("body_marker", detail or "")
|
|
|
|
def test_631_still_container_after_854(self) -> None:
|
|
c = _issue(
|
|
631,
|
|
title="Epic: MCP Control Plane Web Console",
|
|
body=_EPIC_631_BODY,
|
|
)
|
|
is_c, detail = classify_epic_or_child_only_container(c)
|
|
self.assertTrue(is_c)
|
|
self.assertIn("body_marker", detail or "")
|
|
|
|
def test_incidental_vision_roadmap_umbrella_words_not_container(self) -> None:
|
|
cases = (
|
|
(
|
|
"Document vision handoff conventions",
|
|
"Update docs so implementable issues that mention a vision "
|
|
"remain independently executable.",
|
|
),
|
|
(
|
|
"Clarify roadmap sequencing notes",
|
|
"Write a short note about how the roadmap issue relates to children.",
|
|
),
|
|
(
|
|
"Umbrella recovery checklist for authors",
|
|
"Authors should still implement the concrete recovery fix here.",
|
|
),
|
|
(
|
|
"Product vision wording in the help text",
|
|
"Fix a typo in the operator-facing help string that says product vision.",
|
|
),
|
|
)
|
|
for title, body in cases:
|
|
with self.subTest(title=title):
|
|
c = _issue(900, title=title, body=body)
|
|
is_c, detail = classify_epic_or_child_only_container(c)
|
|
self.assertFalse(is_c)
|
|
self.assertIsNone(detail)
|
|
|
|
def test_title_prefix_alone_not_container(self) -> None:
|
|
for title in (
|
|
"Epic: something mentioned only in title",
|
|
"Roadmap: title only without body scope",
|
|
"Product vision: title only without body scope",
|
|
"Umbrella: title only without body scope",
|
|
):
|
|
with self.subTest(title=title):
|
|
c = _issue(
|
|
901,
|
|
title=title,
|
|
body="Implement a concrete fix for the allocator skip list.",
|
|
)
|
|
is_c, detail = classify_epic_or_child_only_container(c)
|
|
self.assertFalse(is_c)
|
|
self.assertIsNone(detail)
|
|
|
|
def test_roadmap_label_alone_is_container(self) -> None:
|
|
c = _issue(
|
|
902,
|
|
title="Console delivery sequencing",
|
|
body="Track phased delivery only.",
|
|
labels=("status:ready", "type:roadmap"),
|
|
)
|
|
is_c, detail = classify_epic_or_child_only_container(c)
|
|
self.assertTrue(is_c)
|
|
self.assertIn("type:roadmap", detail or "")
|
|
|
|
def test_child_referencing_parent_policy_stays_eligible(self) -> None:
|
|
"""Children may quote parent policy without becoming containers."""
|
|
c = _issue(
|
|
637,
|
|
title="Web Console: Workflow-event timeline model (Phase 1)",
|
|
body=(
|
|
_CHILD_BODY
|
|
+ "\n\nParent #652 says do not implement on the vision issue; "
|
|
"this child is the implementable unit."
|
|
),
|
|
)
|
|
is_c, _ = classify_epic_or_child_only_container(c)
|
|
self.assertFalse(is_c)
|
|
|
|
|
|
class AllocateSemanticContainerExclusionTest(unittest.TestCase):
|
|
def setUp(self) -> None:
|
|
self._tmp = tempfile.TemporaryDirectory()
|
|
self.addCleanup(self._tmp.cleanup)
|
|
self.db = ControlPlaneDB(os.path.join(self._tmp.name, "cp.sqlite3"))
|
|
|
|
def _alloc(self, candidates, **kwargs):
|
|
defaults = dict(
|
|
session_id="sess-854",
|
|
role="author",
|
|
remote=REMOTE,
|
|
org=ORG,
|
|
repo=REPO,
|
|
profile_name="prgs-author",
|
|
username="jcwalker3",
|
|
claims={},
|
|
apply=False,
|
|
)
|
|
defaults.update(kwargs)
|
|
return allocate_next_work(self.db, candidates=candidates, **defaults)
|
|
|
|
def test_live_equivalent_canary_excludes_all_containers_selects_child(self) -> None:
|
|
containers = _live_shaped_containers()
|
|
child = _issue(
|
|
637,
|
|
title="Web Console: Workflow-event timeline model (Phase 1)",
|
|
body=_CHILD_BODY,
|
|
)
|
|
inventory = containers + [child]
|
|
res = self._alloc(inventory, apply=False)
|
|
self.assertTrue(res["success"], res)
|
|
self.assertEqual(res["outcome"], OUTCOME_PREVIEW)
|
|
self.assertEqual(res["selected"]["number"], 637)
|
|
|
|
skipped = {s["number"]: s for s in res["skipped"]}
|
|
for number in (631, 652, 653, 655):
|
|
self.assertIn(number, skipped, res["skipped"])
|
|
self.assertEqual(
|
|
skipped[number]["reason_code"],
|
|
SKIP_EPIC_OR_CHILD_ONLY_CONTAINER,
|
|
)
|
|
self.assertIn(
|
|
SKIP_EPIC_OR_CHILD_ONLY_CONTAINER, skipped[number]["reason"]
|
|
)
|
|
|
|
def test_containers_cannot_receive_assignment_or_lease(self) -> None:
|
|
containers = _live_shaped_containers()
|
|
res = self._alloc(containers, apply=True)
|
|
self.assertTrue(res["success"], res)
|
|
self.assertNotEqual(res["outcome"], OUTCOME_ASSIGNED)
|
|
self.assertIsNone(res.get("assignment"))
|
|
self.assertIsNone(res.get("selected"))
|
|
skipped = {s["number"]: s for s in res["skipped"]}
|
|
for number in (631, 652, 653, 655):
|
|
self.assertEqual(
|
|
skipped[number]["reason_code"],
|
|
SKIP_EPIC_OR_CHILD_ONLY_CONTAINER,
|
|
)
|
|
|
|
leases = []
|
|
if hasattr(self.db, "list_active_leases"):
|
|
leases = self.db.list_active_leases(
|
|
remote=REMOTE, org=ORG, repo=REPO
|
|
)
|
|
if not leases and hasattr(self.db, "list_leases"):
|
|
leases = self.db.list_leases(remote=REMOTE, org=ORG, repo=REPO)
|
|
for lease in leases or []:
|
|
work_number = (
|
|
lease.get("work_number") if isinstance(lease, dict) else None
|
|
)
|
|
self.assertNotIn(work_number, {631, 652, 653, 655})
|
|
|
|
def test_apply_selects_child_not_container(self) -> None:
|
|
containers = _live_shaped_containers()
|
|
child = _issue(
|
|
637,
|
|
title="Web Console: Workflow-event timeline model (Phase 1)",
|
|
body=_CHILD_BODY,
|
|
)
|
|
res = self._alloc(containers + [child], apply=True)
|
|
self.assertTrue(res["success"], res)
|
|
self.assertEqual(res["outcome"], OUTCOME_ASSIGNED)
|
|
self.assertEqual(res["selected"]["number"], 637)
|
|
self.assertEqual(res["assignment"]["work_number"], 637)
|
|
|
|
def test_fingerprint_stable_with_containers_present(self) -> None:
|
|
containers = _live_shaped_containers()
|
|
child = _issue(
|
|
637,
|
|
title="Web Console: Workflow-event timeline model (Phase 1)",
|
|
body=_CHILD_BODY,
|
|
)
|
|
inventory = containers + [child]
|
|
fp_before = candidate_set_fingerprint(inventory)
|
|
res = self._alloc(inventory, apply=False)
|
|
self.assertTrue(res["success"], res)
|
|
self.assertEqual(res["selected"]["number"], 637)
|
|
# Allocator reports the same CAS fingerprint for the full candidate set.
|
|
reported = res.get("candidate_set_fingerprint")
|
|
self.assertEqual(reported, fp_before)
|
|
# Re-fingerprint of the same inventory is byte-stable.
|
|
self.assertEqual(candidate_set_fingerprint(inventory), fp_before)
|
|
|
|
def test_incidental_mentions_remain_eligible(self) -> None:
|
|
ordinary = _issue(
|
|
700,
|
|
title="Document roadmap handoff conventions",
|
|
body="Write runbook text about vision vs roadmap vs child issues.",
|
|
)
|
|
res = self._alloc([ordinary], apply=False)
|
|
self.assertTrue(res["success"], res)
|
|
self.assertEqual(res["selected"]["number"], 700)
|
|
self.assertEqual(res["skipped"], [])
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|