|
28 | 28 | from .adapters import AdapterError, AdapterRegistry, RepoAdapter |
29 | 29 | from .knowledge_view import KnowledgeView |
30 | 30 | from .sdk._resources import adapters_root |
| 31 | +from .ut_coverage import UTCoverageRules, analyze as _ut_analyze |
| 32 | +from .ut_coverage import REVIEWER_INSTRUCTIONS as _UT_INSTRUCTIONS |
31 | 33 |
|
32 | 34 | _ROOT = Path(__file__).resolve().parents[2] |
33 | 35 | _ADAPTERS = adapters_root() |
@@ -158,6 +160,7 @@ def _validate_routes(data: object, relative: str, view: KnowledgeView) -> dict: |
158 | 160 | "Classify every candidate finding as new, duplicate, extends_existing, or resolved_or_outdated. Suppress duplicates; for extensions, point to the existing thread instead of opening a parallel inline comment. Reverify resolved/outdated concerns at the pinned head and suppress them only when fixed. Use disabled only for PR_CONTEXT_MODE=no_discussion evaluation, record unavailable feedback as a validation gap, and use not_applicable only for local/worktree reviews.", |
159 | 161 | "Run subtraction only when the diff adds or expands a helper, class, fallback, compatibility branch, or public behavior; otherwise mark no subtraction signal.", |
160 | 162 | "When subtraction is triggered, read the mandatory simplification guide and prove consumers, trust boundaries, and lifecycle ownership before calling code dead or over-defensive.", |
| 163 | + "When the plan's untested_public_api lists candidates, judge each new public function as indirectly covered, trivial, or a real unit-test gap after reading it and searching the head's tests; when the host passed no diff, list the diff's new public functions and search the tests yourself. File at most three real gaps as minor findings on the def's file:line and name any further ones in one line.", |
161 | 164 | "Report the non-test added-line count the host supplies rather than counting the diff yourself; above the stated budget, ask for a split plan or a concrete exemption on the largest contributing file, and record the reason instead when the PR body already gives one. Size is never a correctness finding and never raises another finding's severity.", |
162 | 165 | "Plan exactly one consolidated final review comment.", |
163 | 166 | ) |
@@ -586,9 +589,14 @@ def direct_review_plan( |
586 | 589 | body: str = "", |
587 | 590 | changed_files: list[str] | None = None, |
588 | 591 | view: KnowledgeView | None = None, |
| 592 | + diff: str = "", |
589 | 593 | ) -> dict: |
590 | 594 | """Return the complete Direct policy bundle for one frozen review. |
591 | 595 |
|
| 596 | + ``diff`` is optional: with it, the bundle carries the new public |
| 597 | + functions no test in the diff names (#164) for the agent to judge; without |
| 598 | + it, the checklist asks the agent to find them itself. |
| 599 | +
|
592 | 600 | This is the canonical provider operation used by both the Python SDK and |
593 | 601 | the MCP adapter. Keeping the full bundle here prevents downstream hosts |
594 | 602 | from reconstructing a smaller, divergent protocol out of routing helpers. |
@@ -620,6 +628,7 @@ def direct_review_plan( |
620 | 628 | ), |
621 | 629 | ) |
622 | 630 | budget_ms = int((time.perf_counter() - budget_started) * 1000) |
| 631 | + untested = _direct_untested_public_api(repo, diff) |
623 | 632 | return { |
624 | 633 | "mode": "direct", |
625 | 634 | "knowledge_entry": ( |
@@ -648,6 +657,7 @@ def direct_review_plan( |
648 | 657 | "fallback_entry": _knowledge_path("AGENTS.md", view), |
649 | 658 | }, |
650 | 659 | "execution_budget": execution_budget, |
| 660 | + "untested_public_api": untested, |
651 | 661 | "first_review_checklist": list(DIRECT_REVIEW_CHECKLIST), |
652 | 662 | "progress_update": { |
653 | 663 | **DIRECT_PROGRESS_UPDATE, |
@@ -697,6 +707,21 @@ def direct_review_plan( |
697 | 707 | } |
698 | 708 |
|
699 | 709 |
|
| 710 | +def _direct_untested_public_api(repo: str, diff: str) -> dict: |
| 711 | + """Candidates from the diff alone: the provider holds no checkout of the |
| 712 | + PR head, so the head-tree test search is the agent's (see checklist).""" |
| 713 | + if not diff: |
| 714 | + return {"status": "no_diff"} |
| 715 | + adapter = _adapter_for_repo(repo) |
| 716 | + rules = UTCoverageRules.from_manifest( |
| 717 | + adapter.manifest if adapter is not None else None) |
| 718 | + if rules is None: |
| 719 | + return {"status": "disabled"} |
| 720 | + report = _ut_analyze(diff, rules) |
| 721 | + return {"status": "ok", **report.to_dict(), |
| 722 | + "instructions": _UT_INSTRUCTIONS if report.candidates else ""} |
| 723 | + |
| 724 | + |
700 | 725 | def _direct_completion_result( |
701 | 726 | subtraction_signal: str = "", |
702 | 727 | subtraction: list[dict[str, str]] | None = None, |
|
0 commit comments