Skip to content

Commit d53ea4b

Browse files
committed
fix(kb): independently review each subscription module card
1 parent c7231cd commit d53ea4b

4 files changed

Lines changed: 127 additions & 10 deletions

File tree

‎doc/architecture/SPEC/kb_service.md‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -432,13 +432,17 @@ x 必须为正。因阈值停下的调用(`stop_reason="max_budget"`)→ `Mo
432432
仓库入口页链接;`components/` 已存在却没有入口页时组直接放在仓库目录下。`_routes.yaml` 只追加(保留开头注释块,
433433
`routes_append_only` 校验不重排、不改名、不删除)。预算用完或超过卡片上限时,已写的卡片照常成 PR,剩下的模块进
434434
`unfinished`;报告里列出仍无路由的模块。测试:`test_kb_init_modules.py`。
435-
显式 `--unlimited-subscription` 使用 `module_prompt_version: 2` 与独立的
435+
显式 `--unlimited-subscription` 使用 `module_prompt_version: 3` 与独立的
436436
`SYSTEM_CARD_IMPL`,从固定 SHA 的共享索引提供带文件哈希、行号与完整性状态的实现片段;
437437
整个卡片输入仍受 40,000 UTF-8 字节限制,按剩余文件分配预算,能放下时提供全文,否则提供
438438
明确的首尾行段,未展示的行为保持未知。读取失败不使用替换字符补造源码;元数据本身超限时
439439
本模块保持未完成,不发起生成。卡片不能从目录名推断开发/运行时职责,也不能在未展示配置
440440
与分支时断言默认副作用;打印的手工操作须与实际执行区分。此模式仅保留每文件前四个签名,
441441
以留出实现片段预算;旧模式仍保留最多四十个签名,提示词和身份不变。
442+
此模式每张候选地图卡再由独立 Codex 按实际引用文件的源码片段评审一次;沿用 prose 的
443+
`faithful / does_not_weaken / non_contradictory` 三项,全部为 yes 才写入。真实结论、维度、
444+
原因、模型与文本哈希保存在阶段记录;拒绝或无法评审只令本卡保持未知,其他组件继续。
445+
未通过的卡片不增加模块路由覆盖,也不计入功能深度认可;后续功能/核心文件目标独立验收。
442446
测试:`test_kb_module_source_evidence.py`。
443447

444448
## 2026-09-30 kb init:阶段 3(deepen)与 shadow 开关

‎src/infermatrix_copilot/kb_service/init_modules.py‎

Lines changed: 45 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
from __future__ import annotations
2727

2828
import copy
29+
import hashlib
2930
import json
3031
import re
3132
from dataclasses import dataclass
@@ -39,7 +40,8 @@
3940
from .init_stages import (
4041
_SLUG, ROUTES_NAME, _fence, _one_line, _page_frontmatter, _slug, _Stage, _title_of, review_route_line,
4142
)
42-
from .init_support import InitError, InitRecord, generate
43+
from .init_support import InitError, InitRecord, classify_verdict, generate, judge
44+
from .models import ModelUnavailable
4345

4446
MAX_CARD_FILES = 200
4547
MAX_CARD_BYTES = 40_000
@@ -88,7 +90,7 @@
8890
"""
8991

9092

91-
def _implementation_payload(payload: dict, index, pin: str) -> dict:
93+
def _implementation_payload(payload: dict, index, pin: str, *, max_bytes: int = MAX_CARD_BYTES) -> dict:
9294
"""Share the card cap fairly; unread or cropped code never becomes whole-file evidence."""
9395
if index.identity.get("pin") != pin:
9496
raise ValueError("module implementation index differs from the fixed source SHA")
@@ -109,7 +111,7 @@ def _implementation_payload(payload: dict, index, pin: str) -> dict:
109111
def size():
110112
return len(_fence(out).encode("utf-8"))
111113

112-
if size() > MAX_CARD_BYTES:
114+
if size() > max_bytes:
113115
raise ValueError("module metadata exceeds the implementation card byte cap; no evidence dispatched")
114116

115117
def ranges(lines, count):
@@ -126,7 +128,7 @@ def ranges(lines, count):
126128

127129
for offset, (item, lines) in enumerate(readable):
128130
before = size()
129-
allocation = (MAX_CARD_BYTES - before) // (len(readable) - offset)
131+
allocation = (max_bytes - before) // (len(readable) - offset)
130132
limit = before + allocation
131133
item.update(status="complete", ranges=ranges(lines, len(lines)))
132134
if size() <= limit:
@@ -360,7 +362,7 @@ class _Modules(_Stage):
360362
STAGE = "modules"
361363

362364
def _input_options(self) -> dict:
363-
return {"module_prompt_version": 2} if self.rt.unlimited_subscription else {}
365+
return {"module_prompt_version": 3} if self.rt.unlimited_subscription else {}
364366

365367
def _build(self, tree: Path) -> InitRecord:
366368
routes_path = f"{self.repo_dir}/{ROUTES_NAME}"
@@ -602,13 +604,50 @@ def items(name: str, allowed: set[str], text_key: str) -> list[tuple[str, str]]:
602604
group = _slug(PurePosixPath(key.rstrip("/")).name or self.lifecycle.repo)
603605
headings = {k: _one_line((data.get("headings") or {}).get(k)).replace("*", "") or v
604606
for k, v in _DEFAULT_HEADINGS.items()}
605-
return _Card(
607+
card = _Card(
606608
module=key, prefixes=[], title=_one_line(data["title"]),
607609
purpose=self._d5_prose(str(data.get("purpose") or "")),
608610
entry_points=items("entry_points", in_module, "what"), key_files=items("key_files", in_module, "what"),
609611
docs=items("docs", doc_paths, "why"),
610612
signals=[s for s in (_one_line(x, 60) for x in data.get("signals") or []) if s][:MAX_ITEMS],
611613
group=group, group_title=_one_line(data.get("group_title")) or group, headings=headings)
614+
if self.rt.unlimited_subscription and not self._review_card(card, listed):
615+
return None
616+
return card
617+
618+
def _review_card(self, card: _Card, listed: list[dict]) -> bool:
619+
"""One independent source review; a rejected card cannot halt other modules."""
620+
from ..knowledge_service.l1 import Block
621+
622+
referenced = {path for path, _ in card.entry_points + card.key_files}
623+
page = f"{self.root}/{card.group}/module-review.md"
624+
text = self._card_text(card)
625+
text_sha = hashlib.sha256(text.encode()).hexdigest()
626+
key = f"module:{card.module}"
627+
payload = {"repository": self.lifecycle.full_name, "module": card.module,
628+
"files": [entry for entry in listed if entry["path"] in referenced],
629+
"doc_files": [path for path, _ in card.docs],
630+
"source_scope": "Only shown implementation ranges support behavior. Directory names do not "
631+
"establish runtime roles; default side effects require shown configuration and branches. "
632+
"Printed/manual instructions do not establish execution; unshown steps remain unknown."}
633+
# Reserve the actual card text and the existing judge envelope/indentation.
634+
allowance = MAX_CARD_BYTES - len(_fence({"card": text}).encode()) - 2048
635+
try:
636+
evidence = _implementation_payload(payload, self.source_index, self.record.pin, max_bytes=allowance)
637+
verdict = judge(self.rt, self.budget, self.lifecycle.init,
638+
Block("prose", page, "", "prose", text_sha), base={}, head={page: text},
639+
evidence=[evidence])
640+
label, dimensions, reasons, model = (classify_verdict(verdict), verdict.dimensions,
641+
verdict.reasons, verdict.model)
642+
except (ValueError, BudgetExhausted, ModelUnavailable) as exc:
643+
label, dimensions, reasons, model = "unjudged", {}, {"source_review": str(exc)}, ""
644+
self.record.verdicts[key] = {"kind": "module_prose", "page": page, "verdict": label, "dimensions": dimensions,
645+
"reasons": reasons, "model": model, "text_sha": text_sha}
646+
if label != "pass":
647+
self.record.unfinished.append(f"module {card.module}: source review {label}; card remains unknown")
648+
self.record.dropped.append({"rule_id": key, "module": card.module,
649+
"why": "source review: " + json.dumps(reasons)})
650+
return label == "pass"
612651

613652
def _card_text(self, card: _Card) -> str:
614653
"""The card page. Sections are bold labels, not headings: the

‎test/test_kb_init_modules.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
import yaml
1313

14-
from infermatrix_copilot.kb_service.init_modules import SYSTEM_CARD, _leading_doc, declaration, routes_append_only
14+
from infermatrix_copilot.kb_service.init_modules import SYSTEM_CARD, SYSTEM_CARD_IMPL, _leading_doc, declaration, routes_append_only
1515
from infermatrix_copilot.kb_service.init_stages import run_stage
1616
from infermatrix_copilot.kb_service.config import RepoLifecycle
1717
from infermatrix_copilot.kb_service.init_support import InitRecord
@@ -44,7 +44,7 @@ def __init__(self, *, card_cost=0.01, group="tooling", **kwargs):
4444
self.group = group
4545

4646
def call_json(self, role, *, system, prompt, validate=None, max_budget_usd=None):
47-
if system != SYSTEM_CARD:
47+
if system not in (SYSTEM_CARD, SYSTEM_CARD_IMPL):
4848
reply = super().call_json(role, system=system, prompt=prompt, validate=validate,
4949
max_budget_usd=max_budget_usd)
5050
if system.startswith("You set up the review knowledge base"):

‎test/test_kb_module_source_evidence.py‎

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
)
1313
from infermatrix_copilot.kb_service.init_stages import _fence, run_stage
1414
from infermatrix_copilot.kb_service.models import ModelRole
15+
from infermatrix_copilot.kb_service.judge_tuning import JUDGE_SYSTEM
1516
from test_kb_init_modules import CardGateway, _modules_lifecycle, _skeleton, _world_with_tools
1617
from test_kb_init_skeleton import _commit, _runtime, world # noqa: F401
1718

@@ -81,7 +82,7 @@ def test_metadata_cap_and_pinned_index_identity_fail_closed():
8182
def test_unlimited_prompt_identity_and_instructions_leave_legacy_unchanged():
8283
assert _Modules._input_options(SimpleNamespace(rt=SimpleNamespace(unlimited_subscription=False))) == {}
8384
assert _Modules._input_options(SimpleNamespace(rt=SimpleNamespace(unlimited_subscription=True))) == {
84-
"module_prompt_version": 2}
85+
"module_prompt_version": 3}
8586
assert "never the full code" in SYSTEM_CARD and "source_pin" not in SYSTEM_CARD
8687
assert "never the full code" not in SYSTEM_CARD_IMPL
8788
assert all(text in SYSTEM_CARD_IMPL for text in
@@ -140,3 +141,76 @@ def call_json(self, role, *, system, prompt, **kwargs):
140141
_modules_lifecycle(), "modules", dry_run=True, unlimited_subscription=True)
141142
assert record.status == "dry_run", record.problems
142143
assert any("key_files 'tools/lint/y.py' is not offered" in note for note in record.notes)
144+
145+
146+
@pytest.mark.parametrize("answer", ["no", "unsure", "unavailable", "subscription_unavailable"])
147+
def test_nonpass_source_review_drops_only_that_card_and_retains_real_unknown(world, answer):
148+
from infermatrix_copilot.kb_service.models import ModelUnavailable
149+
_world_with_tools(world)
150+
_skeleton(world)
151+
152+
class SourceReviewGateway(ImplementationGateway):
153+
judge_subscription_checks = 0
154+
155+
def subscription_billing(self, role):
156+
if role.name == "judge":
157+
self.judge_subscription_checks += 1
158+
if answer == "subscription_unavailable" and self.judge_subscription_checks == 3:
159+
raise ModelUnavailable("scripted subscription recheck failure")
160+
return True
161+
162+
def call_json(self, role, *, system, prompt, **kwargs):
163+
if system == JUDGE_SYSTEM and 'tools/lint/' in prompt:
164+
if answer == "unavailable":
165+
raise ModelUnavailable("scripted independent channel failure")
166+
reply = super().call_json(role, system=system, prompt=prompt, **kwargs)
167+
reply.data["dimensions"]["faithful"] = answer
168+
reply.data["reasons"] = {"faithful": "runtime/conditional claim unsupported by shown source"}
169+
return reply
170+
return super().call_json(role, system=system, prompt=prompt, **kwargs)
171+
172+
gateway = SourceReviewGateway()
173+
record = run_stage(_runtime(world, gateway, generator=ModelRole("generator", "zcode", "GLM-5.3")),
174+
_modules_lifecycle(), "modules", dry_run=True, unlimited_subscription=True)
175+
assert record.status == "dry_run", record.problems
176+
assert "tools/fmt/" in record.coverage["cards"] and "tools/lint/" not in record.coverage["cards"]
177+
assert record.coverage["modules"]["after"] < 1 and "tools/lint/" in record.coverage["unrouted"]
178+
rejected = record.verdicts["module:tools/lint/"]
179+
assert rejected["kind"] == "module_prose" and rejected["verdict"] != "pass"
180+
assert rejected["reasons"] and any("tools/lint/" in x for x in record.unfinished)
181+
assert not record.depth and not record.discovery.get("acceptance")
182+
assert len([call for call in gateway.calls if call["system"] == SYSTEM_CARD_IMPL]) == 2
183+
184+
185+
def test_independent_module_review_is_pinned_focused_bounded_and_three_yes(world):
186+
_world_with_tools(world)
187+
_commit(world["upstream"], {"tools/lint/unused.py": "def unused():\n return private_unused_behavior()\n"},
188+
"unreferenced source")
189+
_skeleton(world)
190+
gateway = ImplementationGateway()
191+
record = run_stage(_runtime(world, gateway, generator=ModelRole("generator", "zcode", "GLM-5.3")),
192+
_modules_lifecycle(), "modules", dry_run=True, unlimited_subscription=True)
193+
assert record.status == "dry_run", record.problems
194+
reviews = [call for call in gateway.calls if call["system"] == JUDGE_SYSTEM]
195+
assert len(reviews) == 2
196+
for call in reviews:
197+
assert call["role"] == "judge" and len(call["prompt"].encode()) <= MAX_CARD_BYTES
198+
payload = json.loads(call["prompt"].split("\n", 1)[1].rsplit("</untrusted_data>", 1)[0])
199+
assert set(payload["dimensions_to_answer"]) == {"faithful", "does_not_weaken", "non_contradictory"}
200+
evidence = payload["evidence"][0]
201+
assert evidence["source_pin"] == record.pin
202+
assert {item["path"] for item in evidence["source_evidence"]} == {evidence["files"][0]["path"]}
203+
assert "private_unused_behavior" not in call["prompt"] or "unused.py" in payload["change"]["after"]
204+
assert "Printed/manual" in evidence["source_scope"]
205+
assert all(verdict["model"] == "gpt-6-sol" and set(verdict["dimensions"].values()) == {"yes"}
206+
for key, verdict in record.verdicts.items() if key.startswith("module:"))
207+
208+
209+
def test_legacy_module_cards_do_not_gain_an_independent_native_call(world):
210+
_world_with_tools(world)
211+
_skeleton(world)
212+
gateway = CardGateway()
213+
record = run_stage(_runtime(world, gateway), _modules_lifecycle(), "modules", dry_run=True)
214+
assert record.status == "dry_run", record.problems
215+
assert not any(call["system"] == JUDGE_SYSTEM for call in gateway.calls)
216+
assert not record.verdicts

0 commit comments

Comments
 (0)