Files
codex-app-server/docs/arch-conformance-metrics.md
host dc556b0c19 audit: lifecycle hook writes the attempt ledger (gap 3 follow-up)
- cloud/attempt_hook.py (new): canonical policy -- one counted
  dispatch = one row, is_defect always False from the worker;
  human/first rounds and human breakpoint resumes count;
  platform auto-resume reruns, coordinator fan-out, and
  pre-execution gate failures do not; transient retries inside
  one dispatch are not new attempts. Platform origin survives
  via the task_attempt_origin_markers table (same-transaction
  write on requeue, queued_at generation guard on consume).
  Defects are explicit-recorder only; corrections are
  append-only (record_correction, last-wins).
- cloud/worker.py: Worker(attempt_ledger_hook=False) + CLI
  --attempt-ledger-hook (default OFF = gap-3 behaviour exactly);
  marker write in auto_resume_interrupted, marker consume +
  classify in run_one, one ledger write at each terminal point
  (cancel branch, normal terminal block); all hook writes
  best-effort, never fail the task or block a requeue.
- cloud/attempt_ledger.py: corrects_attempt_no column (new DDL
  supersedes gap 3's pre-merge DDL) + record_correction +
  correction-aware aggregation; read path uses SELECT * so
  gap-3-era tables still read; cost code untouched.
- cloud/tests/test_attempt_hook.py: 32 tests (policy, markers,
  corrections, worker integration, mixed endpoint).
- docs/arch-conformance-metrics.md: policy, switch, PG manual
  steps (new PG_DDL + PG_MARKER_DDL), remaining user decisions.
2026-11-03 02:49:26 +00:00

226 lines
15 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 架构一致性进 CI 与指标闭环(审计加固第 5 步)
实现:`cloud/arch_conformance.py`(纯检查,标准库)、`cloud/metrics.py`
(纯计算,标准库)、端点 `GET /metrics-report`(`cloud/server.py`,
与 `/metrics` 同款认证与成员范围)。测试失败即 CI 失败:现有
`host-verify` 工作流已跑 `discover -s cloud/tests`,本轨未改任何
workflow——第 2 轨拥有 lint/类型/安扫,本轨不碰。
## 架构一致性检查(规则、理由、测试)
| 检查 | 规则 | 理由 | 测试 |
|---|---|---|---|
| 合同版本 | 冻结后版本只能 +1,且须有协调员批准的变更记录(声明向后兼容、列出受影响客户端;破坏性变更须有回归任务) | 契约静默漂移是并行线程分叉的根源;协议是唯一合法路径,检查执行协议而非只记录协议 | `test_arch_conformance.py::TestContractVersion`(正向:等值/批准+1/破坏性+回归任务;负向:未批准、无记录、跳号、不可解析) |
| 禁目录 | 任务只能写自有目录;`..` 与符号链接逃逸在解析后判定 | 跨目录写会 corrupt 他人 worktree | `TestDirectoryGuard`(正向:目录内;负向:`..`、symlink、混合写只报违规项)。口径复用已有定义,不另造:控制面 = 租户 work 目录(`worker._tenant_dirs` 的 `work`),技能项目 = `apps/<client>/` |
| 禁 import | 基础层(db/models/queue/logging_config)不 import 编排层;执行层(sandbox)不 import 持久化/编排层;除 db/backup/migrations 外任何 cloud 模块不得直接 `sqlite3.connect` 或 import `psycopg*`(服务层走 `cloud.db.connect` 或 Store) | 基础层上引是循环依赖前兆;执行器须可换、可假 runner 测试;驱动直连绕过 sqlite/postgres 唯一分发点 | `TestLayerImports`(每项正向+负向)+ 仓库级 `TestRepoLevelScan`(当前主线零违规,未来违规即 CI 失败)。有意不禁:`server.py` 的 `sqlite3.Row`/异常类型适配(那是在 `cloud.db` 连接上的适配,不是绕过;禁它会在基线误报) |
| 技术栈冻结 | G2 冻结表变更/删除须有批准的变更记录;新增客户端是新增不是换栈,通过 | 共享核心按冻结表构建,中途换栈使其静默失效 | `TestFrozenStack`(正向:不变/新增;负向:换栈/删除无批准) |
技能项目一键版:`check_skill_project` 把合同版本、冻结栈、目录写
三项合在一起验,`TestSkillProjectFixture` 用合成项目目录正向/负向
各验一次。静态 AST 扫描看不到 `importlib` 动态导入——这是已知
边界,不声称覆盖。
## 指标采集与报告
端点:`GET /metrics-report`(需 X-API-Key,成员范围)。报告格式
见 `cloud/metrics.py::build_report`:`golden_set`、`decision_ready`、
`metrics`、`thresholds` + `thresholds_source`、`feedback_actions`、
`feedback_consumers`。
指标口径(`compute_metrics`):
- 首过绿灯率 = 首过成功任务 / 已知 attempts 的终态任务;缺陷率 =
有缺陷任务 / 已知 defects 的终态任务;返工率 = attempts>1 任务 /
已知 attempts 的终态任务。终态 = completed/merged/failed/
merge-failed;`interrupted` 不算终态(平台中断自动重跑,算失败
是罚平台事件)。
- 每任务成本复用 #212 双列口径:有价 USD micros 与无价 actual
token 分列;成本不完整(无成本行或有无价 token)时
`total_usd_micros` 为 `None`,有价部分只以
`priced_usd_micros_partial` 出现——无价永不记 0、永不折进总额。
- 未知率回 `None` + `*_known: false`,绝不报 0。
当前诚实边界(端点实测,审计加固补缺口第 3 项之后):tasks 表
仍无 attempts/defects 列——台账改为独立表
`task_attempts`(`cloud/attempt_ledger.py`),不改主表结构。
端点 `GET /metrics-report` 聚合台账后传给 `compute_metrics`:
有台账行的任务算出真实首过/缺陷/返工率;无台账行的老任务
仍回 `None` + `*_known: false`,绝不被算成首过或无缺陷;
台账表尚不存在(老库、或生产 PG 未手动执行 `PG_DDL`)时
端点与无台账行同等处理,不报错。金任务集未经人工审定,
报告恒为 `pending_human_approval`、`decision_ready: false`。
## 任务尝试台账(审计加固补缺口第 3 项,选型与口径)
选型:独立台账表,不改主表。列方案查询简单(一行一任务、
无关联),但把每次尝试的历史压成两个计数器——哪次尝试失败、
属哪类缺陷、顺序如何,事后无法恢复;且在生产 Postgres 上拓
宽高频 `tasks` 表比重建一张空表更重。台账表多一次聚合查询,
换来完整尝试历史与不碰主表的迁移,取舍写在
`cloud/attempt_ledger.py` 模块 docstring 里。
表 `task_attempts(task_id, attempt_no, outcome, is_defect,
defect_category, created_at, corrects_attempt_no)`,主键
`(task_id, attempt_no)`,`attempt_no` 按任务 `MAX + 1`
递增、只追加不改写(`corrects_attempt_no` 为 NULL 时是真
尝试行,非 NULL 时是纠正行,见钩子一节)。口径:
- `outcome` ∈ success / failure / interrupted;`interrupted`
行只留档,不计入 attempts 也不计入 defects(平台中断自动
重跑是平台事件,不是 agent 失败;只有中断行的任务在聚合
里直接缺席,仍回未知而非 0)。
- `is_defect` 由记录方显式判定,绝不从 `failure` 自动推导
(失败重试未必是缺陷轮);`defect_category` 须配
`is_defect=True`,中断行永不能是缺陷,违例写时即
`ValueError`,不存歧义数据。任务的单类别标签取首个缺陷
行的类别(attempt_no 顺序,先到先得,确定性)。
- 迁移幂等:SQLite 侧 `CREATE TABLE IF NOT EXISTS`,重跑
不动已有行;Postgres 侧 `PG_DDL`(与 SQLite DDL 同文,只用
Postgres 合法类型与复合主键、无自增/identity 列,故无需
序列处理)**须在生产 PG 上手动执行并验证**(`\d
task_attempts` 或 information_schema 等价查询),且有意不
进 `POSTGRES_SCHEMA`(不静默拓宽迁移覆盖集);手动步骤
未做之前,Postgres 端点只会回未知率,不会报错。本机无
PG,真实 PG 验证未做;PG 证据目前只有适配层级别:`?` 占位
经 `cloud.db` 适配翻译为 `%s`、DDL 无需序列处理(测试
`TestPostgresAdapterEvidence` 钉住)。
- 生命周期写入钩子已接(9+ 第 2 项,见下一节):worker
按政策给计数的派发写且只写一行,开关默认关;关时台账
仍只经显式 `record_attempt` / `record_correction` 调用填
充,生产率保持诚实未知。读端点不建表(只读端点不改
schema),建表只发生在写路径;标记表同理,只在钩子开
且平台自动续跑发生时写。
## 任务尝试生命周期写入钩子(9+ 第 2 项,政策与开关)
权威政策在 `cloud/attempt_hook.py` 模块 docstring(代码、
本文档、测试三处一致,冲突时以模块为准)。要点:
- 计为一次尝试的(每派发恰好一行,`is_defect` 恒
`False`):任务首派发;人工 `/retry`(与首派发无法区分、
也不必区分,两者都算);人工 `/continue` 新消息轮次
(含开新目标循环的 goal 任务);人工断点续跑
(`/goal-resume`、旧验收 goal 的 `/continue` 断点语义)
——断点续跑算同一逻辑轮次的行,不是额外尝试:被中断
的片段只写了不计数的 `interrupted` 行,续跑终局行就是
该轮次唯一计数行;续跑再中断则仍只写 `interrupted`
行,该轮次继续保持未知。
- 不计为尝试的(不写行):平台自动续跑派发(goal 断点续
跑与非 goal 全新跑一律不计——统一口径,平台杀掉又重跑
的执行不罚 agent 质量分);协调员父派发(只做扇出聚
合、无模型回合,子任务各自写行);执行前门失败(缺
key / 仓库解析 / 配额门 / 轮次上限 / 网关准备,准入拒绝
不是尝试);未派发(预算门、无就绪任务、认领输给别的
worker,连分类都不跑)。
- 同一派发内的瞬时重试不是新尝试:`max_retries` 循环、
一次真实 fallback 换模型重跑、goal“已存在”断点回落、
后续消息排空都在一次 `run_one` 里,共享成本汇总、只
产一行(终局 outcome)。内层失败后重试成功不留失败
行;重试耗尽只留一行失败行。
- 平台识别靠标记表 `task_attempt_origin_markers`:自动续
跑在同一事务里写标记(`queued_at` = 重排队写的
`updated_at`),认领方在认领前抓排队态 `updated_at`、
认领成功后按 `queued_at` 匹配消费;人为动作(取消/
重试/继续)改写 `updated_at` 后陈旧标记自动作废,绝不
误伤后一轮人工派发;认领输了不消费,标记留给赢家。
标记写失败绝不挡重排队(退化为计数,见模块规则 5)。
- 开关:`Worker(attempt_ledger_hook=False)` / CLI
`--attempt-ledger-hook`,默认关。关时 worker 不写任何
台账行、不写标记、不建台账/标记表,与补缺口第 3 项行
为完全一致;开时才适用本节政策。开前中途(钩子关时已
重排队的任务无标记)有一次性的退化:该派发按人工/首
轮计数,之后每次重排队都有标记,即自愈。
- `is_defect` 判定主体:只有显式记录方(人工维护者或协
调员复核)在记录时判定,worker 钩子永不判定(失败 ≠
缺陷,瞬时失败尤其不是)。标错只追加纠正行
(`record_correction`:新行 `corrects_attempt_no` 指
向原尝试,旧行不动;聚合按 attempt_no 顺序后赢者通
吃;纠正行本身不计尝试、不能改 outcome、不能指中断
行/纠正行/不存在的尝试,违例写时 `ValueError`)。
维护者门槛与 `/retry` 同款(`approve_merge`),但本轨
不建标记/纠正的 HTTP 端点(函数即接口,端点是后续项,
见未做项)。
- 取舍(明写):仅经平台派发终局的任务在台账里保持未
知(无计数行),分母只含人工/首轮终局的任务——率值描
述的是“人工/首轮终局任务”,不是全部终局任务;平台重
跑本身未丢失(`auto_resume_count`、审计
`worker_auto_resumed`、时间线 `auto_resume` 行)。被拒
的替代方案是“单独标记平台行但不进分母”:分母结果相
同,但多一种行、多一列、多一套聚合排除逻辑,只换来
台账内同址可查——本轨不取。另:平台 goal 续跑与人工
goal 续跑同属一轮循环,但前者不计、后者计(标记优于
断点),这是有意的统一平台排除;若用户要把平台 goal
续跑也算作轮次完成,改 `classify_dispatch` 一行 + 测
试即可,属用户拍板项(见待人工项)。
- Postgres:标记表与台账表同款手动纪律——不进
`POSTGRES_SCHEMA`,`PG_MARKER_DDL`(与台账新 `PG_DDL`
含 `corrects_attempt_no` 列)须在生产 PG 上手动执行并
验证后,才可在 PG 控制面启用钩子;未执行时标记写失
败(best-effort),平台派发退化为计数。本机无 PG,真
实 PG 验证未做;PG 证据只有适配层(`?`→`%s` 翻译、
DDL 无自增/identity 列,测试钉住)。
## 回流与审计标准迭代(可测试规则,非口号)
`evaluate_feedback` 是纯函数,只产出 `auto_apply: false` 的行动
记录,不自动改路由、不自动改标准;行动由两处消费:
- 路由:`tighten_check`(某类缺陷率 > 阈值)→ 该类任务路由时加
一道审计门;`route_with_extra_audit`(首过率 < 下限)→ 全量加
审计门。消费动作本身须经协调员/人工确认后才生效。
- 审计标准:`tighten_check` / `recalibrate_check`(某检查的违规被
复核推翻率 > 阈值)各开一条标准复核项;标准文档本身的版本
变更走同一合同变更协议(`check_contract_version`,批准 +1),
测试 `TestGoldenSetAndStandardsLoop` 钉住“无批准改标准失败、
有批准通过”。误报复核规则:推翻率高只触发“复核阈值”行动,
绝不让检查自动放宽自己。
默认阈值(`DEFAULT_THRESHOLDS`:缺陷率 0.20、首过下限 0.50、
推翻率 0.30)只是测试用初值,报告标注
`thresholds_source: "default_unapproved"`;生产阈值须人工拍板后
以 `thresholds_source: "human_approved"` 传入,见待审定清单。
## 审计的审计:抽查口径(`cloud/audit_sampling.py`,纯函数)
审计结论本身要被人工抽查,抽查规则可测试、可核查,不留
“定期复核”口号:
- **必抽(比例 1.0)**:推倒重做案例必抽;第三次失败根因判为
接近完成、续轮仍 fail 的案例必抽。接近完成但续轮成功的、
第 1/2 次失败接近完成的,均不属必抽(阈值就是第三次)。
- **随机抽查**:其余案例按组抽取,普通案例默认 0.10,关键路径
案例默认 0.50;小数组 ceil 取整,绝不因四舍五入抽到 0。
抽取须带 seed(`select_sample` 无 seed 即报错),seed 记入
抽查计划,抽取结果可复现、可事后核查。
- **抽查结果处理**:抽查判审计正确 → 只记一条审计的审计记录,
原结论不动;判错 → 纠正原结论并重新触发相应流程:错发推回
→ `cancel_pushback_and_rerun_audit`;错发推倒 →
`halt_rebuild_and_restore`(旧实现按推倒规则保存在 git 历史,
可恢复);错放行 → `reopen_audit_and_pushback`;原结论未知
→ `escalate_to_human`,绝不编造触发动作。每条结果(无论对错)
都须记入审计的审计记录,未记入即视为未发生。
- **金任务审定清单**(`golden_approval_checklist`):任务清单人工
复核、每任务验收标准人工定义、样本排除、审定人+时间记录,
四项全 done 且至少留一个非样本任务才算批准;样本任务
(`sample=True` 或 id 以 `sample-` 开头)一律从金任务集中剔除
并计数,示例文件永远不算金任务。报告的 `sampling` 段在无抽查
计划时明写 pending,不暗示审计已被复核。
待人工项(单独列):金任务集审定清单的四项人工确认;抽查 seed
的记录与保管;生产抽查比例拍板(默认值 0.10/0.50 只是测试初值)。
台账待人工项(钩子落地之后):生产 PG 上手动执行台账新
`PG_DDL`(含 `corrects_attempt_no` 列;若曾跑过补缺口第 3
项的旧 DDL,先跑 `OLD_TABLE_ALTER`)与标记表
`PG_MARKER_DDL` 并验证(`\d task_attempts` /
`task_attempt_origin_markers` 或 information_schema 等价查询),
验证前不在 PG 控制面启用钩子;钩子政策默认值拍板——平台
goal 续跑不计(本轨默认)还是计为轮次完成、平台非 goal
全新跑不计(本轨默认)是否接受、缺陷标记/纠正的 HTTP
端点(`approve_merge` 门)是否立项;金任务集审定后报告才
可决策(现仍恒为 `pending_human_approval`)。
未做项(本轨有意不做):缺陷标记/纠正的 HTTP 端点(函数即
接口,权限检查随端点做);worker 之外的写行点(服务端
`/retry` / `/continue` / `/goal-resume` 本身不写行——写行
只发生在 worker 派发终局,服务端重排队不直接产生尝试);
真实 PG 验证(本机无 PG);生产部署(只到 PR OPEN)。