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
8 changes: 8 additions & 0 deletions adapters/vllm_omni/manifest.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -423,3 +423,11 @@ knowledge_lifecycle:
protected_rules: []
calibration_set: kb-calibration # adapters/vllm_omni/kb-calibration/cases/*.json
model_dir: repos/vllm-omni/models

# Unit-test coverage candidates for new public functions (#164). Sources are
# the package; tests live under tests/. Entry points and generated/registry
# files are skipped: their coverage is end-to-end by design.
ut_coverage:
source_roots: [vllm_omni/]
test_globs: ["tests/*", "*/test_*.py", "*/conftest.py"]
exclude: ["*/__init__.py", "vllm_omni/entrypoints/cli/*", "vllm_omni/version.py"]
6 changes: 5 additions & 1 deletion doc/architecture/SPEC/direct_routing.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# direct_routing.py —— 规范

<!-- verified-against: 2026-09-28 -->
<!-- verified-against: 2026-09-29 -->

`LOC ~880 · Direct 模式完整策略包与仓库中立的知识路由 · refactor-status: stable`

Expand Down Expand Up @@ -47,6 +47,10 @@ Direct policy bundle)、`direct_knowledge_routes`、
`evidence_head_sha` 7–40 位十六进制、`existing_feedback_status` 枚举、
`finding_dispositions` 的 anchor/disposition/existing_thread/
head_recheck 约束。
- **未测公开函数候选(#164)**:`direct_review_plan(..., diff="")` 有 diff 时带
`untested_public_api`(`status=ok` + 候选 + 评审规则),只基于 diff 自身的测试
文件——provider 没有 PR head 的 checkout,树内测试搜索由 agent 按 checklist 完成;
无 diff 为 `{"status": "no_diff"}`,adapter 关闭时为 `disabled`。
- 叶子模块:**绝不** import 任何 server 模块
(`test_contract.py::test_direct_routing_does_not_import_a_server_module`)。

Expand Down
4 changes: 4 additions & 0 deletions doc/architecture/SPEC/engine/steps/review.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@ agent、评审结果的有界精炼、评测调优过的 prompt 数据和确定
要做的改动、`evidence` 引用该 `[resolved]` 行);覆盖补充(coverage editor)
的输入也剔除 `[resolved]` 行。此前按关键词从散文里“提拔”残留,会把明确
写着不再追究的行发成 P2(#155)。
- **未测公开函数(#164)**:evidence 带 `untested_public_api`——`ut_coverage.analyze`
在 PR-time worktree 上找出无测试引用的新增公开函数,由 lens 判断间接覆盖/trivial/
真缺口;评审者把缺测试评论标为 `kind: untested_api`;finalize 之后 `cap_gap_comments`
只对这类评论至多保留 3 条,其余名字追加到 summary,其他发现不受影响。
- **`review_verdict` 是发布的 state 字段,不是散文**:由
`_review_verdict(review_comments, pr_state)` 计算(**与渲染器同一个
helper,绝不第二份校准规则**),随 `review_text`/`review_summary`/
Expand Down
6 changes: 5 additions & 1 deletion doc/architecture/SPEC/sdk.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# sdk/ —— 规范

<!-- verified-against: 2026-09-28 -->
<!-- verified-against: 2026-09-29 -->


`Python SDK v1 · 跨仓库唯一 typed 边界 · refactor-status: ok`
Expand Down Expand Up @@ -133,3 +133,7 @@ Direct 1.1 / Strict 1.3 accept typed `CarriedFinding` inputs (unique IDs, source
`DirectReviewPlan` 新增 `knowledge_snapshot`(激活快照 ID、`packaged` 或 `unverified`,从不暴露开发树路径)
与 `knowledge_tree_sha256`(已验证快照的清单树哈希,否则为空)。同一 `review_context_id` 下的文档读取都来自这份
快照,RB 应随每次 review 记录这两个值。

## 2026-09-29 未测公开函数候选
`DirectReviewRequest` 新增可选 `diff`(冻结 head 的 unified diff),`DirectReviewPlan` 新增
`untested_public_api`(`direct_routing` 的同名块,默认空字典)。两者都是可选增量字段,旧调用方不受影响(#164)。
4 changes: 3 additions & 1 deletion doc/architecture/SPEC/thin_mcp_server.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# thin_mcp_server.py —— 规范

<!-- verified-against: 2026-09-28 -->
<!-- verified-against: 2026-09-29 -->

`LOC ~490 · 默认 MCP:Direct 门面 + Strict 入口 · refactor-status: ok`

Expand All @@ -26,6 +26,8 @@
委托 —— 没有任何东西从那两个模块向上 import 回 server。下面关于
quick-map fail-closed、路由不静默替换、仓库守卫先跑的不变量**仍然为真**,
但其实现体在 `direct_routing.py`(规范见其页)。
- `review` 的可选 `diff` 只对 Direct 生效:原样交给 `direct_review_plan`,
由其生成 `untested_public_api` 候选(#164);仍然零模型。
- **Strict 分支透传快照绑定**:`_strict_review_request` 把
`expected_head_sha`、`repo_path`、`idempotency_key` 一并送进内部请求;
`review()` 的 Strict 路径按 `strict_readiness(repo, repo_path)`(两参,
Expand Down
45 changes: 45 additions & 0 deletions doc/architecture/SPEC/ut_coverage.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# ut_coverage.py —— 规范

<!-- verified-against: 2026-09-29 -->

`LOC ~290 · 新增公开函数的单测覆盖候选(#164) · refactor-status: ok`

## 职责
两段式检查的**确定性一半**:从 PR diff 找出新增的公开函数/方法,再看 diff 中的
测试文件、以及(有 checkout 时)被评审树的测试文件是否按名字引用它们。没有任何
测试引用的名字只是**候选**,从不直接判定为“缺测试”——由评审者(Strict lens 或
Direct agent)逐个判断:经调用方被间接覆盖、trivial,还是真缺口。

## 公开契约
`UTCoverageRules`(`from_manifest(manifest)`:读 adapter 的 `ut_coverage:`
段——`source_roots`、`test_globs`、`exclude`、`enabled`;非 Python 仓库或
`enabled: false` 返回 `None`);`PublicDef`;`CoverageReport`;
`new_public_defs(diff, rules)`;`analyze(diff, rules, repo=None)`;
`render(report)`(Strict evidence 文本,无候选时为空);
`cap_gap_comments(comments, report)`;`REVIEWER_INSTRUCTIONS`;
`MAX_CANDIDATES=15`;`MAX_COMMENTS=3`。

## 不变量
- **只提名,不裁决**:模块不产生评论;评论只来自评审者对候选的判断。
- 新增 = diff 中 `+def` 且同一**限定名**(`类.方法` 或模块函数名)没有在 diff
任何位置被删除(移动、改签名不算新 API);另一个类的同名方法不受影响。跳过:`_` 开头与 dunder 名、嵌套函数、`@overload`/
`@abstractmethod` 桩、测试文件、`exclude` 命中的文件;私有类的方法跳过。
- 方法判定只看同一 hunk 内可见的 `class`(上下文或新增行)或 hunk 头;看不到
父作用域的缩进 def 按模块函数处理。
- 树内搜索用 `git grep -l -w -F`,只计入 `test_globs` 命中的文件,逐名限时;
失败按“无引用”处理(只会多提名,交给评审者判断)。
- 发布上限在评审之后确定性执行,且只作用于评审者**显式分类**为
`kind: untested_api` 的评论:最多保留 3 条,其余的名字进 summary 一行,不丢失。
同一函数上的其他发现(不论锚点、严重度)从不被截断;`kind` 是内部字段,
发布前一律移除。
- 仓库中立:仓库差异全部来自 adapter 的 `ut_coverage:` 段。

## 边界 —— 不属于这里
评审 prompt 与评论发布(`engine/steps/review/`);Direct 策略包
(`direct_routing.py`);运行任何测试或覆盖率工具。

## 依赖(允许)
标准库(`re`、`subprocess`、`fnmatch`、`dataclasses`)。

## 测试
`test/test_ut_coverage.py`。
25 changes: 25 additions & 0 deletions src/infermatrix_copilot/direct_routing.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@
from .adapters import AdapterError, AdapterRegistry, RepoAdapter
from .knowledge_view import KnowledgeView
from .sdk._resources import adapters_root
from .ut_coverage import UTCoverageRules, analyze as _ut_analyze
from .ut_coverage import REVIEWER_INSTRUCTIONS as _UT_INSTRUCTIONS

_ROOT = Path(__file__).resolve().parents[2]
_ADAPTERS = adapters_root()
Expand Down Expand Up @@ -158,6 +160,7 @@ def _validate_routes(data: object, relative: str, view: KnowledgeView) -> dict:
"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.",
"Run subtraction only when the diff adds or expands a helper, class, fallback, compatibility branch, or public behavior; otherwise mark no subtraction signal.",
"When subtraction is triggered, read the mandatory simplification guide and prove consumers, trust boundaries, and lifecycle ownership before calling code dead or over-defensive.",
"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.",
"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.",
"Plan exactly one consolidated final review comment.",
)
Expand Down Expand Up @@ -586,9 +589,14 @@ def direct_review_plan(
body: str = "",
changed_files: list[str] | None = None,
view: KnowledgeView | None = None,
diff: str = "",
) -> dict:
"""Return the complete Direct policy bundle for one frozen review.

``diff`` is optional: with it, the bundle carries the new public
functions no test in the diff names (#164) for the agent to judge; without
it, the checklist asks the agent to find them itself.

This is the canonical provider operation used by both the Python SDK and
the MCP adapter. Keeping the full bundle here prevents downstream hosts
from reconstructing a smaller, divergent protocol out of routing helpers.
Expand Down Expand Up @@ -620,6 +628,7 @@ def direct_review_plan(
),
)
budget_ms = int((time.perf_counter() - budget_started) * 1000)
untested = _direct_untested_public_api(repo, diff)
return {
"mode": "direct",
"knowledge_entry": (
Expand Down Expand Up @@ -648,6 +657,7 @@ def direct_review_plan(
"fallback_entry": _knowledge_path("AGENTS.md", view),
},
"execution_budget": execution_budget,
"untested_public_api": untested,
"first_review_checklist": list(DIRECT_REVIEW_CHECKLIST),
"progress_update": {
**DIRECT_PROGRESS_UPDATE,
Expand Down Expand Up @@ -697,6 +707,21 @@ def direct_review_plan(
}


def _direct_untested_public_api(repo: str, diff: str) -> dict:
"""Candidates from the diff alone: the provider holds no checkout of the
PR head, so the head-tree test search is the agent's (see checklist)."""
if not diff:
return {"status": "no_diff"}
adapter = _adapter_for_repo(repo)
rules = UTCoverageRules.from_manifest(
adapter.manifest if adapter is not None else None)
if rules is None:
return {"status": "disabled"}
report = _ut_analyze(diff, rules)
return {"status": "ok", **report.to_dict(),
"instructions": _UT_INSTRUCTIONS if report.candidates else ""}


def _direct_completion_result(
subtraction_signal: str = "",
subtraction: list[dict[str, str]] | None = None,
Expand Down
27 changes: 25 additions & 2 deletions src/infermatrix_copilot/engine/steps/review/steps.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
from pathlib import Path

from ....review.planner import DEFAULT_STANDARD_LENSES, DEPTHS, plan_review
from ....ut_coverage import UTCoverageRules, analyze as _ut_analyze
from ....ut_coverage import cap_gap_comments, render as _ut_render
from ...step import FailureKind, StepContext, StepResult
from .._common import gh_read_tools as _gh_read_tools
from .._common import repo_path as _repo_path
Expand Down Expand Up @@ -145,6 +147,17 @@ async def _review_diff(ctx: StepContext) -> StepResult:
except OSError:
continue

# #164: new public functions no test names — candidates for the lenses
# to judge (indirect coverage, trivial, or a real gap), never findings
ut_rules = UTCoverageRules.from_manifest(
adapter.manifest if adapter is not None else None)
ut_report = (await asyncio.to_thread(_ut_analyze, str(diff), ut_rules,
_repo_path(ctx))
if ut_rules is not None else None)
if ut_report is not None:
ctx.trace.record("review_ut_candidates", step="agent.review_diff",
**ut_report.to_dict())

common = dict(
step_name="agent.review_diff",
purpose=f"Review PR #{spec.get('pr')} like an engaged maintainer: "
Expand All @@ -158,14 +171,18 @@ async def _review_diff(ctx: StepContext) -> StepResult:
"gate_report": ctx.state.get("gate_report", ""),
"sweep_targets": _sweep_targets(str(diff), language),
"changed_symbol_consumers": await asyncio.to_thread(
_consumer_sweep, _repo_path(ctx), str(diff))},
_consumer_sweep, _repo_path(ctx), str(diff)),
"untested_public_api": (_ut_render(ut_report)
if ut_report is not None else "")},
output_extension={"review_comments":
"list of {file, line, anchor_snippet, severity: "
"blocker|major|minor|nit, comment, evidence, "
"suggestion: optional replacement code for the "
"cited lines — the concrete edit, no prose, "
"disposition: publish|excluded|duplicate|resolved|"
"no_issue — only publish is published}"},
"no_issue — only publish is published, kind: "
"optional, `untested_api` only for a missing unit "
"test on a listed untested-function candidate}"},
extra_tools={**_gh_read_tools(_repo_path(ctx)),
**review_repo_tools(_repo_path(ctx))},
)
Expand Down Expand Up @@ -301,6 +318,12 @@ async def _review_diff(ctx: StepContext) -> StepResult:
# finalized set (#141).
all_candidates = list(output.get("review_comments") or [])
published, withheld = finalize_review_dispositions(output)
# at most three untested-function comments; the rest are named, not lost
published, ut_dropped = cap_gap_comments(published, ut_report)
if ut_dropped:
output["summary"] = (str(output.get("summary") or "").rstrip()
+ "\n\nAlso without a unit test: "
+ ", ".join(f"`{n}`" for n in ut_dropped) + ".").lstrip()
output["review_comments"] = published
output["_withheld_findings"] = withheld
comments = sorted(output.get("review_comments") or [],
Expand Down
2 changes: 2 additions & 0 deletions src/infermatrix_copilot/sdk/v1/direct.py
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,7 @@ def plan(self, request: DirectReviewRequest) -> DirectReviewPlan:
body=request.body,
changed_files=changed_files,
view=view,
diff=request.diff,
)
routes = tuple(
KnowledgeRoute(
Expand Down Expand Up @@ -380,6 +381,7 @@ def plan(self, request: DirectReviewRequest) -> DirectReviewPlan:
carried_findings=request.carried_findings,
knowledge_snapshot=view.public_snapshot,
knowledge_tree_sha256=view.tree_sha256,
untested_public_api=dict(raw.get("untested_public_api") or {}),
)
self._remember_context(review_context_id, expected_head, carried, view)
return plan
Expand Down
6 changes: 6 additions & 0 deletions src/infermatrix_copilot/sdk/v1/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,9 @@ class DirectReviewRequest(_Serializable):
body: str
changed_paths: tuple[ChangedPath, ...]
carried_findings: tuple[CarriedFinding, ...] = ()
# optional unified diff of the frozen head; enables the provider's
# untested-public-function candidates (#164)
diff: str = ""


@dataclass(frozen=True)
Expand All @@ -254,6 +257,9 @@ class DirectReviewPlan(_Serializable):
# review_context_id) was served from: record it with the review
knowledge_snapshot: str = ""
knowledge_tree_sha256: str = ""
# new public functions no test in the request's diff names (#164);
# {"status": "no_diff"} when the request carried no diff
untested_public_api: dict[str, Any] = field(default_factory=dict)

FeedbackStatus = Literal[
"checked", "disabled", "unavailable", "not_applicable"
Expand Down
5 changes: 5 additions & 0 deletions src/infermatrix_copilot/thin_mcp_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,7 @@ def review(
repo_path: str = "",
expected_head_sha: str = "",
idempotency_key: str = "",
diff: str = "",
) -> dict:
"""Begin a Direct or Strict review.

Expand All @@ -289,6 +290,9 @@ def review(
``expected_head_sha`` (Strict only) pins the review to one snapshot: pass
the full 40-hex head you observed, and the run stops as stale rather than
reviewing a different commit if the PR moved in between.
``diff`` (Direct only, optional) is the frozen unified diff; with it the
plan lists the new public functions no test in the diff names
(``untested_public_api``) for you to judge.
``idempotency_key`` (Strict only) makes a retry safe: pass a stable id for
the attempt and a repeated call returns the SAME run — including a
finished one, whose result you can read — instead of starting a second
Expand All @@ -311,6 +315,7 @@ def run() -> dict:
title=title,
body=body,
changed_files=changed_files,
diff=diff,
)

if post:
Expand Down
Loading
Loading