Files
codex-app-server/cloud/scorecard_baseline_store.py
T
host aba06d9a24 gapfill2: worker/sub90 coverage tests, G7 rollback branch, G5 caller-contract docs
H1 worker.py 92%->94% (277->210 miss): 35 real-assertion tests
covering goal-signal, tenant/env, followup, poll/wait,
process/watchdog and injected defensive branches; prod code 0 changes.
H2 sub-90 sweep: approval_timeout 100%, merge_gate_hard 100%,
worker_sweep 98% (only __main__ line 174 left); 14 tests.
H3 G7 rollback except:pass (server.py:6615-6616) covered by an
injected rollback-raising connection; original error still propagates,
status stays completed, retry succeeds; no semantics change.
H4 caller contract documented only (docstrings in _audit_scorecard
endpoint, check_baseline_stale, build_scorecard, baseline store):
current_main_head caller-supplied, any string as-is, no format checks,
missing/empty treated as not passed with no stale determination at the
endpoint, always 200, overall never rewritten; no new validation or
rejection, no response-structure change. 3 doc tests pin the wording.
H5 design notes (no decisions, no code changes) live in
verification/code-to10-gapfill2-design-notes.md, referenced by the
batch summary.
No bug found by the new tests; no existing assertion touched.
2026-11-04 16:20:10 +00:00

178 lines
7.6 KiB
Python
Raw 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.
"""评分卡冻结版的服务端存储(v7 机制第 5 项:基线漂移门禁)。
定位:只存“冻结的那一版”,不算分、不判 stale、不改旧分。
算分在 cloud/audit_scorecard.py 的 build_scorecard,
漂移判定在 check_baseline_stale,本模块只做冻结行的写入与读取。
键思路沿用仓库 DB 模式:ledger.py / audit_loop_driver.py 按
(module_id, contract_version) 计 slot,本表按
(rubric_version, baseline_head) 计冻结版——同一 rubric 版本在
同一 main 头上冻结的评分卡快照,是一版;main 头推进后冻结的,
是另一版,旧版仍在表里供追溯,不被覆盖。
行字段:
- rubric_version:评分口径版本(调用方传入的字符串,如 "v1";
本模块不规定口径内容、不替用户拍板阈值)。
- baseline_head:冻结时调用方传入的 main 头(字符串,非空;
接受任意字符串原样存、不做格式校验,本模块不验证其真实性,
可信来源仍是调用方责任)。
- frozen_at:冻结时间戳(秒,调用方不传时取当前时间)。
- scorecard_snapshot:build_scorecard 输出的完整 dict,以 JSON
文本存入,读回时还原为 dict。
只追加,不改删:
- 本模块只提供 ensure_schema(建表,幂等)、freeze_scorecard
(INSERT 新行)、get_frozen_scorecard / get_latest_frozen_scorecard /
list_frozen_scorecards(只读)。刻意不提供任何 UPDATE / DELETE
方法;SQLite 下另有触发器在表层挡住 UPDATE / DELETE
(写法沿用 ledger.py 的只追加触发器)。
- 不 commit:调用方在自己的事务边界提交(与 ledger.ensure_schema
同款约定);独立调用方须自己 commit,否则同连接外看不到新行。
- 真实 PG 未接通:PG 适配连接跳过 SQLite 触发器建(方言不同,
与 ledger.py 同款处理),PG 下的只追加护栏与真实 PG 验证均未做,
本项只用 SQLite / 内存库测试并明说。
- 本模块绝不在生产代码里探测版本控制状态、绝不读版本控制目录:
rubric_version / baseline_head / frozen_at 全部由调用方传入。
"""
from __future__ import annotations
import json
import time
_FROZEN_TABLE_SQL = (
"CREATE TABLE IF NOT EXISTS scorecard_baseline_freeze("
"id INTEGER PRIMARY KEY AUTOINCREMENT, "
"rubric_version TEXT NOT NULL, "
"baseline_head TEXT NOT NULL, "
"frozen_at DOUBLE PRECISION NOT NULL, "
"scorecard_snapshot TEXT NOT NULL)"
)
_FROZEN_INDEX_SQL = (
"CREATE INDEX IF NOT EXISTS scorecard_baseline_freeze_key "
"ON scorecard_baseline_freeze(rubric_version, baseline_head)"
)
# SQLite 只追加触发器(PG 不走这段,见模块 docstring)。
_SQLITE_GUARD_SQL = (
"CREATE TRIGGER IF NOT EXISTS scorecard_baseline_freeze_no_update "
"BEFORE UPDATE ON scorecard_baseline_freeze BEGIN "
"SELECT RAISE(ABORT, 'scorecard_baseline_freeze is append-only'); END",
"CREATE TRIGGER IF NOT EXISTS scorecard_baseline_freeze_no_delete "
"BEFORE DELETE ON scorecard_baseline_freeze BEGIN "
"SELECT RAISE(ABORT, 'scorecard_baseline_freeze is append-only'); END",
)
def _is_pg_conn(conn):
"""PgConnection 适配连接(触发器方言不同,须跳过 SQLite 护栏)。"""
return type(conn).__name__ == "PgConnection"
def ensure_schema(conn):
"""建冻结表 + 索引 + SQLite 只追加触发器(幂等,不 commit)。"""
conn.execute(_FROZEN_TABLE_SQL)
conn.execute(_FROZEN_INDEX_SQL)
if not _is_pg_conn(conn):
for stmt in _SQLITE_GUARD_SQL:
conn.execute(stmt)
def _require_non_empty_str(value, name):
if not isinstance(value, str) or value == "":
raise ValueError("%s must be a non-empty string" % name)
return value
def freeze_scorecard(conn, rubric_version, baseline_head,
scorecard_snapshot, frozen_at=None):
"""写入一个新冻结行(INSERT only),返回读回形态的行 dict。
scorecard_snapshot 必须是 build_scorecard 输出的 dict;不是 dict
时拒绝写入,不做任何格式猜测。调用方负责 commit。
"""
_require_non_empty_str(rubric_version, "rubric_version")
_require_non_empty_str(baseline_head, "baseline_head")
if not isinstance(scorecard_snapshot, dict):
raise ValueError("scorecard_snapshot must be a dict")
stamp = float(frozen_at) if frozen_at is not None else time.time()
payload = json.dumps(scorecard_snapshot, sort_keys=True,
ensure_ascii=False)
cur = conn.execute(
"INSERT INTO scorecard_baseline_freeze"
"(rubric_version, baseline_head, frozen_at, scorecard_snapshot) "
"VALUES (?, ?, ?, ?)",
(rubric_version, baseline_head, stamp, payload))
return {
"id": cur.lastrowid,
"rubric_version": rubric_version,
"baseline_head": baseline_head,
"frozen_at": stamp,
"scorecard_snapshot": scorecard_snapshot,
}
def _row_to_dict(row):
row_id, rubric_version, baseline_head, frozen_at, snapshot_text = row
return {
"id": row_id,
"rubric_version": rubric_version,
"baseline_head": baseline_head,
"frozen_at": frozen_at,
"scorecard_snapshot": json.loads(snapshot_text),
}
def get_frozen_scorecard(conn, rubric_version, baseline_head):
"""读某 (rubric_version, baseline_head) 的最新冻结行,无则 None。
同一键有多行时(不同 frozen_at 多次冻结)返回 id 最大的最新行,
旧行仍在表里,不被覆盖、不被删除。
"""
_require_non_empty_str(rubric_version, "rubric_version")
_require_non_empty_str(baseline_head, "baseline_head")
row = conn.execute(
"SELECT id, rubric_version, baseline_head, frozen_at, "
"scorecard_snapshot FROM scorecard_baseline_freeze "
"WHERE rubric_version = ? AND baseline_head = ? "
"ORDER BY id DESC LIMIT 1",
(rubric_version, baseline_head)).fetchone()
return _row_to_dict(row) if row is not None else None
def get_latest_frozen_scorecard(conn, rubric_version=None):
"""读最新冻结行;传 rubric_version 时只在该口径内取最新,无则 None。"""
if rubric_version is None:
row = conn.execute(
"SELECT id, rubric_version, baseline_head, frozen_at, "
"scorecard_snapshot FROM scorecard_baseline_freeze "
"ORDER BY id DESC LIMIT 1").fetchone()
else:
_require_non_empty_str(rubric_version, "rubric_version")
row = conn.execute(
"SELECT id, rubric_version, baseline_head, frozen_at, "
"scorecard_snapshot FROM scorecard_baseline_freeze "
"WHERE rubric_version = ? ORDER BY id DESC LIMIT 1",
(rubric_version,)).fetchone()
return _row_to_dict(row) if row is not None else None
def list_frozen_scorecards(conn, rubric_version=None, baseline_head=None):
"""按 id 升序列出冻结行;两参数均可选作过滤条件,不过滤时全列。"""
sql = ("SELECT id, rubric_version, baseline_head, frozen_at, "
"scorecard_snapshot FROM scorecard_baseline_freeze")
clauses, params = [], []
if rubric_version is not None:
_require_non_empty_str(rubric_version, "rubric_version")
clauses.append("rubric_version = ?")
params.append(rubric_version)
if baseline_head is not None:
_require_non_empty_str(baseline_head, "baseline_head")
clauses.append("baseline_head = ?")
params.append(baseline_head)
if clauses:
sql += " WHERE " + " AND ".join(clauses)
sql += " ORDER BY id ASC"
return [_row_to_dict(row) for row in conn.execute(sql, params).fetchall()]