Skip to content

Commit 19a1dcc

Browse files
authored
Merge pull request #204 from JiusiServe/kb/knowledge-ops-l1
Add typed knowledge operations with a rule lifecycle and the L1 gate
2 parents 5910694 + ced168f commit 19a1dcc

15 files changed

Lines changed: 1850 additions & 8 deletions

File tree

‎doc/architecture/SPEC/adapters/base.md‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# adapters/base.py —— 规范
22

3-
<!-- verified-against: 2026-09-26 -->
3+
<!-- verified-against: 2026-09-28 -->
44

55
`LOC ~423 · 边缘(仓库知识) · refactor-status: ok`
66

@@ -58,3 +58,6 @@
5858
**把 `capabilities` 推导保持为 planner 信任的唯一来源** ——
5959
`app.repository_context` 只为实际 checkout **补上** `repo.path`,
6060
并将同一 adapter policy 交给执行层。
61+
62+
## 2026-09-28 退役规则不再提供
63+
`render_briefing_docs` 经 `visible_text` 去掉退役规则。

‎doc/architecture/SPEC/knowledge_docs.md‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,6 @@
3333

3434
## 2026-09-28 快照校验
3535
可选 `verify(rel)` 回调在 `read`/`search` 读取每个文件前调用;MCP 传入 `KnowledgeView.path`,快照中缺失或被改动的文件使读取/搜索 fail-closed。
36+
37+
## 2026-09-28 退役规则不再提供
38+
`read`/`search` 经 `visible_text` 去掉退役规则后再返回。

‎doc/architecture/SPEC/knowledge_service.md‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# knowledge_service/ — provider curation components
22

3-
<!-- verified-against: 2026-09-26 -->
3+
<!-- verified-against: 2026-09-28 -->
44

55
The provider owns knowledge curation beneath the public SDK v1 facade.
66
`KnowledgeCurator` composes four domain components over one explicit work
@@ -17,3 +17,11 @@ local commits and proposal export. The public `sdk.v1.knowledge` module only
1717
re-exports the curator and validator error. Existing proposal IDs, errors,
1818
wire projections, validator order and rollback behavior are compatibility
1919
contracts across the move.
20+
21+
Knowledge Ops API 2.0 sits beside the v1 curator and does not change it:
22+
`lifecycle` parses rule pages (byte-exact) and their `kb:rule` footers,
23+
`ops.apply_operations` applies typed add / edit_same_meaning / replace / retire
24+
/ purge changes with their mechanical consequences, and `l1` is the
25+
deterministic half of the quality gate (`check_tree`, `check_changeset`). The
26+
three modules need only the standard library and PyYAML and import nothing else
27+
from the package, so the kb-gate verifier bundle can vendor them.
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# knowledge_service/l1.py —— 规范
2+
3+
<!-- verified-against: 2026-09-28 -->
4+
5+
`LOC ~420 · 知识质量门的确定性一层(L1) · refactor-status: stable`
6+
7+
## 职责
8+
- `check_tree(files)`:尾注合法、supersede 双向一致、tombstone ID 未被复用、
9+
`_routes.yaml` 合法且引用页面存在。
10+
- `check_changeset(base, head, changes, external_texts=)`:
11+
- 路径白名单(`knowledge/{repos/<r>,general}/**/*.md`、`_routes.yaml`、`_tombstones.yaml`)
12+
与文件模式 `100644`;拒绝 `knowledge/{skills,tools,.claude}/`、顶层文件、非 UTF-8。
13+
- 无新增重复 ID、不复用退役或 tombstone ID、无指向本次退役/删除规则的悬空引用、
14+
无新增树级问题、页面不超拆分线。
15+
- 机械项按规则重算:frontmatter 只允许 `updated`(不倒退)与 `sources`(等于
16+
`expected_sources`)变化;`_index.md` 只允许为新页追加索引行;tombstones 等于本次
17+
purge 的 ID 且为规范格式。不符即 issue,不能靠判定覆盖。
18+
- 其余变更分块:`rule`(add / edit / retire / supersede / purge,按段内容哈希)与
19+
`prose`(页头、非规则段、非规则页、`_routes.yaml`)。每块有稳定 `block_id`,
20+
供 L2 判定与签名逐块绑定。
21+
- `external_refs`:知识目录之外引用了本次退役/删除 ID 的文件(配套 PR 流程)。
22+
- 嵌套 `###` 规则随父规则退役、删除并记入 tombstones;编辑不得增删嵌套 ID;保护状态以
23+
旧尾注为准且不得被移除;页面类型不得借改 `type:` 退出生命周期;每个新页面都必须在
24+
同目录 `_index.md` 出现。
25+
- purge 时序:tombstone 的 `purged_at` 不得等于该规则的 `retired_at`;传入可信的
26+
`release` 时 `purged_at` 必须等于它。
27+
28+
## 不变量
29+
纯函数;标准库 + PyYAML + `lifecycle`/`ops`,验证包原样携带。段比较忽略尾部空行
30+
(追加段落时前一段多出的分隔空行不算改动)。
31+
32+
## 测试
33+
`test_knowledge_ops_l1.py`(伪造的 sources/frontmatter/删除 active 规则/重复 ID、白名单与
34+
模式、prose 分块、外部引用、真实知识树上执行各类操作后两个既有校验器均通过)。
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# knowledge_service/lifecycle.py —— 规范
2+
3+
<!-- verified-against: 2026-09-28 -->
4+
5+
`LOC ~330 · 规则页数据模型与生命周期尾注 · refactor-status: stable`
6+
7+
## 职责
8+
把规则页解析为 frontmatter、head 与 `## ` 段(跳过代码围栏内的标题),未改动的页
9+
解析再渲染逐字节相同。规则段可带 `<!-- kb:rule ... -->` 尾注(status、since、retired_at、
10+
reason、evidence、supersedes、superseded_by、protected);无尾注即 active。
11+
12+
## 公开契约
13+
`Page.parse/render`、`Page.rules/rule/replace_section/append_section`、
14+
`Page.sources/with_sources`(按页面原风格写 flow 或 block 列表)、`Section.footer/
15+
with_footer/body_without_footer/content_sha256/citations`、`Footer.parse/render/check`、
16+
`expected_sources`(旧列表 + active 规则新引用,只追加)、`visible_text`(去掉退役规则,
17+
无退役规则时逐字节原样返回)。
18+
19+
## 不变量
20+
- 仅依赖标准库与 PyYAML、不 import 包内其他模块:kb-gate 验证包原样携带。
21+
- 尾注键未知、重复、格式错误或状态约束不满足时抛 `LifecycleError`。
22+
23+
## 测试
24+
`test_knowledge_ops_l1.py`(全树逐字节往返、服务端隐藏退役规则)。
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# knowledge_service/ops.py —— 规范
2+
3+
<!-- verified-against: 2026-09-28 -->
4+
5+
`LOC ~360 · Knowledge Ops API 2.0(类型化知识变更) · refactor-status: stable`
6+
7+
## 职责
8+
`apply_operations(files, ops, release, today)` 是纯函数:输入知识相对路径到文本的映射与
9+
类型化操作(`add`、`edit_same_meaning`、`replace`、`retire`、`purge`),输出变更后的文件。
10+
随后写入机械结果:`updated:`、只追加的 `sources:`、新页面在同目录 `_index.md` 的一行、
11+
purge 的 `_tombstones.yaml` 条目。
12+
13+
## 不变量
14+
- 新 ID(含嵌套 `###`)在全树唯一,且不在 tombstones 中。
15+
- `replace` 在一次调用内同时退役旧规则(`reason=superseded`、`superseded_by`)并添加带
16+
`supersedes` 的后继规则。
17+
- `purge` 只针对已退役且 `retired_at` 不是本次发版的规则。
18+
- `protected` 规则需 `allow_protected`(人工路径)。
19+
- 超过拆分线(32 KiB / 500 非空行)的页面被拒绝,提示改用新的 `rules-<topic>.md`。
20+
- 任何非法操作抛 `LifecycleError`,输入映射从不被修改(无部分应用)。
21+
- 版本常量 `KNOWLEDGE_OPS_API_VERSION = 2.0.0`,与 v1 `KnowledgeCurator`(1.1.0)并存。
22+
23+
## 依赖(允许)
24+
标准库 + PyYAML + `lifecycle`(验证包原样携带)。
25+
26+
## 测试
27+
`test_knowledge_ops_l1.py`。

‎doc/architecture/SPEC/sdk.md‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,3 +121,6 @@ Direct 1.1 / Strict 1.3 accept typed `CarriedFinding` inputs (unique IDs, source
121121

122122
## 2026-09-28 知识视图
123123
`DirectClient` 每次 `plan` 解析一个 `KnowledgeView`,文档引用与 `resource_revision` 均取自该视图;`validate` 按计划时钉住的知识根校验资源未变,激活新快照不会使进行中的 review 失效;`diagnostics.knowledge_snapshot` 报告所用快照。`read_document(..., review_context_id=...)` 从该 review 计划时钉住的知识树读取(含分页),未签发的 context id 被拒绝;不带 context id 时读当前激活树。
124+
125+
## 2026-09-28 退役规则不再提供
126+
`DirectClient` 返回的文档内容与 `sha256` 均基于去掉退役规则后的服务文本(无退役规则时与磁盘字节一致)。

‎doc/knowledge/SCHEMA.md‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,33 @@ contradictions: [相对路径] # 可选:与本页冲突的页面
6464
先把仍然有效的独有结论合入最近 owner 的幸存页面,再删除被取代或重复页面并修复
6565
入链。Git 历史负责追溯旧版本;`knowledge/` 内不再维护 `_archive/` 副本。
6666

67+
## 规则生命周期尾注
68+
69+
规则段(`## <ID> — <标题>`)可以以一行机器可读尾注结束,渲染时不可见:
70+
71+
```
72+
<!-- kb:rule status=retired since=v0.30.0 retired_at=v0.31.0 reason=upstream-removed evidence="PR #8201" -->
73+
<!-- kb:rule status=active since=v0.31.0 supersedes=BENCH-1b -->
74+
```
75+
76+
- 没有尾注 = `status=active`(兼容全部既有规则)。
77+
- `status=retired` 必须带 `retired_at`、`reason`(`upstream-removed` / `superseded` /
78+
`incorrect` / `duplicate`)与 `evidence`;`superseded` 还必须带 `superseded_by`,且
79+
后继规则带对应的 `supersedes`。
80+
- 退役规则的正文在磁盘上保留一个发版周期供复核与撤销,但**立即停止对外提供**:
81+
Direct 文档读取、`doc_read`/`doc_search`、briefing 都会去掉它。
82+
- `protected=true` 的规则只能经人工路径修改、退役或删除。
83+
- 物理删除(purge)只针对已退役满一个发版周期的规则;被删除的 ID 记入同仓库的
84+
`_tombstones.yaml`,永不复用。
85+
- 尾注、`updated:`、`sources:`(只追加)、新页面的 `_index.md` 行与 tombstones 由知识服务
86+
按 Knowledge Ops API 2.0 写入,并由 L1 门禁重算核对;手工改动也必须符合同一规则。
87+
88+
## Direct 路由表
89+
90+
每个仓库的 Direct owner 路由在 `repos/<repo>/_routes.yaml`(`schema_version: 1`,
91+
`owners[{owner, path, signals, scope_prefixes}]`,可选 `models{dir, page}`)。路由引用的页面
92+
必须存在;新增仓库只需提供它和 adapter,不改代码。
93+
6794
## 校验
6895

6996
```bash

‎src/infermatrix_copilot/adapters/base.py‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616

1717
import yaml
1818

19+
from ..knowledge_service.lifecycle import visible_text
20+
1921
HIGH_RISK_SECTIONS = ("push", "repo", "upstream", "rebase")
2022

2123

@@ -87,7 +89,7 @@ def render_briefing_docs(root: Path | str, docs: list[str], *, header: str = "",
8789
if warnings is not None:
8890
warnings.append(f"briefing document missing or not Markdown: {rel}")
8991
continue
90-
text = p.read_text(encoding="utf-8", errors="replace")
92+
text = visible_text(p.read_text(encoding="utf-8", errors="replace"))
9193
parts.append(_without_frontmatter(text).strip())
9294
if not parts:
9395
return ""

‎src/infermatrix_copilot/knowledge_docs.py‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
from pathlib import Path
88
from typing import Callable
99

10+
from .knowledge_service.lifecycle import visible_text
11+
1012

1113
class KnowledgeDocsError(ValueError):
1214
"""A refused or invalid knowledge-base operation."""
@@ -94,7 +96,7 @@ def read(self, path: str, *, offset: int = 0, limit: int = 24_000) -> dict:
9496
raise KnowledgeDocsError("limit must be a positive integer")
9597
limit = min(limit, 65_536)
9698
target = self._resolve_doc(path)
97-
data = target.read_text(encoding="utf-8", errors="replace")
99+
data = visible_text(target.read_text(encoding="utf-8", errors="replace"))
98100
end = offset + limit
99101
return {
100102
"path": target.relative_to(self.root).as_posix(),
@@ -121,8 +123,8 @@ def search(self, query: str, *, limit: int = 40) -> list[dict]:
121123
seen_files.add(target)
122124
self._checked(target)
123125
rel = target.relative_to(self.root).as_posix()
124-
for lineno, line in enumerate(
125-
target.read_text(encoding="utf-8", errors="replace").splitlines(), 1):
126+
served = visible_text(target.read_text(encoding="utf-8", errors="replace"))
127+
for lineno, line in enumerate(served.splitlines(), 1):
126128
line_folded = line.casefold()
127129
exact = folded in line_folded
128130
term_match = bool(terms) and all(t in line_folded for t in terms)

0 commit comments

Comments
 (0)