CONVENTIONS.md
6.57 KB
跨切约定(Conventions)
补全各 spec 没有点明的"细节但全局"的实现约定。Coder Agent 写代码前先读这页。 本文件是所有 spec 的隐含前提——任何与下面约定冲突的具体 spec 文本以 spec 为准。
1 · 时间格式
所有 Type.String({format:'date-time'}) 字段一律 ISO 8601 with milliseconds + Z:
2026-05-13T10:31:00.000Z
- 写入:
new Date().toISOString() - 比较:字符串字典序即时序
- timeline.ndjson 例外:用 unix seconds(
number)以省字节 - runId 命名里的
YYYYMMDD-HHMM是本地时区(与服务器 TZ 一致),但事件 ts 字段始终 UTC
2 · ID 生成器
| 函数 | 格式 | 示例 | 实现 |
|---|---|---|---|
genProjectId() |
p- + 6 位 base36 |
p-731042 |
'p-' + Date.now().toString(36).slice(-6) |
genCallId() |
call-{YYYYMMDD-HHMM}-{4hex} |
call-20260513-1031-7f3a |
timestamp + crypto.randomBytes(2).toString('hex') |
genRunId(loop) |
L1: run-{YYYYMMDD-HHMM} · L2: run-int-{YYYYMMDD-HHMM} |
run-20260513-0731 |
Date formatted |
genBugId() |
bug-{YYYYMMDD-HHMM}-{4hex} |
bug-20260513-0731-7f3a |
同 genCallId 形式 |
genResumeToken() |
rt-{16hex} |
rt-7f3a91c80b2e4a6d |
'rt-' + crypto.randomBytes(8).toString('hex') |
不变量:
- ID 都是 ASCII,无空格无特殊字符(直接做文件名安全)
- 时间戳部分不保证全局唯一——靠后缀随机位避免碰撞;同一秒可以有多条
- 所有 ID 落盘后不可变(即使重命名也保留旧 ID)
3 · 错误响应(REST)
对齐 LemonBoss 既有惯例(见 server/routes/evolution.js):
res.status(<code>).json({ error: '<reason>' })
- 一律
{error: string}单字段 - error 值是人类可读的简短英文短句或reason_code(snake_case),看上下文
- 4xx 时 error 描述出错原因;5xx 时通常是
error: e.message
例外:
- 验证失败带字段名:
{error: 'missing field: company_id'} - 5xx 内部异常允许带 stack:
{error, stack}(仅 NODE_ENV=development)
不要发明 {code, message}、{error: {code, message}}、{success:false, error:...} 等其它形态——保持单字段。
4 · DocStore 内部工具
| 工具 | 签名 | 行为 |
|---|---|---|
now() |
() => string |
等价 new Date().toISOString() |
snapshot(obj) |
(obj) => Promise<string> |
JSON.stringify + sha1 → 写到 `agent-state/snapshots/{call_id}-{in\ |
dedupKey(bug) |
(bug) => string |
`${unit_id}:${test_name |
pidDir(cid, pid) |
(cid, pid) => string |
path.join(dataRoot, 'companies', cid, 'projects', pid) |
companyDir(cid) |
(cid) => string |
path.join(dataRoot, 'companies', cid) |
streamLogPath(pid) |
path.join(pidDir, 'stream.log') |
5 · 文件写入(一致性 / 原子性)
| 类型 | 工具 | 原因 |
|---|---|---|
Agent state(agent-state/*.json) |
fs.writeFileSync(p, JSON.stringify(o), {flag:'w'}) |
小文件,覆盖式原子 |
| ndjson append(timeline / bugs / changelog / stream.log) | fs.appendFileSync(p, line) |
append-only,POSIX 保证 ≤PIPE_BUF (4KB) 原子 |
| HTML/JSON 大文件(PRD/SDD/units/contract) | 写到 ${target}.tmp → fs.renameSync |
rename 是原子的,避免读到半文件 |
writeFileSync 自带 fsync 在 Linux/macOS 不保证 — 关键路径需要显式 fs.fsyncSync(fd)。但 P1-P4 范围不要求 fsync(接受偶发 power-loss 数据丢失,避免性能 30% 下降)。
6 · 并发与锁
| 资源 | 锁机制 |
|---|---|
| 同 (pid, agent) 的 callAgent | Map<key, Promise> 单飞,第二个等第一个完成 |
| 同 pid 的 runProject / runUnit | 同上 |
| 同 pid 的 onHumanFeedback drain | Map<pid, boolean> worker 标志,串行 |
| ndjson 文件 append | 依赖 OS append 原子性,无文件锁 |
| HTML 文件 write | tmp + rename,无文件锁 |
不引入 proper-lockfile 或类似库。所有协调都在内存里。
7 · SSE 事件 ID
stream.log 里每条事件的 id 字段:
- 进程内 counter,从启动时读
stream.log末行 id 作为起点(无文件则 0) - 不持久化 counter;重启后从文件末尾恢复
- counter 溢出
Number.MAX_SAFE_INTEGER才需要 wrap,实际不会触发
8 · Agent 调用上下文
callAgent(name, input) 默认注入到 system prompt 的上下文:
- Memory section(M-11):top-5 by relevance × recency
- Project facts:
{ pid, prd_rev, sdd_rev, unit_count } - Available tools:从
system/templates/_registry.json该 agent 的 toolPolicy 读
不在 input 里重复传 pid/cid——它们由 SessionPool key 隐含。Agent 通过工具读取自己的 cwd 即可。
9 · 日志策略
| 文件 | 内容 | 滚动 |
|---|---|---|
stream.log |
SSE 客户端视角,1:1 重放 | 7 天 + gzip |
timeline.ndjson |
事件视角(audit log) | 永不删 |
bugs.ndjson / changelog.ndjson |
状态机轨迹 | 永不删 |
units/{id}/coder.log |
Coder 决策日志 | 项目交付后压缩 |
units/{id}/runs/{runId}/stdout.txt |
vitest 完整输出 | 14 天后 .gz(可配) |
console.log / console.error 的逃逸:禁止。所有日志走 timelineLogger.append 或 unitLog.write。
10 · 测试约定
- 单测:vitest,每 unit 自己的
tests/ - 集成测试:
integration/spec.json驱动,Integrator 跑 - spec 里
tests:行 = 必须存在的测试断言,而非穷举(Coder 可以加更多) - spec 里没列的边角 case,Coder 自己判断要不要测
11 · 命名风格速查
| 场景 | 风格 | 例 |
|---|---|---|
| JS 函数 / 变量 | camelCase | loopController.runProject |
| JS 类 / 类型 | PascalCase | ProjectMeta, RunResult |
| 文件名 | kebab-case | loop-controller.js, bug-tracker.js |
| REST path | kebab-case | /sign-off, /devloop/projects |
| Event 名 | dot.snake_case | unit.state, agent.todolist |
| timeline.kind | snake_case | prd_drafted, agent_started |
| Bug category | snake_case | contract_mismatch, impl_bug |
| SSE event id | 数字 counter | id: 1234 |
12 · 不变量速查
- 任意
(pid, agent)同时只允许 1 个 status=running - 任意
(pid, unit)同时只允许 1 个 runUnit - 任意
pid同时只允许 1 个 onHumanFeedback worker - 任意 ndjson 文件 append 后永不修改已有行
- DocStore 写入前必须 validate;不通过抛 ValidationError
- 跨 unit 读取只允许
../{otherUnit}/contract.json,禁止读 code/