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
6 changes: 6 additions & 0 deletions doc/architecture/SPEC/kb_service.md
Original file line number Diff line number Diff line change
Expand Up @@ -642,6 +642,12 @@ documented_manual;人工步骤须来自已有文档并标明本轮未执行。
与验证类型,仍受两页和 6,000 字符预算约束。

共享的源码/测试/文档索引绑定 pin、政策、完整生产范围、版本与摘要;只建立一次。
轻量深化还从该索引定位功能源码父目录中的实际项目文档,到最近组件指南为止,
包括相邻的 SKILL、README、设计文档和 reference;有局部正文时先保留它们,否则
使用通用 owner 文档,不扩展到其他组件的兄弟目录。各文档共享 8 KB 完整行段预算,
优先实际入口文件名附近的行段及评审反馈,最终组包保留已选片段;没有既有操作和
可观察预期的文档仍保持未知,补读不表示执行测试。strict 和未配置共享索引的旧输入不变。
旧草稿和原生审批仍核对原始保存的输入,不重写上下文或重置累计修正次数。
轻量组包保持完整小片段,修正优先补读评审指出的符号和范围。每个缺口至多首轮加三次
自动修正,计数在派发前落盘,恢复或显式 retry 不清零;通过的维度冻结,失败只留在该项。
未派发评审的草稿可在原轮恢复。逐批核对格式、引用、哈希与计数,完整验收集中在交付前。
Expand Down
9 changes: 9 additions & 0 deletions plugins/infermatrix-copilot/skills/imkbinit/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,15 @@ points, and separately measures paths-only ambiguity. Retrieval remains bounded
to two documents and 6,000 content characters. Distinguish all `available_facets`
from actually delivered `included_facets`, `not_injected_facets`, and their bases;
read the remaining relevant facets through the existing bounded doc tools.
Lightweight depth also reads existing indexed project documents in each source
file's ancestor directories up to the nearest component guide, including
adjacent SKILL, README, design and reference guides. Generic owner documents
are the fallback when local text is unavailable. Documents share the existing
8 KB budget, favor real source filename windows, and retain exact pinned ranges
in the final packet. Documented manual operations
and observable expected results may support an explicitly unexecuted manual
validation entry; unrelated sibling components and invented tests cannot.
Keep saved native inputs and cumulative correction counts unchanged on resume.
`verified_gaps` describe absent evidence, not tests passed; missing facets remain
unknown. Save reports outside `knowledge/` and report retrieval separately from
breadth/depth and actual PR quality. This audit does not establish bug recall.
Expand Down
13 changes: 12 additions & 1 deletion src/infermatrix_copilot/kb_service/depth_inputs.py
Original file line number Diff line number Diff line change
Expand Up @@ -586,7 +586,7 @@ def _indexed_test_association(self, feature, docs):
return json.loads(json.dumps(result))

def document_slices(self, docs, *, paths=(), limit=8_000, previous_review=None, evidence_round=0,
facets=(), symbols=()):
facets=(), symbols=(), preferred_ranges=()):
"""Select contiguous real lines from full indexed docs, then fallback input.

References in a rejection take priority over a document prefix. The
Expand All @@ -605,6 +605,7 @@ def document_slices(self, docs, *, paths=(), limit=8_000, previous_review=None,
patterns = [re.compile(_FACET_PATTERNS[facet], re.I) for facet in facets if facet in _FACET_PATTERNS]
references = [(match[1], int(match[2]), int(match[3] or match[2]))
for match in re.finditer(r"([\w./-]+):L?(\d+)(?:[-:]L?(\d+))?", review)]
references.extend(preferred_ranges) # indexed filename matches, not approval evidence
ranges, lines_by_path = [], {}
for path in names:
if not safe_source_path(path):
Expand Down Expand Up @@ -638,6 +639,16 @@ def document_slices(self, docs, *, paths=(), limit=8_000, previous_review=None,
rotated.extend(tied[shift:] + tied[:shift])
ranges = rotated
selected, used = {}, 0
for path, start, end in preferred_ranges:
lines = lines_by_path.get(path, {})
if not all(number in lines for number in range(start, end + 1)):
continue
pending = {number: lines[number] for number in range(start, end + 1)
if number not in selected.get(path, {})}
size = sum(len(line.encode("utf-8")) + 1 for line in pending.values())
if used + size <= limit:
selected.setdefault(path, {}).update(pending)
used += size
for _, path, start, end in ranges:
for number in range(start, end + 1):
line = lines_by_path[path][number]
Expand Down
79 changes: 74 additions & 5 deletions src/infermatrix_copilot/kb_service/init_knowledge_depth.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import time
from collections import Counter
from dataclasses import dataclass, replace
from pathlib import Path
from pathlib import Path, PurePosixPath

from ..knowledge_service.lifecycle import Page
from ..knowledge_service.ops import page_over_capacity
Expand Down Expand Up @@ -397,7 +397,65 @@ def _slot_result(entry, facet, status, reason):
if visits[key] >= 3:
slot["blocked"] = "three unsuccessful repairs with the same pinned evidence; needs new evidence or a corrected candidate"

def _docs(self, tree: Path, feature, owner) -> list[dict]:
def _docs(self, tree: Path, feature, owner, *, context=None, facets=(),
previous_review=None, evidence_round=0) -> list[dict]:
if context is not None and context.mode == "lightweight" and context.index is not None:
# A source-only feature can still have a README/SKILL/design guide
# beside its implementation. Select only real indexed ancestor
# documents, never another component's sibling directory.
by_parent = {}
for path, fact in context.index.files.items():
document = PurePosixPath(path)
if (document.suffix.lower() not in
{".md", ".mdx", ".rst", ".txt", ".adoc"} or fact["sha256"] is None):
continue
name = document.stem.lower()
priority = next((number for number, prefix in enumerate(
("skill", "readme", "design", "architecture", "reference"))
if name.startswith(prefix)), 5)
by_parent.setdefault(document.parent.as_posix(), []).append((priority, path))
adjacent = []
for source in context._scope(feature):
for distance, parent in enumerate(PurePosixPath(source).parents):
siblings = by_parent.get(parent.as_posix(), ())
adjacent.extend((distance, priority, path) for priority, path in siblings)
if any(priority < 4 for priority, _ in siblings):
break # the nearest component guide is the local boundary
paths = list(dict.fromkeys([*feature.docs, *(path for _, _, path in sorted(adjacent))]))
filenames = {PurePosixPath(path).name for path in context._scope(feature)}
preferred = []
for path in paths:
matches, examples, fenced = [], [], False
for number, line in enumerate(context.index.files.get(path, {}).get("lines", ()), 1):
if line.lstrip().startswith(("```", "~~~")):
fenced = not fenced
if any(name in line for name in filenames):
matches.append(number)
if fenced:
examples.append(number)
# Existing executable examples beat prose lists of unrelated
# tools. Nearby output shapes/criteria remain real line ranges.
preferred.extend((path, max(1, number - 6),
min(len(context.index.files[path]["lines"]), number + 6))
for number in (examples or matches))
# The immutable index supplies complete real line ranges, including
# late manual acceptance steps. The existing 8KB packet cap applies.
docs, used = [], 0
for number, path in enumerate(paths):
selected = context.document_slices([], paths=(path,),
limit=(8000 - used) // (len(paths) - number), facets=facets,
previous_review=previous_review, evidence_round=evidence_round,
preferred_ranges=preferred)
docs.extend(selected)
used += sum(len(line.encode("utf-8")) + 1 for item in selected for line in item["text"])
if docs:
# build() rereads whole indexed documents. Offering a generic
# owner path here would let that reread crowd out the local
# command/criteria despite this first selection's byte cap.
return docs
generic = [item for item in self._docs_for(tree, owner) if item["path"] not in paths]
return context.document_slices(generic, limit=8000, facets=facets,
previous_review=previous_review, evidence_round=evidence_round)
docs = self._sources(tree, list(feature.docs), MAX_DOC_BYTES // 2)
used = sum(len(d["text"].encode("utf-8")) for d in docs)
for item in self._docs_for(tree, owner):
Expand Down Expand Up @@ -444,17 +502,28 @@ def _attempt(self, tree, context, feature, entry):
owner = next((o for o in self.owners if o.owner == feature.owner),
Owner(feature.owner, feature.page, tuple(p.split("*", 1)[0] for p in feature.source_globs)))
guidance = getattr(self, "_repair_guidance", None)
docs = [] if guidance else self._docs(tree, feature, owner)
requested = entry.get("requested_facets") or [f for f in FACETS if f not in old_blocks]
docs = [] if guidance else self._docs(tree, feature, owner, context=context,
facets=requested, previous_review=entry.get("reason"),
evidence_round=entry.get("evidence_round", 0))
existing = self._bounded_context(self._existing_knowledge(owner, self.head.get(feature.page, "")))
if lightweight:
docs = self._bounded_slices(docs, 8000)
existing = self._bounded_texts(existing, 4000)
requested = entry.get("requested_facets") or [f for f in FACETS if f not in old_blocks]
retrieval = context.guided(feature, requested, guidance) if guidance else context.build(
feature, self.head.get(feature.page, ""), docs, facets=requested,
previous_review=entry.get("reason"), evidence_round=entry.get("evidence_round", 0))
if lightweight:
docs = retrieval.get("docs", docs)
if context.index is not None and not guidance:
# Preserve the already selected local document ranges; a second
# full-index rerank can replace their commands/criteria. Bind
# the actual final packet, without changing any saved payload.
retrieval["docs"] = docs
Comment on lines +516 to +520

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve symbol-aware document ranking

When an indexed lightweight feature has a large explicit or adjacent guide whose relevant section mentions a source symbol but not the source filename or generic facet keywords, _docs() can retain only unrelated earlier slices because it ranks without the symbols that DepthContext.build() derives. build() still finds the symbol-relevant range, but this assignment overwrites it with the first-pass slices; across the four bounded repair rounds, late sections can therefore remain unavailable and a supportable facet may be exhausted as unknown. Preserve the symbol-ranked result or pass the source symbols into the initial selection.

Useful? React with 👍 / 👎.

retrieval["context_sha256"] = hashlib.sha256(json.dumps(
{key: value for key, value in retrieval.items() if key != "context_sha256"},
sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode()).hexdigest()
else:
docs = retrieval.get("docs", docs)
files = retrieval["files"]
if not files and not (lightweight and docs):
entry["attempts"] = entry.get("attempts", 0) + 1
Expand Down
Loading
Loading