Skip to content

Commit 92dff03

Browse files
committed
Retain checked facets when a depth trace is unsupported
1 parent cc9f903 commit 92dff03

5 files changed

Lines changed: 81 additions & 13 deletions

File tree

‎doc/architecture/SPEC/kb_service.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -532,6 +532,8 @@ flow 至少包含两个不同定义的直接调用;Python 校验限定符号
532532
每维用 `kb:depth` 绑定 feature、facet、完整 pin 与正文哈希,`kb:depth-proof` 绑定精确证据
533533
区间及其哈希、flow trace。区间必须完整展示给生成器,不允许跨缺口。页面必须是无规则的
534534
architecture/guide;整页 advisory pass 后才写入,拒绝或证据不足的草稿保留在本地 checkpoint/trace。
535+
机器枚举始终用固定英文标识,正文遵循知识语言。单个无证据或无效维度被剔除并记录原因,
536+
其它有效维度仍须整页通过裁判才保存;flow 不允许用测试调用链替代生产实现。
535537
来源引用、符号、L1 与知识树校验仍需通过。
536538

537539
`coverage.semantic_depth` 单列完整功能数、七维槽位数和有语义证据的生产文件;

‎src/infermatrix_copilot/kb_service/depth_inputs.py‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,13 @@
3939
Only the provided source/document arrays are evidence. Preserve setting names,
4040
defaults and error conditions exactly. Use the language of the language sample.
4141
Use placeholders for user/machine directories and addresses.
42+
Schema keys and enum values MUST remain the exact English identifiers shown
43+
below, including flow/api/configuration/dependencies/failure_modes/tradeoffs/
44+
validation and fact/inference. Only titles and body prose follow the language
45+
sample. Merge claims for one facet into ONE section; never repeat a facet.
46+
flow describes production implementation, not a test fixture's execution.
47+
Adjacent trace steps must call one another, not be siblings called by an entry
48+
point. Omit flow if the offered slices cannot establish that direct chain.
4249
4350
Return one JSON object in a json fence, with no preamble or epilogue:
4451
{"title":"feature implementation","sections":[

‎src/infermatrix_copilot/kb_service/init_knowledge_depth.py‎

Lines changed: 30 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import json
66
import re
77
import time
8+
from collections import Counter
89
from dataclasses import dataclass, replace
910
from pathlib import Path
1011

@@ -24,6 +25,11 @@
2425
from .models import ModelUnavailable
2526

2627

28+
def validate_container(data: dict) -> None:
29+
if not isinstance(data, dict) or not isinstance(data.get("sections"), list) or len(data["sections"]) > len(FACETS):
30+
raise ValueError("sections must list at most seven depth facets")
31+
32+
2733
@dataclass
2834
class _KnowledgeDepth(_Knowledge):
2935
STAGE = "knowledge-deepen"
@@ -43,7 +49,7 @@ def _init_identity(self) -> str:
4349
":subscription-generator" if self.rt.subscription_generator else "")
4450

4551
def _input_options(self) -> dict:
46-
return {**super()._input_options(), "depth_version": 1}
52+
return {**super()._input_options(), "depth_version": 2}
4753

4854
def _base_for_run(self, latest: str) -> str:
4955
previous = InitRecord.load(self.rt.state_dir, self.lifecycle.repo, self.STAGE)
@@ -100,6 +106,9 @@ def _build(self, tree: Path) -> InitRecord:
100106
_, problems = verified_blocks(self.head[page], feature, tree, self.record.pin)
101107
if problems:
102108
return self._blocked(problems + ["refresh stale/edited depth explicitly before extending it"])
109+
initial = audit_depth(self.head, tree, policy, self.record.pin)
110+
if initial["problems"]:
111+
return self._blocked(initial["problems"])
103112
context = DepthContext(tree, inventory(tree, policy))
104113
# A completed first pass takes priority over repairs, so one difficult
105114
# feature cannot consume the budget before the others get visited.
@@ -211,26 +220,35 @@ def _attempt(self, tree, context, feature, entry):
211220
entry["status"] = "extracting"
212221
self.record.save(self.rt.state_dir)
213222
entry["draft"] = generate(self.rt, self.budget, self.lifecycle.init, system=SYSTEM_DEPTH,
214-
prompt=prompt(payload), validate=validate_draft).data
223+
prompt=prompt(payload), validate=validate_container).data
215224
entry["context_sha256"] = digest(prompt(payload))
216225
entry["status"] = "extracted"
217226
self.record.save(self.rt.state_dir)
218227
elif entry.get("context_sha256") != digest(prompt(payload)):
219228
raise ValueError("checkpoint draft context changed")
220229
data = entry["draft"]
221-
validate_draft(data)
230+
validate_container(data)
222231
offered = files + docs
223232
blocks = dict(old_blocks)
233+
duplicates = Counter(s.get("facet") for s in data["sections"] if isinstance(s, dict) and isinstance(s.get("facet"), str))
234+
skipped = []
224235
for section in data["sections"]:
225-
if section["facet"] in old_blocks or section["facet"] not in payload["facets"]:
226-
raise ValueError("draft attempts to rewrite an existing depth facet")
227-
for evidence in section["evidence"]:
228-
if not any(item["path"] == evidence["path"] and item["start"] <= evidence["start"]
229-
<= evidence["end"] <= item["end"] for item in offered):
230-
raise ValueError("depth evidence crosses an unshown source gap")
231-
blocks[section["facet"]] = render_block(feature, section, tree, self.lifecycle.full_name, self.record.pin)
236+
try:
237+
validate_draft({"sections": [section]})
238+
if duplicates[section["facet"]] > 1 or section["facet"] not in payload["facets"]:
239+
raise ValueError("duplicate or unrequested depth facet")
240+
for evidence in section["evidence"]:
241+
if not any(item["path"] == evidence["path"] and item["start"] <= evidence["start"]
242+
<= evidence["end"] <= item["end"] for item in offered):
243+
raise ValueError("depth evidence crosses an unshown source gap")
244+
if section["facet"] == "flow" and any(s["path"] not in context.production for s in section["trace"]):
245+
raise ValueError("flow must trace production implementation")
246+
blocks[section["facet"]] = render_block(feature, section, tree, self.lifecycle.full_name, self.record.pin)
247+
except (ValueError, TypeError, KeyError, SyntaxError) as exc:
248+
skipped.append(str(section.get("facet", "invalid")) + ": " + str(exc) if isinstance(section, dict) else str(exc))
249+
entry["skipped_facets"] = skipped
232250
if blocks == old_blocks:
233-
raise ModelUnavailable("no supported new depth facets")
251+
raise ModelUnavailable("no supported new depth facets; " + "; ".join(skipped))
234252
front = _page_frontmatter(_one_line(feature.title) + ":实现深读", kind="architecture",
235253
today=self.today, tags=self.tags)
236254
related = f"[功能概览]({Path(feature.page).name}) · [owner 入口](_index.md)\n\n"
@@ -260,7 +278,7 @@ def _attempt(self, tree, context, feature, entry):
260278
self.record.verdicts[key] = entry["checked"]
261279
entry["shown_source_files"] = sorted({f["path"] for f in files})
262280
entry["status"] = label
263-
entry["reason"] = json.dumps(reasons, ensure_ascii=False)
281+
entry["reason"] = json.dumps({"review": reasons, "unsupported_facets": skipped}, ensure_ascii=False)
264282
entry.pop("draft", None)
265283
entry.pop("checked", None)
266284
if label != "pass":

‎src/infermatrix_copilot/kb_service/knowledge_depth.py‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@ def render_block(feature, section: dict, tree: Path, full_name: str, pin: str) -
197197
body += "设计推断(非作者历史意图):\n\n"
198198
body += section["body"].strip() + "\n\n"
199199
if proof["trace"]:
200-
body += "调用路径:" + " → ".join(chr(96) + s["path"] + "::" + s["symbol"] + chr(96)
200+
body += "调用路径:" + " → ".join(chr(96) + s["path"] + chr(96) + "(" + chr(96) + s["symbol"] + chr(96) + ")"
201201
for s in proof["trace"]) + "\n\n"
202202
body += "来源:" + ", ".join(
203203
f"[{e['path']}:L{e['start']}–L{e['end']}](https://github.com/{full_name}/blob/{pin}/"
@@ -256,6 +256,11 @@ def audit_depth(head: dict[str, str], tree: Path, policy, pin: str) -> dict:
256256
for feature in policy.features:
257257
page = depth_page(feature)
258258
blocks, problems = verified_blocks(head[page], feature, tree, pin) if page in head else ({}, [])
259+
if "flow" in blocks:
260+
proof = json.loads(_PROOF.search(_BLOCK.search(blocks["flow"]).group(5)).group(1))
261+
if any(s["path"] not in production for s in proof["trace"]):
262+
blocks.pop("flow")
263+
problems.append(f"{feature.id}/flow: trace must use production implementation")
259264
errors.extend(problems)
260265
for block in blocks.values():
261266
proof = json.loads(_PROOF.search(_BLOCK.search(block).group(5)).group(1))

‎test/test_kb_knowledge_depth.py‎

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,42 @@ def test_repeated_explicit_retry_can_retry_missing_facets(world):
274274
assert accepted.spent_usd == 2.5 and accepted.coverage["semantic_depth"]["complete_features"] == 1
275275

276276

277+
def test_bad_flow_does_not_discard_checked_other_facets(world):
278+
_baseline(world)
279+
280+
class BadFlow(DepthGateway):
281+
def call_json(self, role, **kwargs):
282+
result = super().call_json(role, **kwargs)
283+
if kwargs["system"] == SYSTEM_DEPTH:
284+
for section in result.data["sections"]:
285+
if section["facet"] == "flow":
286+
section["trace"][0]["symbol"] = "OtherEngine.step"
287+
return result
288+
289+
record = _run(world, BadFlow())
290+
assert record.status == "dry_run", record.problems
291+
assert record.coverage["semantic_depth"]["covered_facets"] == 6
292+
assert record.coverage["semantic_depth"]["features"]["step0"]["missing_facets"] == ["flow"]
293+
assert record.depth["features"]["step0"]["skipped_facets"]
294+
295+
296+
def test_client_trace_uses_its_own_checker_and_ignores_comment_calls(tmp_path):
297+
from types import SimpleNamespace
298+
from infermatrix_copilot.knowledge_service.facts import claims_in
299+
from infermatrix_copilot.kb_service.knowledge_depth import verify_trace
300+
301+
(tmp_path / "client.ts").write_text("function run() { return helper(); }\nfunction helper() { return 1; }\n")
302+
section = {"facet": "flow", "title": "Client flow", "body": "run returns helper's result.",
303+
"interpretation": "fact", "evidence": [{"path": "client.ts", "start": 1, "end": 2}],
304+
"trace": [{"path": "client.ts", "symbol": "run", "start": 1, "end": 1},
305+
{"path": "client.ts", "symbol": "helper", "start": 2, "end": 2}]}
306+
block = render_block(SimpleNamespace(id="client"), section, tmp_path, "o/demo", "a" * 40)
307+
assert not any(c.kind == "symbol" for c in claims_in(block, {"client.ts"}, active=True))
308+
(tmp_path / "client.ts").write_text("function run() { /* helper() */ return 1; }\nfunction helper() { return 1; }\n")
309+
with pytest.raises(ValueError, match="does not show"):
310+
verify_trace(tmp_path, section["trace"])
311+
312+
277313
@pytest.mark.parametrize("command, stage", [("widen", "knowledge"), ("deepen", "knowledge-deepen")])
278314
def test_aliases_use_init_before_opening_a_ledger(monkeypatch, command, stage):
279315
from infermatrix_copilot.kb_service import cli

0 commit comments

Comments
 (0)