holoaeon / scripts /check_contract_draft.py
weideyouni's picture
B1a: scripts/check_contract_draft.py
c7a62c9 verified
Raw History Blame Contribute Delete
5.14 kB
"""B1a 契约字段草案校验器(纯标准库 / 离线 / 只读)。
用法:
python scripts/check_contract_draft.py # 打印 JSON 结果,退出码 0/1
python scripts/check_contract_draft.py --path <draft.md>
判据(对应 data/contract_v1_draft.md §六):
C1 顶层 6 必填 + 4 可选 = 10 行齐,每行类型/约束/示例非空
C2 payload 白名单 6 键齐,每键类型与约束非空
C3 三条口径决定 D1–D3 在案
C4 两条示例块均为合法 JSON
不做的事:不写文件、不联网、不调 git、不 import 非标准库。
"""
import hashlib
import json
import os
import sys
REPO = os.environ.get("HOLOAEON_REPO", os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
DEFAULT_DRAFT = os.path.join(REPO, "data", "contract_v1_draft.md")
TOP_FIELDS = ["contract_id", "source", "target", "skill", "mechanism", "lane", "payload", "nonce", "budget", "note"]
PAYLOAD_KEYS = ["domain", "inputs", "outputs", "evidence", "redlines", "notes"]
DECISIONS = ["D1", "D2", "D3"]
def read_text(path):
with open(path, encoding="utf-8") as f:
return f.read()
def row_for(text, name):
"""定位 '| `name` | ...' 表格行,返回单元格列表;找不到返回 None。"""
token = "| `" + name + "` |"
for line in text.splitlines():
if line.strip().startswith(token):
cells = [c.strip() for c in line.strip().strip("|").split("|")]
return cells
return None
def row_ok(cells, want):
if not cells or len(cells) < want:
return False, "cells={} want>={}".format(len(cells) if cells else 0, want)
for c in cells[2:want]:
if not c:
return False, "empty cell"
return True, "ok"
def json_blocks(text):
blocks = []
cur = None
for line in text.splitlines():
stripped = line.strip()
if stripped == "```json":
cur = []
continue
if cur is not None and stripped == "```":
blocks.append("\n".join(cur))
cur = None
continue
if cur is not None:
cur.append(line)
return blocks
def check(path):
checks = []
def add(cid, ok, detail):
checks.append({"id": cid, "ok": bool(ok), "detail": detail})
if not os.path.isfile(path):
add("C0", False, "draft missing: " + path)
return checks
text = read_text(path)
# C1 顶层 10 行
miss, bad = [], []
for name in TOP_FIELDS:
cells = row_for(text, name)
ok, detail = row_ok(cells, 5)
if cells is None:
miss.append(name)
elif not ok:
bad.append(name + ":" + detail)
add("C1", not miss and not bad,
"rows={} missing={} bad={}".format(len(TOP_FIELDS) - len(miss), miss, bad))
# C2 payload 6 键
miss2, bad2 = [], []
for key in PAYLOAD_KEYS:
cells = row_for(text, key)
ok, detail = row_ok(cells, 5)
if cells is None:
miss2.append(key)
elif not ok:
bad2.append(key + ":" + detail)
add("C2", not miss2 and not bad2,
"keys={} missing={} bad={}".format(len(PAYLOAD_KEYS) - len(miss2), miss2, bad2))
# C3 三条口径
found = [d for d in DECISIONS if d in text]
add("C3", len(found) == len(DECISIONS), "decisions={}".format(found))
# C4 示例 JSON 可解析
blocks = json_blocks(text)
parsed, errs, objs = 0, [], []
for i, b in enumerate(blocks):
if '"contract_id"' not in b:
continue
try:
obj = json.loads(b)
if isinstance(obj, dict) and "contract_id" in obj:
parsed += 1
objs.append(obj)
except Exception as exc:
errs.append("block{}:{}".format(i, type(exc).__name__))
add("C4", parsed >= 2 and not errs, "parsed={} errors={}".format(parsed, errs))
# C5 示例指纹可复算(同参同指纹,见草案 §四)
bad_ids = []
for obj in objs:
body = {}
for k in ("source", "target", "skill", "mechanism", "lane", "payload"):
if k in obj:
body[k] = obj[k]
canon = json.dumps(body, sort_keys=True, separators=(",", ":"), ensure_ascii=False)
want = hashlib.sha256(canon.encode("utf-8")).hexdigest()[:12]
if obj.get("contract_id") != want:
bad_ids.append("{}!={}".format(obj.get("contract_id"), want))
add("C5", not bad_ids, "recomputed={} mismatch={}".format(len(objs), bad_ids))
return checks
def main(argv):
path = DEFAULT_DRAFT
if "--path" in argv:
idx = argv.index("--path")
if idx + 1 < len(argv):
path = argv[idx + 1]
checks = check(path)
verdict = "PASS" if all(c["ok"] for c in checks) else "FAIL"
payload = {
"batch": "B1a",
"draft": path,
"verdict": verdict,
"failed": [c["id"] for c in checks if not c["ok"]],
"checks": checks,
}
print(json.dumps(payload, ensure_ascii=False, indent=2))
return 0 if verdict == "PASS" else 1
if __name__ == "__main__":
sys.exit(main(sys.argv))