codeontic
v0.14.0
Published
受本体论启发的行为建模引擎:把「系统应该怎么运转」写成机器可校验的模型并绑定到真实代码,每次 PR 零 LLM 对账,防止实现与认知漂移。
Readme
起点是一个我们答不上来的问题
一次上线评审,运维指着一个大部分由 agent 写出来的服务问:"这些代码你们还控制得住吗?"
老实说:控制不住——至少不是他说的那种控制。没人逐行读过,以后也不会有人去读。
但往回走不是答案。手写更慢,而且差距还在拉大;agent 写码这件事是单向门,这个项目直接把它当成既定前提,不再论证。所以"控制"只能换个意思:不是有人全读过,而是系统应该怎么运转,被写在了一个机器能拿来对账的地方。
多读代码解决不了这个问题
一个真实的仓库是几百个文件、几十万行。人读不完,agent 也读不准——塞不进上下文,检索回来的是片段,而片段足够让它给出一个听起来对、其实错的答案。"先把代码通读一遍"对人对 agent 都不成立。
而且真正要紧的问题也不是某个文件写得对不对,是:这套系统里有哪些会自己往前走的机器?一个请求怎么从 A 走到 B?哪里根本没人守着? 这些答案不在任何一个文件里——所以谁也查不到,所以每个人脑子里那张系统图都在悄悄过期。
办法:在代码之上再加一层
没人靠读机器码去控制一个现代系统。我们在它上面加了一层语言,然后让编译器负责让两边保持一致。这件事再做一次,往上一层:在源码之上放一层行为模型,写清系统应该怎么运转,再让一个确定性的检查器逼着代码对齐它。
这一层不是文档。文档没有办法"当场出错";模型是结构化的,绑在真实代码符号上,每次 PR 都被核对一遍。
建什么模
五种固定节点,写成 YAML,一个节点一个文件:
| | | | --- | --- | | loop(循环) | 会自己往前走的机器:状态机、轮询、重试链、渲染循环 | | flow(链路) | 一段端到端的旅程——要么由若干 loop 按执行先后串成,要么直接绑在代码上(CLI 路径、一次性管线) | | junction(交接点) | 两个东西交棒的位置,最容易出问题,所以单独记录、单独打分 | | scenario(场景) | 用业务的话写下的一条行为:给定 / 当 / 则,并指向一个真实测试 | | debt(旧账) | 看着像行为、但查下来不是的东西:死状态机、声明了却没兑现的能力 |
# .codeontic/model/loops/*.yaml —— 一个后台控制循环(示例是虚构的)
- id: L1
kind: loop
title: Order 状态机
boundary: "pending → processing → shipped/cancelled;cancelled 是终态"
owner: packages/orders
anchors: ["packages/orders/src/order-service.ts#OrderService"] # 绑定到真实代码符号
consumes_queues: ["order:process"] # 按名字对账生产和消费
scenarios: [GWT-A1-001] # GWT 场景,verified_by 指向真实测试完全没有后台循环的仓库——CLI、构建工具、一次性管线——直接给旅程建模:
# .codeontic/model/flows/*.yaml —— 自己带代码绑定的旅程
- id: C1
kind: flow
shape: anchored # 直接绑定到代码(composed 则由其他节点组成)
title: 安装一个 skill
anchors: ["src/install.ts#install"] # 跟 loop 一样的绑定方式
scenarios: [GWT-C1-001]想法从哪来。 这套设计受本体论启发:用一套固定的概念和关系,把一个领域里"有什么、彼此怎么关联"写成机器能处理的结构。上面五种节点就是一个专门描述"系统行为"的小本体,只是方向反了过来——传统本体从现实里归纳,现实永远是对的;这里的模型是规范:先写下系统应该怎么运转,再让代码来对齐。归纳出来的图只能描述现状,立成规范的模型才能审判现状。
模型才是真相源,代码只是投影
这是整件事真正颠倒的地方:模型说了算,代码只是它当前的一次投影。 而投影是可以重写、可以重新生成、可以扔掉的——只要它投出来的骨架没变。在一个由 agent 写码的仓库里,这恰好是你最想要的性质。
所以两边对不上的时候,结论不是"模型过期了",而是代码欠模型一笔账——就像编译器对它吐出的汇编有裁判权,方向从不反转。
从代码里算出来的 code graph 做不到这件事:它是代码的函数,所以代码永远"没错",它只能告诉你代码长什么样。codeontic 保留一份独立的真相源,从三个方向核对:
conformance——模型 → 代码:给每个建模行为打分met/partial/gap,缺什么点名什么。reconcile——代码 → 模型:找代码里有、模型没登记的信号(队列、定时器、轮询)。coverage——模型自查:多少节点真的绑到了代码上。
模型怎么保证自己没说谎
模型里的每一条声明,在三个地方跟真实世界扣住,每次 PR 由确定性门禁核对:
- 行为绑定到代码符号:
anchors: path#symbol; - 行为写成 GWT 场景:给定 / 当 / 则,用业务的话写;
- 场景指向真实测试:
verified_by。测试标题是带空格的句子?写{file, text}就行。
这也是它顺带把测试完善度摆出来的方式,而且是结构层面的审计:每一个建模行为——包括每一个交接点,也就是两个组件交棒的位置——要么有场景指向一个真实存在的测试,要么就被点名成缺口。真实仓库上跑一遍,浮出来的是"这些行为根本没人写过场景"和"这些场景绑的测试文件已经没了"——这两样,覆盖率百分比看不见。
机器核对的是这些东西存不存在、指的地方对不对。它不判断"这个测试是否真的测到了场景说的事"——要判断那个就得跑你的代码,而门禁永远不跑。这条线是故意画的,凡是报数字的地方都写着这句话。
拿它来做什么
- 高频、低压力地重构。 代码只是投影,怎么改都行,改完跑一遍
conformance,它告诉你骨架还在不在。骨架稳定,代码流动,甚至可以是一次性的。 - 给人做技术决策的依据。 同一件事有四份互不知道的实现、一整套写完了但生产上没人用的机制、同一种循环写法散在三个不相干的包里——这些都是待拍板的决策,而它们只有在整个系统摆到一页上时才看得见。
- 让 agent 查逻辑,而不是猜。
codeontic mcp按需提供模型切片(影响面、场景、证据),agent 回答问题靠的是模型,不是检索器碰巧捞到的那几段。 - PR 阶段校验对齐。 门禁亚秒出结果、不调任何模型,所以每次改动都跑得起——漂移在制造它的那个 PR 里就被抓住,而不是两个季度以后。
一个真实的例子
earendil-works/pi 是个公开的 agent 工具仓库:10 个 package,约 670 个 TypeScript 源文件。整个仓库建完是这样:
| | |
| --- | --- |
| 模型 | 30 loop · 19 flow · 7 junction · 4 debt · 51 scenario |
| 怎么建的 | 5 个并行 agent 会话,加一次人工合并裁决 |
| 门禁 | check --strict-anchors → exit 0,零 warning |
| 覆盖 | 绑定 52 个代码文件;最近 26 次提交里 13 次改到它们(50%) |
| 打分 | 43 met · 7 partial · 2 gap |
直接打开完整交互地图——开页就是一张全景图:所有建模节点按代码包分区,然后才是链路、建模细节,欠账放在最后。每个节点都能点开,代码链接指向 pi 在 666d897 的真实文件。它是一个自包含的 HTML 单文件,也在仓库里(docs/examples/pi-overview.html),下载后离线可开。
pi 是个健康、活跃的仓库。这样的账每个大仓都有,区别只是有没有一张图把它摆出来:
- 有三处功能,代码写完了,但生产路径上没有任何人在用。
- 一个包里有四份互相不知道的"查过期就刷新"代码。单看每份都对,放在一起就是一个要不要合并的问题。
- 同一种写法——
setTimeout的回调里再挂一个setTimeout——在三个不相干的包里各出现一次:渲染节流、SQLite 租约心跳、WebSocket 保活。只搜setInterval,一个都搜不到。
最后这条解释了为什么值得建全仓:跨包的模式,只建一个子系统永远看不见。方法和全部证据:Proposal 016。
快速开始
npx codeontic init # 生成 model 骨架 + agent 指令 + /codeontic 前门
npx codeontic check . --repo-root . --strict-anchors # 确定性门禁,亚秒出结果
npx codeontic conformance . --repo-root . # met / partial / gap 成绩单
npx codeontic overview . --repo-root . # 上面那张交互式系统地图init 写出 .codeontic/ 骨架和一套 agent 指令:你仓库里的 coding agent 照着它扫代码、起草模型。草稿由你审,没核实的不落库。
命令
| 命令 | 做什么 | 挡不挡 PR |
| --- | --- | --- |
| check | 确定性门禁:schema、引用完整、图无环、锚点存在、AST 不变量。另有两项一致性检查只报 warning:两个节点绑定同一个符号、正文引用了不存在的 id。--diff 只查增量 | 挡——唯一会挡的 |
| conformance | 给每个建模行为打分:met / partial / gap,缺什么点名什么 | 只提醒 |
| reconcile | 找代码里有、模型没登记的信号 | 只提醒 |
| coverage | 模型自查:多少节点真的绑到了代码 | 只提醒 |
| backtest | 最近 N 个改过 .ts/.tsx 的提交里,多少落在模型绑定的文件上 | 只提醒 |
| overview | 交互式系统地图:全景图 → 链路 → 建模细节 → 欠账 | — |
| graph | 整张模型的 HTML,按打分上色 | — |
| topology | 按声明的 component + 抽取到的事实画组件依赖图 | — |
| snapshot | 每晚全量扫一遍,出漂移报告 | 永不挡 |
| impact <id> | 改这里会牵动什么 | — |
| mcp | 起一个 MCP server,agent 按需查模型切片,不用通读 spec | — |
--strict-anchors 只把两种"错就是错"的检查升成 error:锚点写法不合法、绑定的文件不存在。文件还在、只是里面找不到那个符号——这种永远只是 warning:它靠全文匹配,一次正常重构就可能误伤,老误报的门禁最后会被人关掉。但它没有被扔掉:conformance 会用它,锚点失效的节点拿不到 met。门禁宽,成绩单严。
coding agent 怎么用它
不用教。init 已经把说明书放进了你的仓库:
.claude/skills/codeontic/SKILL.md——Claude Code 自动识别成/codeontic,按意图分路:发现建模(小仓单 agent;大仓走.codeontic/agent/loop-discovery-parallel.md分域并行)、跑门禁、查缺口、出图、查模型。Cursor、Codex 这类能读文件的 agent,读同一份文件照做。.codeontic/agent/三份指令——发现建模的四遍扫描法、给 PR 模板加声明栏、按你仓库的惯例配 CI。codeontic mcp——起一个 MCP server,agent 按需查模型切片(影响面、场景、证据),不用通读整个模型。
哪些行为值得建模,让 LLM 找;模型是不是还诚实,让引擎管。门禁本身不调 LLM、不联网、不执行你的代码。
要花什么
先把成本说清,这是筛选,不是劝退:
- 建模烧 token。 一个子系统(8–15 个节点)大约一次 agent 会话;上面那个 670 文件的仓库,用了 5 个并行会话加一次人工合并。
- 模型要进 git。
.codeontic/model/是一堆 YAML,一个节点一个文件。它是资产:提交、review,跟代码同等对待。生成的报告在.codeontic/ws/,用完即弃,gitignore 掉。 - 门禁不花钱。
check和conformance亚秒出结果,不调用任何模型。日常唯一的成本:代码变了,让 agent 起草模型更新,你确认。
什么仓库能用
TS/JS 仓库是一等公民。 符号级核实认 .ts .tsx .mts .cts .js .jsx .mjs .cjs;AST 不变量扫 .ts;backtest 只统计动过 .ts/.tsx 的提交。
其他语言:模型照用,校验打折。 模型本身和门禁里的结构检查(schema、引用、无环、场景、旧账)不挑语言;锚点的文件存在性也不挑语言——这恰好是 --strict-anchors 会升级的那类。打折的部分,直说:
- 符号级检查在 TS/JS 之外一律回答"判断不了"。不会误报,但也等于没查;
conformance靠它降级的那条链路,也就不会触发。crux和verified_by文本锚同样受限。 backtest会是空的:它只认.ts/.tsx提交。reconcile目前只认 TS。适配器接口是开放的,抽取器随你写,但喂给它的候选文件来自一句写死的git grep -- "*.ts"——Python、Go 的抽取器拿不到文件。
硬前提:得是 git 仓库。 facts、backtest、snapshot 都要调 git。
有些代码不值得建模。 建模有成本,只在"看不清、错不起"的地方划算:后台机制、跨组件交接、多步旅程。纯函数库(日期工具、校验器)不值,代码自己就说清了;没有交接、没有多步推进的薄 CRUD 不值;马上要删掉重写的代码更不值。一句话判断:新人光读代码,会不会理解错? 不会,就别建。
设计红线
- 门禁零 LLM、零网络、零代码执行——快、便宜、可复现。
- 缓存不参与正确性:冷跑和热跑逐字节一致。
- 快照和漂移报告永不挡 PR。
- LLM 只出草稿;维护者核实之后才能落库。
适配器:开放接口
包里不带任何适配器。你的仓库自己带一个小抽取器,放在 .codeontic/adapter/,读你这套技术栈的实现信号(队列名、定时器、轮询)。加 --strict-adapter,缺了适配器就挡 CI,而不是悄悄跳过。让 init 生成的 CI 锁定 codeontic 版本,别用 @latest——新版本不该有机会在绿灯下悄悄改变门禁的含义。
项目现在到哪了
很早期。 引擎、门禁、地图都是真的,每天在真实仓库上跑——上面那张 pi 的图是生成出来的,不是效果图——但项目还年轻,接口还在动。
- 目前一等公民是 TypeScript / JavaScript;其他语言拿得到模型和结构层门禁,打折的地方上面已经列清。
- 先拿一个子系统试,别一上来建全仓:一次 agent 会话、8–15 个节点,一个小时之内你就知道这张图有没有告诉你新东西。
- 欢迎提 issue,尤其是这两类:"模型表达不了 X"、"门禁把没问题的东西拦了"。这两种反馈直接决定下一步做什么。
想要到达的终态是:人审模型,agent 写码。
参与贡献
见 CONTRIBUTING.md:按 CI 顺序跑的本地门禁、stacked PR 的坑(squash 合并 base 会把子 PR 送错地方,而且哪里都不报错)、新增锚点字段时的扇出契约。
License
见 package.json。
