From 7cf943cf5fdff5e846d30e5510e2c73e89887f8c Mon Sep 17 00:00:00 2001 From: tzhouam Date: Wed, 30 Sep 2026 17:01:41 +0800 Subject: [PATCH 1/4] Let L1 accept new-directory indexes in bootstrap mode, and check index links MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit kb init (design v3 §9.2/§9.3) must create repos//_index.md and components//_index.md, which check_changeset refuses outright ("index files are never created or deleted by a change set"). check_changeset gains a keyword `bootstrap` (default False, behaviour unchanged). When set it relaxes exactly two rules: - an _index.md may be created in a directory that had no files at base; it must link every page and child-directory index in its directory, and an existing parent index must link it. Creating an index in an existing directory and deleting any index stay refused. - the shared knowledge/repos/_index.md may be modified (listing a new repository); adding or deleting it stays refused. check_index_links(base, head) is the init-side check for index edits that L1 only classifies as prose (tables, cross-directory links): links a change adds must resolve inside the knowledge tree, a changed index keeps its base links, and every new page or child index is linked from its parent index. A test pins that only kb_service/init_stages.py (kb init, a later PR) may pass bootstrap=True. Co-Authored-By: Claude Opus 5.5 --- doc/architecture/SPEC/knowledge_service.md | 5 + doc/architecture/SPEC/knowledge_service/l1.md | 32 +- .../knowledge_service/l1.py | 259 ++++++++++++++- test/test_kb_init_l1.py | 306 ++++++++++++++++++ 4 files changed, 594 insertions(+), 8 deletions(-) create mode 100644 test/test_kb_init_l1.py diff --git a/doc/architecture/SPEC/knowledge_service.md b/doc/architecture/SPEC/knowledge_service.md index bf4c3261..b0fd0262 100644 --- a/doc/architecture/SPEC/knowledge_service.md +++ b/doc/architecture/SPEC/knowledge_service.md @@ -48,6 +48,11 @@ library only. ## 2026-09-28 T1 索引修复 L1 的索引检查允许为目录中已存在但未列入索引的页面补链接(巡检 T1 修复),新页面仍必须被链接。 +## 2026-09-30 kb init 的 L1 bootstrap 模式 +`l1.check_changeset(..., bootstrap=)` 只供 `kb init`(`kb_service/init_stages.py`):允许在新目录新建 +`_index.md`、修改共享的 `repos/_index.md`;`l1.check_index_links(base, head)` 检查 init 改动的索引链接。 +细节见 [l1](knowledge_service/l1.md)。服务门禁与发布器从不传 `bootstrap`。 + ## 2026-09-28 知识验证器(v8:发布器本地门禁) `gate_verifier.verify_change(ctx, pr=, head_sha=, pre=, post=, final=, effective_base=, verdict=)` 是发布器本地门禁 (`kb_service.local_gate`)在合并前与合并后运行的检查;v7 的仓库端 `kb-gate` 工作流、`.github/kb-gate/` 验证包、 diff --git a/doc/architecture/SPEC/knowledge_service/l1.md b/doc/architecture/SPEC/knowledge_service/l1.md index 3fe1f133..ebc863fa 100644 --- a/doc/architecture/SPEC/knowledge_service/l1.md +++ b/doc/architecture/SPEC/knowledge_service/l1.md @@ -1,8 +1,8 @@ # knowledge_service/l1.py —— 规范 - + -`LOC ~420 · 知识质量门的确定性一层(L1) · refactor-status: stable` +`LOC ~600 · 知识质量门的确定性一层(L1) · refactor-status: stable` ## 职责 - `check_tree(files)`:尾注合法、supersede 双向一致、tombstone ID 未被复用、 @@ -24,11 +24,39 @@ 同目录 `_index.md` 出现。 - purge 时序:tombstone 的 `purged_at` 不得等于该规则的 `retired_at`;传入可信的 `release` 时 `purged_at` 必须等于它。 +- `check_changeset(..., bootstrap=)`:仅供 `kb init`(唯一允许的调用方是 + `kb_service/init_stages.py`,测试扫描源码钉住)。只放宽两条: + - 新目录(base 中该目录下没有任何文件)可以**新建** `_index.md`,产出一个 prose 块;新索引必须 + 链接本目录的每个页面和每个子目录的 `_index.md`,已存在的父目录索引必须链接它 + (父索引也是本次新建时由父索引自己报告)。在已有目录新建索引、删除索引仍然拒绝。 + - 共享的 `knowledge/repos/_index.md` 可以被**修改**(登记新仓库),新增/删除仍拒绝。 + 其余规则(尾注、frontmatter、`sources`、白名单其余部分、保护规则)不变;默认 `False` + 时行为与原来逐字节一致。 +- `check_index_links(base, head)`:init 侧的索引链接检查(索引改成表格或跨目录链接时 + L1 只把它当 prose 块)。对每个新增或改动的 `_index.md`:本次新增的相对链接必须解析到 + head 中的文件或目录、不得逃出知识树(issue `index_link_broken` / `index_link_escapes`; + URL、纯锚点不算链接,base 已有的链接不重新判定——有些指向 `doc/`);链接识别行内链接 + (可带 `<尖括号>` 目标与 `"标题"`/`'标题'`/`(标题)`)和引用式链接(定义 `[r]: target` 只在被 `[文本][r]`、`[r][]`、`[r]` 实际使用时才算链接, + 标签不区分大小写),其余无法解析 + 的 `](` 一律报 `index_link_unparsed`,不静默跳过;改动的索引不得丢掉 + base 的链接(`index_link_dropped`);head 中每个新页面、新子目录索引都必须被父索引链接 + (`index_unlinked`,知识树根没有索引)。 + - 代码处理按方向 fail-closed,不实现 Markdown 解析器:解析/逃逸/无法解析检查看**所有**像链接的 + 文本(藏在代码里的也查);“是否链接了新页面”“是否保留了原链接”只认**确定可见**的链接——它之前 + (含本行)没有任何类围栏行(容器前缀后的 ```` ``` ```` / `~~~`),且在所在块(段落、列表项、标题、 + 引用,或真表格——空行后开始、第二行是分隔行——正文行的单元格,代码段跨不过单元格)内它之前没有 + 反引号。引用式链接:解析检查查该标签的**每一个**定义(哪个生效取决于哪个在代码里);导航只认只有 + 一个定义且该定义确定可见的标签。“保留”也 fail-closed:base 的链接只有在 head 中确定可见,或它所在的 + 每个块在 head 中原样存在、且该块之前的类围栏行也相同时,才算保留;否则报 `index_link_dropped`(把链接挪进代码 = + 丢链接)。 + 在真实知识树的 142 个索引上逐一追加一行,无误报。 ## 不变量 纯函数;标准库 + PyYAML + `lifecycle`/`ops`,验证包原样携带。段比较忽略尾部空行 (追加段落时前一段多出的分隔空行不算改动)。 ## 测试 +`test_kb_init_l1.py`(bootstrap 新目录索引、父子链接、已有目录仍拒绝、删除拒绝、`repos/_index.md` +只允许修改、链接检查各类 issue、仅 `init_stages.py` 可传 `bootstrap=True`); `test_knowledge_ops_l1.py`(伪造的 sources/frontmatter/删除 active 规则/重复 ID、白名单与 模式、prose 分块、外部引用、真实知识树上执行各类操作后两个既有校验器均通过)。 diff --git a/src/infermatrix_copilot/knowledge_service/l1.py b/src/infermatrix_copilot/knowledge_service/l1.py index bb0bd49a..eaa5e068 100644 --- a/src/infermatrix_copilot/knowledge_service/l1.py +++ b/src/infermatrix_copilot/knowledge_service/l1.py @@ -18,6 +18,14 @@ pages, tombstones. Recomputed here; a mismatch is an issue, never a block a verdict could cover. +``check_changeset`` with ``bootstrap`` set is for ``kb init`` only (the one +allowed caller is ``kb_service/init_stages.py``; a test pins that). It relaxes +exactly two rules so a repository's knowledge can be created: an ``_index.md`` +may be created in a directory that had no files before, and the shared +``repos/_index.md`` may be edited to list a new repository. +``check_index_links(base, head)`` is the init-side link check for the index +edits that bootstrap produces. + Standard library + PyYAML + ``lifecycle``/``ops`` only. """ @@ -25,6 +33,7 @@ import hashlib import json +import posixpath import re from dataclasses import dataclass from pathlib import PurePosixPath @@ -48,6 +57,87 @@ _DENY = re.compile(r"knowledge/(?:skills|tools|\.claude)/|knowledge/[^/]+$") _REGULAR_MODE = "100644" _INDEX_LINE = re.compile(r"- \[[^\]\n]+\]\((?P[^)/\s]+\.md)\)") +# bootstrap only: the shared list of repositories, edited to link a new one +_REPOS_INDEX = "knowledge/repos/_index.md" +# inline links (optional target and "title" / 'title' / (title)) and +# reference definitions; any other "](" is unparsed and refused by the link check +_LINK_TITLE = r"""(?:\s+(?:"[^"\n]*"|'[^'\n]*'|\([^)\n]*\)))?""" +_MD_LINK = re.compile(r"\]\(\s*(?:<(?P[^>\n]*)>|(?P[^)\s]+))" + _LINK_TITLE + r"\s*\)") +_MD_REF = re.compile(r"(?m)^ {0,3}\[(?P