规范对齐

对照 OKF,
差距只有一个字段

逐条取证下来,291 页知识页里有 241 页缺 type。补一个键即可——不必为"合规"重构知识库

00 · 全景

四层一图:okf.py 站在哪一环

来源只读 → 引擎编译 → 流水线补齐并体检 → 各消费者读取。这张图真正要说的是最后一层:哪些字段有人读,哪些没有。

OKF 合规改造四层架构图 四层:来源层(raw 只读)→ 引擎层(sage-wiki compile 写出 summaries、concepts、entities)→ 合规层(compile.sh 流水线依次跑 okf.py --fix 注入 type 与 stale_after、build-index.py 重建索引、lint 记账含 okf.py --check 体检;注入必须早于建索引,且每轮自愈)→ 消费层(entity_type、类别、dimension、stale_after 各有实际消费者,而 okf.py 注入的 type 目前没有任何程序读取)。 来源层 RAW · 只读 raw/clippings raw/todo raw/flomo/delta raw/pdfs 编译(只读源,不回写) 引擎层 SAGE-WIKI 领地 sage-wiki compile _wiki/summaries/ 8 页 _wiki/concepts/ 211 页 · 带 entity_type _wiki/entities/ 0 页(尚未产出) 合规层 COMPILE.SH 流水线 ① okf.py --fix 注入 type 与 stale_after ② build-index.py 重建 _wiki/index.md ——不读 type,顺序是防御性的 ③ okf.py --check 合规体检 → lint 报告 引擎领地的唯一门禁 自愈:引擎重写会抹掉注入的键,下一轮 ① 再补回来(幂等,不产生空 diff) 消费层 谁真的在读 这些字段 entity_type build-index.py:71 · build-wiki-site.py:107 类别 build-wiki-site.py:230(浏览站分组配色) dimension Dataview 五维决策看板 stale_after freshness.py → lint 报告的「值得复核」 type okf.py 注入的那个 ✗ 暂无消费者 全库 grep 过:脚本、索引、检索、 三个看板,没有一处读它

窄屏可左右滑动查看完整图形

这张图要说的其实是最后两行。

前三层是机制,末层是账。okf.py 注入的 type,目前全库没有任何程序在读——build-index.py / build-wiki-site.py / indexlib / searchlib / 三个 Dataview 看板,读的都是 entity_type类别dimensiondecision_type,没有一个读 type

所以对「要不要补齐」这个问题,诚实的回答是:补它今天产出为零;真正划算的是顺带做成的 okf.py --check(引擎领地此前零校验)与 stale_after 链路——而那两件都不需要 OKF。留着 type 的唯一理由是成本≈0 的互操作期权。已在 dev-log 记了 2026-11-03 复核点:届时仍无消费者就考虑撤回注入,免得它成为一段伪装成特性的死代码。

01 · 规范

Open Knowledge Format 是什么

OKF 是 Google Cloud Knowledge Catalog 提出的中立知识格式:markdown 正文 + YAML frontmatter 表示一条知识,一个目录就是一个 bundle。它刻意做得薄。

#硬性要求本次改造
1每个非保留 .md 有合法 YAML frontmatter✅ 管
2每个 frontmatter 的 type 非空✅ 管
3index.md / log.md 结构符合 §8-9❌ 另议(见第 7 节)
保留文件名只有两个index.mdlog.md,它们不得有 frontmatter——本库早就是这么用的
消费者必须容忍未知键这条是低成本对齐的关键:本库自有的 entity_type类别decision_type 全都可以原样留着,不构成不合规
02 · 取证

取证:对照下来差多少

先克隆内容库实测,不是读代码猜。

目录页数已有 type现状字段
_wiki/concepts/2110entity_type: concept|technique|claim
_wiki/summaries/80source / source_type / compiled_at
_wiki/outputs/1510部分页已有
material/3519中文键 类别
reports/2121全有(生成时就写)
_wiki/entities/0引擎尚未产出
合计29150缺 241
不需要改目录、不需要改文件名、不需要动任何现有键。 形态上本库与 OKF 高度重合:bundle 就是目录、index.md / log.md 早就是保留文件、未知键本来就到处都是。若不取证而按直觉"对齐规范",很容易走成改名重构——那会砸掉三个现成消费者(见第 4 节)。
03 · 工具

okf.py:体检与注入

两个互斥模式:只读体检,与幂等注入。

python3 scripts/okf.py --check    # 只读体检,发现不合规页 → 非零退出
python3 scripts/okf.py --fix      # 幂等注入缺失字段
python3 scripts/okf.py --check --json
作用面(bundle 边界)
纳入_wiki/
material/
reports/
排除_wiki/under_review/ 查询原始产物,确认后才提升入库
writing/ 写作成稿,不是知识页
raw/ browse/ site/ docs/ scripts/
跳过index.md
log.md
OKF 保留文件,按规范不得有 frontmatter
原则一

确定性映射,不用 LLM

type 的值从目录 + 已有键推出,同样输入永远同样输出。

原则二

幂等

已有目标键就一个字节都不写。不幂等的话每轮编译都改文件——git 每天一堆空 diff,还会触发引擎的 reconcile churn。

原则三

只加不改

已有 type 的页一律不碰(reports 全部、outputs 十来页)。

04 · 映射

type 从哪来:确定性映射

目录决定大类,已有键提供更细的值。概念页那一行是关键:entity_type 本来就是 OKF 的 type 语义,搬过来即可。

路径type取值来源
任意目录下的 CHANGELOG.md / README.mdchangelog / readme文件名(最先判)
_wiki/concepts/concept / technique / claim直接搬 entity_type,缺失才回落 concept
_wiki/summaries/summary目录
_wiki/entities/entity目录
_wiki/outputs/decisions/decision目录(比 outputs/ 更具体,先判)
_wiki/outputs/output目录
material/material目录
reports/report目录
其余note兜底
注入后的真实分布 —— 值是真传导了,不是一律拍成 concept
143 concept 38 technique 35 material 30 claim 21 report 14 output 8 summary 1 decision 1 changelog
一页改造前后 —— 只多一行
--- + type: concept concept: 某个概念 entity_type: concept aliases: ["别名一", "别名二"] confidence: medium created_at: 2026-07-19T01:47:22Z ---
原键一个不动entity_type 有两个现成消费者(建索引、生成浏览站),类别 有 Obsidian 属性面板在读,decision_type 是决策队列状态机的输入。改名等于为了合规砸自家管线
辅助文件必须排在目录判断之前首版放在末尾,material/README.mdmaterial/ 前缀吃掉、拿到 type: material。测试抓到后追到"分支排序"这个根因,而不是给 README 打特例补丁。
05 · 接线

接进编译流水线

编译流水线从五步变六步,okf.py 占两个位置。

  1. sage-wiki compile引擎写出 summaries / concepts / entities
  2. okf.py --fix ← 新增注入 type / stale_after。必须在这里
  3. build-index.py重建 _wiki/index.md——它读 frontmatter 生成
  4. lint 记账 + okf.py --check ← 新增sage-wiki lint · 保鲜检查 · 决策队列不变量 · OKF 合规体检
  5. build-wiki-site.py本地浏览站(best-effort,失败不中断)
  6. vault.sh commit写集校验 → 提交编译产物
第 2 步早于第 3 步:防御性约定,不是数据依赖 这里更正一处早前的错误说法。本页原写「注入若发生在建索引之后,索引会读到上一轮旧字段」——实测不成立:build-index.py 只读 entity_type / concept / sources / source / 标题|title,并不读 okf 注入的 type / stale_after,顺序目前对索引没有影响。保持顺序的理由换成防御性的:一旦索引哪天开始消费 type,顺序反了会读到旧字段,而那种 bug 极难察觉。
assert i_fix < i_idx, "注入应在重建 index 之前(防御性约定)"
assert i_chk > i_idx, "体检应在 lint 记账阶段(index 之后)"
自愈,不是一次性改造源文件一变,引擎会整页重写、抹掉注入的键——但下一步就是 --fix,每轮自动重放。所以别手动补 type,跑 --fix 就行。
为什么允许触碰引擎领地Schema 规定 _wiki/{summaries,concepts,entities} 由编译引擎全权负责、人不要手改。okf.py 不是"手改",它是流水线的一环,在引擎写完之后确定性地补齐字段。
06 · 保鲜

stale_after:把保鲜机制接上 OKF

OKF 有个 stale_after 键表示"此页何时应视为过期"。本库早就有等价机制(半衰期保鲜),只是没写成 OKF 的形状——现在把两者接上。

声明半衰期适用
volatility: high30 天行业信号、新闻流蒸馏、在办事项的行动清单
volatility: medium90 天厂商/监管对比、随底层页漂移的综合产物、工具实践
volatility: low365 天变化很慢的判断
half_life_days: NN 天显式覆盖,优先于 volatility
一页的保鲜时钟
人工确认last_confirmed: 2026-07-16 —— 确认"此页仍代表现状"的日期
+ 半衰期
写回 OKF 键stale_after: 2026-08-15 —— okf.py --fix 算出
每轮 lint 复算
衰减函数0.5 ^ (距今天数 / 半衰期)
≤ 0.50列入 lint 报告的"值得复核"
≤ 0.25标急

不追踪的页不塞这个键。没声明 volatility / half_life_days 的页面一律不碰,与既有语义一致:不加字段就不参与追踪。

⚠️ 一条如实的记录

这套保鲜机制代码早就在、lint 里也跑着,但上线后很长时间零采用——全库没有一页声明过这三个字段,所以 stale_after 首次跑出来是 0 页。后来挑了 5 页真会过时的产出页打标(2 页 high、3 页 medium),链路才真正跑起来。

这是本项目反复撞见的那条:约束写在文档里等于没有约束;想让规范被遵守,就为它写可执行检查。

只挑真会过时的页,不搞全量打标复盘类页面记录的是已发生的事,不会过时;ingest 摘要是快照式存档;看板由脚本生成。保鲜报表的价值在于短到有人看,全量打标等于没打。
last_confirmed 取写作日期,不是打标当天字段语义是"上次人工确认此页仍代表现状"。没逐条核对过就盖当天,等于伪造一次人工确认,还会把首次复核平白推迟一个半衰期。
07 · 门禁

门禁:引擎领地从零到有

改造之前,写集校验的作用面里没有引擎领地——引擎写出什么都没人看。

# validate_write_set.py 改造前的作用面
LLM_SCOPE = ("_wiki/outputs/", "material/", "writing/", "reports/")
#            ↑ _wiki/summaries、concepts、entities 不在里面 = 零门禁
改造前改造后
引擎领地校验okf.py --check(非零退出)
合规页 / 总页50 / 291291 / 291
幂等复跑改动0 页
stale_after 覆盖无此键已声明保鲜的页自动写回
验收数据(真实内容库,非合成数据)
体检(改造前)  291 页,241 页待补     退出码=1
注入            291 页扫描,241 页更新
体检(改造后)  291 页,0 页待补        退出码=0
幂等第二遍      291 页扫描,0 页更新
写集校验        0 败
没做的部分OKF §8-9 规定了 index.md / log.md 的结构。本库这两个文件各有现成生成器和现成格式,强行对齐要动生成器、收益不明,留待另议。这是刻意的取舍,不是遗漏。

看系统全景:系统如何运作 → OKF 规范原文