- cloud/attempt_ledger.py: separate task_attempts table (not tasks columns) with per-attempt rows, MAX+1 attempt_no, append-only intent; interrupted rows kept but never counted; is_defect is explicit, never inferred from failure; first defect category wins; no-ledger tasks stay unknown (None), never backfilled. SQLite DDL is CREATE TABLE IF NOT EXISTS (idempotent); PG_DDL is the verbatim manual step for production Postgres, kept out of POSTGRES_SCHEMA on purpose (migrations coverage untouched). - cloud/server.py GET /metrics-report: aggregates the ledger into compute_metrics records; missing ledger table degrades to unknown rates, not an error. Cost dual-column code untouched. - cloud/metrics.py: docstring only (ledger is now the source); compute_metrics logic and all #238 semantics unchanged. - Tests: 16 ledger tests (store/aggregate/metrics/PG adapter evidence) + 3 endpoint tests (real rates, old task beside ledger task, interrupted status non-terminal).
145 lines
10 KiB
Markdown
145 lines
10 KiB
Markdown
# 架构一致性进 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)`,主键 `(task_id, attempt_no)`,
|
||
`attempt_no` 按任务 `MAX + 1` 递增、只追加不改写。口径:
|
||
|
||
- `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` 钉住)。
|
||
- 未做项(有意不做,原因见模块 docstring):生命周期写
|
||
入钩子未接——worker 重试/续跑循环里哪一次算一次尝试、谁
|
||
判定 `is_defect`,接错会把平台自动续跑算成尝试、把瞬时
|
||
重试算成缺陷,正好违反本轨钉住的规则;在此之前台账只
|
||
经显式 `record_attempt` 调用填充,生产率在写钩子落地前
|
||
保持诚实未知,不自动填数。读端点不建表(只读端点不改
|
||
schema),建表只发生在写路径。
|
||
|
||
## 回流与审计标准迭代(可测试规则,非口号)
|
||
|
||
`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 只是测试初值)。
|
||
台账待人工项(补缺口第 3 项之后):生产 PG 上手动执行
|
||
`task_attempts` 的 `PG_DDL` 并验证;生命周期写入钩子拍板——
|
||
哪次执行算一次尝试、谁判定 `is_defect`(在拍板前台账只经
|
||
显式调用填充,生产率保持未知)。
|