Download scripts/admit_batch.py from weideyouni/holoaeon: direct link, hf CLI and curl.
- Browser
- Download file 15.9 kB
-
https://huggingface.co/weideyouni/holoaeon/resolve/main/scripts/admit_batch.py
- Command line
-
hf download hf://weideyouni/holoaeon/scripts/admit_batch.py
-
curl -L -o admit_batch.py https://huggingface.co/weideyouni/holoaeon/resolve/main/scripts/admit_batch.py
15.9 kB
| # -*- coding: utf-8 -*- | |
| """SOP-10 · B7 分批准入(每批 ≤5,累计 ≤15,四硬闸 + 人工签字)。 | |
| 用法: | |
| # 出票:跑四硬闸 + 生成准入票据 + 写台账(待签字) | |
| python scripts/admit_batch.py --batch B7-1 --out data/B7_准入票据_B7-1.md | |
| # 复核:只读重算(不跑闸),落票据 | |
| python scripts/admit_batch.py --check --json reports/b7_admission_check.json | |
| # 人工签字:缺签字票据不作废,但不得进执行层 | |
| python scripts/admit_batch.py --sign --sign-by "姓名" --signed-at 2026-11-02 | |
| 设计约束: | |
| 1. 候选只从 B5 池取(`data/semantic_family.json` 的 bridge_candidates + members); | |
| 不在池内 → 直接拒绝出票。 | |
| 2. 条数不补齐:票上几条就写几条,凑 5 条视为违规(A9)。 | |
| 3. 四硬闸:v3_gate / unittest / verify_skill_matrix / redline_scan。 | |
| 4. 签字是人工动作:脚本只记录「谁、何时」,不自称签字。 | |
| """ | |
| from pathlib import Path | |
| import argparse | |
| import json | |
| import os | |
| import re | |
| import subprocess | |
| import sys | |
| ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) | |
| sys.path.insert(0, ROOT) | |
| from holoaeon.fusion_upgrade import admission as ADM # noqa: E402 | |
| REPO = ROOT | |
| RULES = os.path.join(REPO, "data", "admission_rules.json") | |
| LEDGER = os.path.join(REPO, "data", "admission_ledger.csv") | |
| FAMILY = os.path.join(REPO, "data", "semantic_family.json") | |
| GATES_JSON = os.path.join(REPO, "reports", "b7_gates.json") | |
| V3_JSON = os.path.join(REPO, "reports", "gate_fusion_v4.json") | |
| CHECK_JSON = os.path.join(REPO, "reports", "b7_admission_check.json") | |
| def read_json(path): | |
| with open(path, encoding="utf-8") as f: | |
| return json.load(f) | |
| def write_json(path, doc): | |
| os.makedirs(os.path.dirname(path), exist_ok=True) | |
| with open(path, "w", encoding="utf-8", newline="\n") as f: | |
| f.write(json.dumps(doc, ensure_ascii=False, indent=2) + "\n") | |
| def run(cmd): | |
| env = dict(os.environ, PYTHONIOENCODING="utf-8") | |
| p = subprocess.run(cmd, cwd=REPO, capture_output=True, text=True, | |
| encoding="utf-8", errors="replace", env=env) | |
| return p.returncode, (p.stdout or "") + (p.stderr or "") | |
| def run_gates(): | |
| """跑四道硬闸并解析成 {gate_id: {passed,total,ok,cmd,raw_tail}}。""" | |
| out = {} | |
| for gid, cmd in ADM.HARD_GATES: | |
| rc, text = run(cmd.split()) | |
| tail = text.strip().splitlines()[-1] if text.strip() else "" | |
| passed = total = None | |
| if gid == "v3_gate": | |
| try: | |
| d = read_json(V3_JSON) | |
| passed, total = d.get("passed"), d.get("total") | |
| except Exception: | |
| pass | |
| elif gid == "unittest": | |
| m = re.search(r"Ran (\d+) tests", text) | |
| if m: | |
| passed = total = int(m.group(1)) | |
| elif gid == "skill_matrix": | |
| try: | |
| d = json.loads(text[text.index("{"):]) | |
| st = d.get("selftest", {}) | |
| passed = int(st.get("passed") or 0) | |
| total = len(st.get("detail") or []) | |
| except Exception: | |
| pass | |
| elif gid == "redline": | |
| m = re.search(r"扫描文件数:(\d+)", text) | |
| if m: | |
| passed = total = int(m.group(1)) | |
| ok = rc == 0 and bool(passed) and passed == total | |
| out[gid] = {"passed": passed, "total": total, "ok": ok, "cmd": cmd, | |
| "raw_tail": tail[:200]} | |
| return out | |
| def load_pool(): | |
| fam = read_json(FAMILY) | |
| skills = {c["skill"] for c in fam.get("bridge_candidates", [])} | |
| skills |= {m.get("skill") for m in fam.get("members", []) if m.get("skill")} | |
| return fam, skills | |
| def build(batch, out_path, skills_arg=None): | |
| fam, pool = load_pool() | |
| if skills_arg: | |
| chosen = [s.strip() for s in skills_arg.split(",") if s.strip()] | |
| else: | |
| chosen = [c["skill"] for c in fam.get("bridge_candidates", [])] | |
| outside = [s for s in chosen if s not in pool] | |
| if outside: | |
| print(json.dumps({"verdict": "REFUSED", "reason": "outside_pool", | |
| "skills": outside}, ensure_ascii=False)) | |
| return 2 | |
| if len(chosen) > ADM.MAX_PER_BATCH: | |
| print(json.dumps({"verdict": "REFUSED", "reason": "over_per_batch", | |
| "count": len(chosen)}, ensure_ascii=False)) | |
| return 2 | |
| cand = {c["skill"]: c for c in fam.get("bridge_candidates", [])} | |
| for m in fam.get("members", []): | |
| cand.setdefault(m.get("skill"), m) | |
| gates = run_gates() | |
| write_json(GATES_JSON, {"gates_id": "SG-B7-HARD-GATES-V1", "batch": batch, | |
| "evidence_class": ADM.EVIDENCE_CLASS, "gates": gates}) | |
| gate_cells = {gid: ("%s/%s" % (gates[gid]["passed"], gates[gid]["total"]) | |
| if gates[gid]["passed"] else "FAIL") for gid in ADM.GATE_IDS} | |
| rows = [] | |
| for s in chosen: | |
| c = cand.get(s, {}) | |
| rows.append({ | |
| "batch": batch, "ticket": out_path, "skill": s, | |
| "target_mechanism": c.get("target_mechanism", "semantic_bus"), | |
| "module": c.get("module", ""), "usage_class": c.get("label", ""), | |
| "gate_v3_gate": gate_cells["v3_gate"], | |
| "gate_unittest": gate_cells["unittest"], | |
| "gate_skill_matrix": gate_cells["skill_matrix"], | |
| "gate_redline": gate_cells["redline"], | |
| "signature_status": "PENDING", "signer": "", "signed_at": "", | |
| "decision": ADM.DECISION_WAITING, | |
| "note": "B5 candidate_bridge;未签字不进执行层", | |
| }) | |
| # 幂等:同批行整段替换,其它批保留 | |
| old = [] | |
| if os.path.isfile(LEDGER): | |
| old = ADM.parse_ledger(Path(LEDGER).read_text(encoding="utf-8")) | |
| kept = [r for r in old if r.get("batch") != batch] | |
| allrows = kept + rows | |
| os.makedirs(os.path.dirname(LEDGER), exist_ok=True) | |
| open(LEDGER, "w", encoding="utf-8", newline="\n").write(ADM.dump_ledger(allrows)) | |
| res = ADM.validate(allrows, pool=pool, gate_results=gates) | |
| write_ticket(out_path, batch, rows, gates, res) | |
| print(json.dumps({"batch": batch, "ticket": out_path, "rows": len(rows), | |
| "verdict": res["verdict"], "gates_ok": all(g["ok"] for g in gates.values()), | |
| "admitted": res["counts"]["admitted"], | |
| "remaining": res["counts"]["remaining"]}, ensure_ascii=False)) | |
| return 0 | |
| def write_ticket(path, batch, rows, gates, res, signature=None): | |
| g = gates | |
| ok_all = all(v["ok"] for v in g.values()) | |
| lines = [] | |
| lines.append("# B7 分批准入票据 · %s" % batch) | |
| lines.append("") | |
| lines.append("> 规则 `%s` · 每批 ≤%d · 累计 ≤%d · 四硬闸 + 人工签字" | |
| % (ADM.RULES_ID, ADM.MAX_PER_BATCH, ADM.MAX_CUMULATIVE)) | |
| lines.append("> evidence_class=`%s`(仿真/离线证据,不等于治疗证据)" % ADM.EVIDENCE_CLASS) | |
| lines.append("") | |
| lines.append("## 一、判据与来源") | |
| lines.append("") | |
| lines.append("判据取自 `holoaeon/fusion_upgrade/plan.py` B7 段与 SOP-10:") | |
| lines.append("") | |
| lines.append("1. 每批 ≤%d 个技能;" % ADM.MAX_PER_BATCH) | |
| lines.append("2. 四道硬闸全绿(v3_gate / unittest / verify_skill_matrix / redline_scan);") | |
| lines.append("3. 每批有签字人姓名与日期;缺签字 → `WAITING_SIGNATURE`,不进执行层;") | |
| lines.append("4. 累计准入 ≤%d;" % ADM.MAX_CUMULATIVE) | |
| lines.append("5. 准入票据存在(本文件)。") | |
| lines.append("") | |
| lines.append("## 二、本批候选(只从 B5 池取,不扩池、不补齐)") | |
| lines.append("") | |
| lines.append("| # | 技能 | 目标机制 | 模块 | B3 标签 | 来源状态 |") | |
| lines.append("| --- | --- | --- | --- | --- | --- |") | |
| for i, r in enumerate(rows, 1): | |
| lines.append("| %d | `%s` | %s | %s | %s | candidate_bridge |" | |
| % (i, r["skill"], r["target_mechanism"], r["module"], r["usage_class"])) | |
| lines.append("") | |
| k_count = sum(1 for r in rows if r["usage_class"] == "K") | |
| lines.append("本批 **%d 条**(≤%d,未按 5 补齐)。全部取自 B5 登记池(bridge_candidates + 族成员)," | |
| "其中 B3 标签 K(知识源)%d 条、非 K %d 条 —— 非 K 条目须人工复核后才可进执行层。" | |
| % (len(rows), ADM.MAX_PER_BATCH, k_count, len(rows) - k_count)) | |
| lines.append("") | |
| lines.append("## 三、四道硬闸实测") | |
| lines.append("") | |
| lines.append("| 闸 | 命令 | 结果 | 判定 |") | |
| lines.append("| --- | --- | --- | --- |") | |
| for gid, _cmd in ADM.HARD_GATES: | |
| v = g[gid] | |
| lines.append("| %s | `%s` | %s/%s | %s |" | |
| % (gid, v["cmd"], v["passed"], v["total"], "PASS" if v["ok"] else "FAIL")) | |
| lines.append("") | |
| lines.append("闸全绿:**%s**;闸票据 `reports/b7_gates.json`。" % ("是" if ok_all else "否")) | |
| lines.append("") | |
| lines.append("## 四、准入结论") | |
| lines.append("") | |
| lines.append("- 判定:`%s`" % (signature["decision"] if signature else res["verdict"])) | |
| lines.append("- 已准入累计:**%d / %d**(剩余 %d)" | |
| % (res["counts"]["admitted"], ADM.MAX_CUMULATIVE, res["counts"]["remaining"])) | |
| lines.append("- 未签字前,本批 %d 条技能**不进执行层**,`kernel.skills` 恒 47。" % len(rows)) | |
| lines.append("") | |
| lines.append("## 五、人工签字") | |
| lines.append("") | |
| if signature: | |
| lines.append("- 签字人:**%s**" % signature["signer"]) | |
| lines.append("- 签字日期:**%s**" % signature["signed_at"]) | |
| lines.append("- 签字后结论:`ADMITTED`(仍不计入 `kernel.skills`,只解除执行层禁令)") | |
| else: | |
| lines.append("待签字(签名与日期两项缺一不可;缺签字票据不作废但不得进执行层)。") | |
| lines.append("") | |
| lines.append("```") | |
| lines.append("签字人:____________________ 日期:____________________") | |
| lines.append("```") | |
| lines.append("") | |
| lines.append("## 六、可复现") | |
| lines.append("") | |
| lines.append("```bash") | |
| lines.append("python scripts/admit_batch.py --batch %s --out %s # 出票" % (batch, path)) | |
| lines.append("python scripts/admit_batch.py --check --json reports/b7_admission_check.json") | |
| lines.append("python scripts/admit_batch.py --sign --sign-by \"姓名\" --signed-at 2026-11-02") | |
| lines.append("python -m unittest discover -s tests_contract # 旁挂全部单测") | |
| lines.append("```") | |
| lines.append("") | |
| lines.append("## 七、维度不变自证") | |
| lines.append("") | |
| lines.append("| 维度 | 锁值 | 本批后 | 结论 |") | |
| lines.append("| --- | --- | --- | --- |") | |
| lines.append("| agents | 25 | 25 | 未动 |") | |
| lines.append("| skills | 47 | 47 | 未动(准入只改登记态,不进 `kernel.skills`) |") | |
| lines.append("| packs | 9 | 9 | 未动(未新增 pack) |") | |
| lines.append("| spec.VERSION | 0.9.1 | 0.9.1 | 未动 |") | |
| lines.append("") | |
| os.makedirs(os.path.dirname(path), exist_ok=True) | |
| open(path, "w", encoding="utf-8", newline="\n").write("\n".join(lines) + "\n") | |
| def check(json_out=None): | |
| rules_sha_ok = True | |
| if os.path.isfile(RULES): | |
| rules_sha_ok = read_json(RULES).get("sha256") == ADM.rules_doc()["sha256"] | |
| if not os.path.isfile(LEDGER): | |
| print(json.dumps({"verdict": "FAIL", "reason": "ledger_missing"}, ensure_ascii=False)) | |
| return 1 | |
| rows = ADM.parse_ledger(Path(LEDGER).read_text(encoding="utf-8")) | |
| fam, pool = load_pool() | |
| gates = read_json(GATES_JSON)["gates"] if os.path.isfile(GATES_JSON) else None | |
| tickets = {} | |
| for b in sorted({r["batch"] for r in rows}): | |
| p = next(r["ticket"] for r in rows if r["batch"] == b) | |
| if os.path.isfile(p): | |
| tickets[b] = Path(p).read_text(encoding="utf-8") | |
| res = ADM.validate(rows, pool=pool, ticket_texts=tickets, gate_results=gates) | |
| extra = [] | |
| if not rules_sha_ok: | |
| extra.append("rules_sha256_drift") | |
| payload = { | |
| "check_id": "SG-B7-ADMISSION-CHECK-V1", | |
| "rules_id": ADM.RULES_ID, | |
| "evidence_class": ADM.EVIDENCE_CLASS, | |
| "verdict": "FAIL" if extra else res["verdict"], | |
| "failed": extra + res["violations"], | |
| "checks": res["checks"], | |
| "counts": res["counts"], | |
| "rules_sha256_match": rules_sha_ok, | |
| "gates": {k: {"passed": v["passed"], "total": v["total"], "ok": v["ok"]} | |
| for k, v in (gates or {}).items()}, | |
| } | |
| if json_out: | |
| write_json(json_out, payload) | |
| print(json.dumps({k: payload[k] for k in ("verdict", "failed", "counts")}, | |
| ensure_ascii=False)) | |
| return 0 if payload["verdict"] in ("PASS", ADM.DECISION_WAITING) else 1 | |
| def sign(batch, signer, signed_at): | |
| if not signer or not signed_at: | |
| print(json.dumps({"verdict": "REFUSED", "reason": "signature_incomplete"}, | |
| ensure_ascii=False)) | |
| return 2 | |
| if not os.path.isfile(LEDGER): | |
| print(json.dumps({"verdict": "REFUSED", "reason": "ledger_missing"}, ensure_ascii=False)) | |
| return 2 | |
| rows = ADM.parse_ledger(Path(LEDGER).read_text(encoding="utf-8")) | |
| hit = [r for r in rows if r["batch"] == batch] | |
| if not hit: | |
| print(json.dumps({"verdict": "REFUSED", "reason": "batch_not_found"}, ensure_ascii=False)) | |
| return 2 | |
| if ADM.admitted_count(rows) + len(hit) > ADM.MAX_CUMULATIVE: | |
| print(json.dumps({"verdict": "REFUSED", "reason": "over_cumulative", | |
| "admitted": ADM.admitted_count(rows), "batch": len(hit)}, ensure_ascii=False)) | |
| return 2 | |
| gates = read_json(GATES_JSON)["gates"] if os.path.isfile(GATES_JSON) else None | |
| if gates and not all(v["ok"] for v in gates.values()): | |
| print(json.dumps({"verdict": "REFUSED", "reason": "hard_gate_red"}, ensure_ascii=False)) | |
| return 2 | |
| for r in rows: | |
| if r["batch"] == batch: | |
| r["signature_status"] = "SIGNED" | |
| r["signer"] = signer | |
| r["signed_at"] = signed_at | |
| r["decision"] = ADM.DECISION_ADMITTED | |
| open(LEDGER, "w", encoding="utf-8", newline="\n").write(ADM.dump_ledger(rows)) | |
| ticket = hit[0]["ticket"] | |
| fam, pool = load_pool() | |
| tickets = {} | |
| for b in sorted({r["batch"] for r in rows}): | |
| p = next(r["ticket"] for r in rows if r["batch"] == b) | |
| if os.path.isfile(p): | |
| tickets[b] = Path(p).read_text(encoding="utf-8") | |
| res = ADM.validate(rows, pool=pool, ticket_texts=tickets, gate_results=gates) | |
| batch_only = [r for r in rows if r["batch"] == batch] | |
| write_ticket(ticket, batch, batch_only, gates or {}, res, | |
| signature={"signer": signer, "signed_at": signed_at, "decision": "ADMITTED"}) | |
| print(json.dumps({"batch": batch, "signed_by": signer, "signed_at": signed_at, | |
| "admitted": res["counts"]["admitted"], | |
| "remaining": res["counts"]["remaining"]}, ensure_ascii=False)) | |
| return 0 | |
| def main(): | |
| ap = argparse.ArgumentParser() | |
| ap.add_argument("--batch", default="B7-1") | |
| ap.add_argument("--skills", default="") | |
| ap.add_argument("--out", default="") | |
| ap.add_argument("--check", action="store_true") | |
| ap.add_argument("--json", default="") | |
| ap.add_argument("--sign", action="store_true") | |
| ap.add_argument("--sign-by", default="") | |
| ap.add_argument("--signed-at", default="") | |
| a = ap.parse_args() | |
| if a.sign: | |
| return sign(a.batch, a.sign_by, a.signed_at) | |
| if a.check: | |
| return check(a.json or None) | |
| out = a.out or os.path.join(REPO, "data", "B7_准入票据_%s.md" % a.batch) | |
| return build(a.batch, out, a.skills or None) | |
| if __name__ == "__main__": | |
| sys.exit(main()) | |