CONVENTIONS.md 6.78 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 的上下文:

  1. Memory section(M-11):top-5 by relevance × recency
  2. Project facts:{ pid, prd_rev, sdd_rev, unit_count }
  3. 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, scope) 同时只允许 1 个 status=running(scope 缺省时退化为旧不变量;27 号 P4 §4.2 起,图引擎并发调用同一 agent 时会传 scope=gr:{nodeRunId} 多加一维,避免同一 run 内两个并发节点撞会话键)
  • 任意 (pid, unit) 同时只允许 1 个 runUnit
  • 任意 pid 同时只允许 1 个 onHumanFeedback worker
  • 任意 ndjson 文件 append 后永不修改已有行
  • DocStore 写入前必须 validate;不通过抛 ValidationError
  • 跨 unit 读取只允许 ../{otherUnit}/contract.json,禁止读 code/