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
11 changes: 9 additions & 2 deletions doc/architecture/SPEC/engine/steps/knowledge.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# engine/steps/knowledge.py —— 规范

<!-- verified-against: 2026-09-28 -->
<!-- verified-against: 2026-09-30 -->

`知识服务的 knowledge.* 步骤 · refactor-status: new`

Expand All @@ -12,10 +12,17 @@ auto_merge 仓库写 outbox 的 `open_pr` 项,shadow 只记录)。逻辑全
## 不变量
- 只处理 `params.repo` 指定的一个仓库;未知或未启用的仓库 → BLOCKED。
- 步骤之间只经 `state_updates` 传递纯数据(`kb_repo`、`kb_changeset`、`kb_changeset_status`)。
- 从不直接写 GitHub;发布只经签名 outbox 由 GPU 盒发布器执行。
- 从不直接写 GitHub;发布只经签名 outbox 由 GPU 盒发布器执行(唯一例外:`knowledge.init`,见下)。

## 测试
`test_kb_intake_gate.py`。

## 2026-09-28 合并、巡检、激活步骤
新增 `knowledge.advance_merges`、`knowledge.sweep`、`knowledge.activate`;`kb run` 路径自行持有租约,调度器路径复用其租约。

## 2026-09-30 `knowledge.init`(kb init)
参数 `repo`、`stage`、`dry_run`(缺省 true)、`pin`。用自己的 `InitRuntime`(`kb_service/init_support.py`),**不**经
`_runtime`/`_lifecycle`:服务运行时会打开 `kb.db`,且 `_lifecycle` 拒绝未启用的仓库,而为未启用的仓库建库正是 init 的目的。
调 `init_stages.run_stage`;`InitError`/`NotImplementedError` 与状态 `blocked` 的记录都返回 BLOCKED。`state_updates`:
`kb_init_stage`、`kb_init_status`、`kb_init_pr`。它是本模块里唯一直接写 GitHub 的步骤:经仓库主人的 `gh` 开一个由人合并的
PR,且只在 `ALLOW_PUSH=1` 与 `ALLOW_POST=1` 同时成立、非 dry run、上游非私有时。测试:`test_kb_init_skeleton.py`。
101 changes: 101 additions & 0 deletions doc/architecture/SPEC/kb_service.md
Original file line number Diff line number Diff line change
Expand Up @@ -333,3 +333,104 @@ knowledge_root)` 是运行时检查:种子必须是知识树中已存在的文
中,`uncovered_hot` 按次数降序、路径升序;`churn_by_module(prs, modules)` 按最长模块前缀归属(已折叠目录归到折入的
祖先),降序。空集合的比例记为 1.0(没有可漏的路由)。`ROOT_MODULE` 与 `profiles.establish.ROOT_MODULE` 相同,
由测试钉住(本包不依赖 `profiles`)。源根与变更路径按与 `profiles.establish.normalize_root` 相同的规则规范化(`.`、`./` 表示仓库根,接纳一切路径)。测试:`test_kb_init_coverage.py`。


## 2026-09-30 单次调用花费阈值(kb init 预算硬上限的一块积木)
`ModelGateway.call_json(..., max_budget_usd=x)`:x 是**停止阈值**,不是硬上限 —— 花费达到 x 后不再发起新请求,但越过 x 的
那个请求照样计费。硬上限由调用方负责:调用前预留 x + 单个请求的最坏情况。transport 的 `stops_at_spend` 为 False 时在发出调用
**之前**抛 `ModelUnavailable("<provider> cannot stop a call at a spend threshold")`(fail-closed,绝不用 `max_tokens` 近似);
x 必须为正。因阈值停下的调用(`stop_reason="max_budget"`)→ `ModelUnavailable`,并作为失败记录(不会成为训练样本)。
`ModelReply.cost_usd` 取自 `usage["cost_usd"]`(未知为 None),trace 记录同时写入 `cost_usd` 与 `max_budget_usd`。
不传上限的现有调用方行为不变:该参数根本不会发给 transport。
`runtime.trace_recorder` 把阈值与花费持久化到 `model_call` 记录的 `result.max_budget_usd` / `result.cost_usd`
(未设或未知为 null),包括 transport 抛错的失败记录。

## 2026-09-30 kb init:阶段 1(skeleton)、运行时、预算与发布
设计见 kb-init 设计 v3。`kb init REPO --stage S [--dry-run] [--pin SHA]` 经 playbook `kb-init`(单步
`knowledge.init`)运行;`kb init REPO --suggest-seeds` 只打印候选种子(不调模型)。CLI 在打开任何账本**之前**分派
`init`:kb init 从不碰服务的 `kb.db`、outbox、`kb serve`/`kb publish`。

- `init_budget`:`Budget(limit)` 的 `reserve(amount)` 在调用**之前**预留,放不下抛 `BudgetExhausted`;块内
`charge(cost)` 记实际花费,未知/非法花费或块内异常按整笔预留计。生成调用预留 = 阈值 `generator_call_usd` +
单个请求最坏值 `input_bytes × 1.25 × in_price + max_output_tokens × out_price`(claude 的 `--max-budget-usd`
只拦下一个请求)。价格表 `DEFAULT_PRICES`(claude-opus-5-5 = $4/$20 每百万 token、128000 输出上限,2026-09-25
标价)可由 `KB_INIT_PRICES` JSON 覆盖;没有价格的生成模型在任何调用之前被拒(`PriceError`)。判定调用按
`judge_call_usd` 固定记账 —— 这是记账约定,不是花费上限。
- `config.InitConfig` 增加 `generator_call_usd`(>0,缺省 2.0)与 `harness_overhead_bytes`(≥0,缺省 200000)。
- `init_support`:`InitRuntime`(自己的运行时:registry、`ModelGateway`(trace 写到 `<state>/init/traces`)、
知识库克隆 `KB_INIT_KNOWLEDGE_CLONE` 或 `<state>/init/knowledge-repo`、价格表、环境);`InitRecord`(每阶段一个
`<state>/init/<repo>/<stage>.json`:钉点、知识库基点、输入摘要、花费、种子来源、逐规则证据、判定、丢弃原因、清单、
问题、未完成项、PR);`UpstreamPin`(上游 bare 镜像、`resolve`、只读 `export`(拒绝链接与越界成员)、
`PinnedObserver`);`collect_docs`;`generate`/`judge`(先预留);`claim_problems`(设计 §9.1);
`other_path_problems`(`knowledge/` 之外的路径必须在 `INIT_PATHS` 内且有专门检查);`run_knowledge_validators`
(在基点树叠加改动后跑 CI 同款的 `check_knowledge_tree.py` 与 `check_wiki_lint.py`);`InitPublisher`
(临时索引在确切基点上构建一个确定性提交,推到**必须不存在**的分支 —— `--force-with-lease=<ref>:`,经 `gh`
开 PR 并确认 head;dry run 把树与 PR 正文写到本地)。只在 `ALLOW_PUSH=1` 且 `ALLOW_POST=1` 时发布;私有上游
永远 dry run。与 kb 发布器一样不经 `push.guard_push`:它推的是全新分支、从不强推、只写 `INIT_PATHS`。
- `init_stages`(唯一允许调用 `check_changeset(bootstrap=True)` 的模块,由 L1 测试钉住):`run_stage` 与
`validate_change`(设计 §9.3:L1 changeset(bootstrap)、`check_tree`、`check_index_links`、钉点声明与证据、
`knowledge/` 之外的路径)。skeleton:文档语料 → 地图调用(入口页、架构页、owner 路由、general 链接)→ 文档不变量
调用 → 每个 `repos/<other>/` 种子规则页一次改写调用 → 逐条筛查:D5 去掉与文档重复的行、证据在钉点可取且哈希入记录、
钉点声明成立、在一棵逐条累积的树上落位(页满则转到兄弟页 `rules-doc-invariants.md` / `<stem>-<n>.md`,**先落位再
判定**,判定看到的就是规则最终所在的页)→ 咨询性判定(fail 剔除,unsure/unjudged 标出)→ 经 `ops.apply_operations`
写入(新规则页先建壳并登记到目录索引;已有规则只追加,从不修改)→ 新目录建 `_index.md`、已有目录只追加链接、
仓库未登记时登记到 `repos/_index.md`(`INIT_PATHS` 允许这一个共享文件);`_routes.yaml` 只在不存在时生成:先由
已有子目录入口页**确定性**地得出 owner(页中反引号里、在钉点存在、落在 `source_roots` 内的代码路径;`pkg/**` 即目录
`pkg/`),再补模型提出的 owner(scope 前缀须在钉点存在)→ 已有页面的问题进清单(钉点下不成立的声明、与文档重复的行、
没有路由到达的页、表格索引的样式)→ 检查 → dry run 或 PR。钉点缺省取上游镜像的 `HEAD`(远端默认分支,不假设
`main`;每次同步都按 `ls-remote --symref` 刷新镜像的 `HEAD`,默认分支改名或删除后也跟得上)。没有可用 owner 时的
兜底路由用规范化后的 `source_roots`(`./pkg/` → `pkg/`),源根是仓库本身或未设置时用钉点下的全部顶层目录。发布时 `KB_INIT_GIT_AUTHOR` 缺失在任何模型调用之前拒绝。推送之前先把这次发布(基点、分支、文件、标题、
正文、作者、时间)写成 `<stage>-publish.json`(`save_prepared`),记录状态为 `publishing`;推送/开 PR 失败记为
`blocked` 并保留它,重跑时直接完成**同一个**发布(同输入重建出同一提交,已推送的分支与已开的 PR 都复用,不再调模型)。
dry run 每次整体替换输出目录,不残留上一次的页面;有待完成的发布时拒绝 dry run(不覆盖它)。init 写的非规则页
(入口页简介、架构页、目录标题)经 `neutral_headings`:代码之外的 ATX 标题改为粗体段落,其余以 `#` 开头的行转义为
`\#`,代码块里的此类行缩进一个空格 —— 没有任何行以 `#` 开头,`RULE_HEADING`/`ANY_RULE_HEADING` 永远匹配不到,
模型写的 `## Overview` 或 `## SERV-1 — x` 不会变成规则 ID。模型提出的 owner 页必须
是本仓库目录下已有或本阶段创建的页面,指向其他仓库的页面一律丢弃并记入 notes。仓库标签不在 `doc/knowledge/SCHEMA.md` 分类法里、种子不存在、生成模型不可用或无价格 → 记录为 `blocked`,
不产生 PR。同输入摘要重跑直接返回记录;已发布记录的输入变了则拒绝。`harvest-calibration` 抛
`NotImplementedError`。测试:`test_kb_init_skeleton.py`、`test_kb_init_config.py`。

## 2026-09-30 kb init:阶段共用流程与阶段 2(modules)
- `init_stages._Stage`:各阶段共用的流程——钉点、记录、前序阶段链、规则筛查与落位、确定性检查(`_conclude`)、发布;
子类只设 `STAGE` 并实现 `_build`。**阶段链**(设计 §9):前序每个阶段都必须已合并(记录为 `published` 且
`gh pr view` 为 `MERGED`,此时 main 已含它),或者(仅当本阶段是 dry run 时)是 dry run——其快照(`<stage>-dryrun/tree`)
叠加在 main 上作为本阶段的基底,并随本阶段的变更一起交给知识树校验器;`empty`(没有可改的)直接放行。未合并、只 dry
run 却要发布、未运行或处于 blocked/publishing 的前序阶段 → `blocked`,不调模型。后续阶段缺省沿用前序阶段的钉点
(`--pin` 可改,改了记入 notes);链的状态进入输入摘要。阶段没有任何改动时记为 `empty`(不是错误)。
`InitRecord.coverage` 是阶段的覆盖率报告,PR 正文渲染成 before/after 表。适配器 manifest 从前序 dry run 的改动或知识
仓库基点读取(`adapters/<adapter 目录名>/manifest.yaml`);`repo.language` 决定模块扫描的语言。
- `init_modules`(阶段 2):在钉点树上 `scan_modules_at_depth`(`source_roots`、`module_depth`、`min_module_loc`、
`exclude`;adapter 未声明语言或语言未知 → 不扫描,进清单)。按当前 `_routes.yaml` 算模块覆盖率。未覆盖的模块由深到浅:
已被某个 owner 部分覆盖的,**吸收**进覆盖其文件最多的 owner(只在该 owner 的 `scope_prefixes` 末尾追加模块目录;仓库
根模块因前缀不能表达,逐文件追加);完全无人覆盖的,每个模块一次有界的生成调用(文件名、符号签名、文件开头的 docstring
或注释块,字节上限;从不给函数体:签名正则匹配整行,`declaration` 截掉函数体——
花括号语言(JavaScript、Go、Rust)在整行**最早**的 `{`/`=>`/`;`/单独的 `=` 处截断,不去词法分析字符串、注释或正则字面量
(函数体不可能出现在它们之前,默认值可能被截短,这是失败即关闭的取舍);Python 在括号、字符串与行尾 `#` 注释之外的
第一个 `:` 处截断;单行函数也只留声明)写一张**地图卡片**:prose 页(`type: architecture`),目的、入口、关键文件(只保留该
模块真实存在的文件)、要读的文档(只保留文档集里的文件)、路由前缀;分节用加粗标签而非标题(知识格式把任何
`## <Word> …` 标题当作规则 ID)。卡片进入组:已有的 `components/<group>/`(入口页只追加链接,已有 owner 只追加前缀),
或新组(新目录、自己的 `_index.md` 与新的路由 owner),新组由上级入口页链接;`components/` 不存在时新建其入口页并由
仓库入口页链接;`components/` 已存在却没有入口页时组直接放在仓库目录下。`_routes.yaml` 只追加(保留开头注释块,
`routes_append_only` 校验不重排、不改名、不删除)。预算用完或超过卡片上限时,已写的卡片照常成 PR,剩下的模块进
`unfinished`;报告里列出仍无路由的模块。测试:`test_kb_init_modules.py`。

## 2026-09-30 kb init:阶段 3(deepen)与 shadow 开关
- `init_support.UpstreamPin.first_parent_changes(pin, count=, max_age_days=)`:钉点 first-parent 历史里最近 `count` 个
提交各自改动的文件(合并提交对其第一父提交),且不早于**钉点提交时间**之前 `max_age_days` 天——窗口只由钉点决定,
与时钟无关(沿 first-parent 的提交时间不一定递减:过期的提交跳过而不是就此停止;路径用 `-z` 读取,非 ASCII 文件名不被
引号转义);合并/squash 式默认分支上每个 first-parent 提交就是一个合入的 PR。这就是设计 §7.3 的 PR 窗口,从镜像离线
读取(不走 GitHub API,没有速率限制)。
- `init_deepen`(阶段 3):按窗口内源文件改动数(`make_include`:源根、`exclude`、语言后缀)给模块排序,从高到低:没有路由
的模块跳过(记入 notes);路由 owner 已承载规则的模块跳过(对指标无增益)——owner **承载规则**指 owner 页本身有生效规则,
或(仅当 owner 是入口页/prose 页时)**同一目录**下有含生效规则的页(不看子目录,否则任何组件有了规则仓库入口就算承载;
owner 本身是规则页却没有生效规则时不承载,不看兄弟页)。其余模块每个
一次生成调用:模块代码带行号、字节上限(这是深度阶段,读代码本身),产出带行范围证据的规则 → 证据必须在本模块文件内 →
共用筛查(D5、钉点证据与声明、落位、咨询性判定)→ 经 `ops.apply_operations` 追加到 owner 的规则页(owner 页本身是规则页
时用它,否则用旁边的 `rules.md`;已有规则只追加、不修改,满页转兄弟页)。每写完一个模块重算承载规则的 PR 加权覆盖率,
达到 `coverage_target`、预算用完或热模块用尽即停。报告:路由/承载规则两种覆盖率的 before/after、窗口 PR 数、仍无规则的热路径(`uncovered_hot`:没有承载规则的 owner 到达的
源文件,包括已路由但 owner 无规则的;`unrouted_hot` 另列完全无路由的)。
- 同一 PR 打开 shadow:`flip_to_shadow` 对 adapter manifest 做**文本**编辑,只改 `knowledge_lifecycle` 块里同缩进的
`enabled`(→ `true`,缺失则插在块头下)与 `mode`(→ `shadow`)两行,注释、顺序与其他行逐字保留;没有该块 → 在任何模型调用
之前 `blocked`。`_check_flip` 校验:只能改这一个 manifest,`check_flip_to_shadow`(除这两个键外解析值不变、head 开启且为
shadow),并在 head manifest 上跑 `config.parse_lifecycle`(开启、shadow、仍服务同一个知识目录)。已开启且为 shadow 时不改,
记入 notes。测试:`test_kb_init_deepen.py`。
6 changes: 5 additions & 1 deletion doc/architecture/SPEC/playbooks/PLAYBOOKS.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# playbooks/*.yaml —— 规范

<!-- verified-against: 2026-09-29 -->
<!-- verified-against: 2026-09-30 -->

`9 个文件 · 声明式编排数据 · refactor-status: ok`

Expand Down Expand Up @@ -57,3 +57,7 @@ success, steps[]`。

## kb-merge / kb-sweep / kb-activate(candidate)
知识服务的合并推进、发版巡检与快照激活;candidate 状态,由 `kb serve` 调度或 `kb run` 手动运行。

## kb-init(candidate)
为一个仓库建知识库:单步 `knowledge.init`,每次运行一个阶段(skeleton → modules → deepen + 生命周期开关 →
harvest-calibration),每阶段一个由人合并的 PR。只由 `kb init REPO --stage S` 运行,不经 `kb serve`,不写服务账本。
7 changes: 5 additions & 2 deletions doc/architecture/SPEC/profiles/establish.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

## 公开契约
`fact_id`、`build_doc_corpus`、`is_redundant`、`extract_directives`、
`scan_modules`、`scan_modules_at_depth`、`normalize_root`、`ROOT_MODULE`、`HUMAN_DOC_NAMES`。
`scan_modules`、`scan_modules_at_depth`、`normalize_root`、`ROOT_MODULE`、`HUMAN_DOC_NAMES`、`doc_files`。

## 不变量
- `is_redundant`(对 README+docs 做 6 词 shingle)会丢弃任何**仓库自己的文档已经写过**
Expand All @@ -19,6 +19,9 @@
- `scan_modules_at_depth`(kb init 的模块定义):源根以下至多 `depth` 层目录为模块,更深的文件归最深的那层祖先;
非空行少于 `min_loc` 的模块由深到浅折入父目录模块,直到稳定;源根永不折叠(`normalize_root` 先把 `.`、`./pkg`、`/pkg/` 等写法规范化,仓库根为 `""` 且包含其他根,保证折叠必然终止);嵌套源根各自拥有自己的文件;
每一层都跳过隐藏目录与非代码目录;`exclude` 按仓库相对路径做 fnmatch;未知语言返回 `{}`;结果确定、有序。
- `build_doc_corpus(..., globs=None)`:不给 `globs` 时语料与原来完全相同(README* + docs/**/*.md);给出时读
`doc_files(repo, globs)` —— 按 glob 顺序、再按路径排序,每个文件只读一次,只收普通文件,解析后逃出仓库的
符号链接一律跳过。kb init 用它按 adapter 的 `doc_globs` 建 D5 语料,归一化与 briefing 路径是同一份。

## 边界 —— 不属于这里
纯确定性 helper —— 不含 LLM、不写 store、不含 step 逻辑。
Expand All @@ -27,7 +30,7 @@
仅 stdlib。

## 测试
`test_profile_steps.py`(冗余过滤、模块扫描、指令抽取);`test_kb_init_coverage.py`(按深度的模块扫描)。
`test_profile_steps.py`(冗余过滤、模块扫描、指令抽取);`test_kb_init_coverage.py`(按深度的模块扫描);`test_kb_init_skeleton.py`(`doc_files` 与带 `globs` 的语料)。

## 重构备注
纯函数 —— 易测、易复用。冗余过滤器是承载 ETH 研究结论的关键防线;**保持它确定性**。
Expand Down
21 changes: 21 additions & 0 deletions playbooks/kb-init.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# kb init for ONE repository: one stage per run (skeleton, then modules, then
# deepen + lifecycle flip, then harvest-calibration), each opened as a
# human-merged PR (design kb-init v3). Runs outside the knowledge service: it
# never writes the service ledger. Candidate.
name: kb-init
version: 1
status: candidate
provenance:
created_by: human
source: kb init design v3 (bootstrap a repository's knowledge base)
task_kinds: [knowledge_maintenance]
repos: []
requires: []
params:
repo: "knowledge repository name"
stage: "skeleton | modules | deepen | harvest-calibration"
dry_run: "true (default): write the tree and PR body locally; false needs ALLOW_PUSH=1 and ALLOW_POST=1"
pin: "upstream commit to pin (default: the default branch head)"
steps:
- {id: init, step: knowledge.init}
success: the stage's PR is open (or its dry run is written), or the record says why it is blocked
Loading
Loading