#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ l3_precheck_detector.py — v0 (2026-10-22) 把《派生规格必须随件上链》的五行打勾清单变成可执行检测器。 判据源:pin://12e0b5592413e10de2f3e442e892f25a5ccfc1a040c4fff25e9f9a995e962657i0 设计约束: * 纯本地、无网络。可达性只能就「证据文件里是否已带有被指内容」判定;没带就 unparsed,不猜。 * 三态 pass / fail / unparsed。unparsed ≠ fail:判不出来的行必须写明「缺哪一项证据」。 * 任一行 fail 或 unparsed → 整体结论最高只能是「L2 可复算」。 * 本器只做静态判定(五行)。L1 字节层、L2 种子层的数值复算属于另一道工序(sha256/wc -c), 检测器不越权替它打勾。 用法: python3 l3_precheck_detector.py input.json input.json: { "piece_id": "可选,覆盖件 pinId", "content": "覆盖件正文(字符串,通常是把 packet JSON 原文整个放进来)", "evidence": { "": "该指针对应的已取回正文", ... } } 输出:stdout 一份 JSON 判定表。 """ import json import hashlib import re import sys import unicodedata POINTERS_RE = re.compile(r"(pin://|metafile://)([0-9a-f]{64}i0)") POINTERISH_RE = re.compile(r"(pin://|metafile://)[0-9a-fA-F]{8,}") NO_DEP_RE = re.compile(r"无外部依赖|无外部规格|不依赖外部|no external dependency", re.I) EXTERNAL_BY_NAME_RE = re.compile(r"按[^,。;\n]{0,8}文档|参见|见文档|按技能文档|按协议实现|详见[^,。;\n]{0,10}文档") DERIV_RE = re.compile( r"seed_formula|rws_draw|抽牌|抽卡|派生式|seed\s*=|uint32|sha-?256|digest_|mod\s*\d+|%\s*\d+", re.I) HASH_CALL_RE = re.compile(r"(sha-?256|hmac|blake2)\s*\(", re.I) INT_DECODE_RE = re.compile(r"uint32|大端|小端|big.?endian|little.?endian|int\s*\([^)]*16", re.I) SLICE_RE = re.compile(r"\[\s*\d*\s*:\s*\d*\s*\]") MOD_RE = re.compile(r"mod\s*\d+|%+\s*\d+|%\s*\d+", re.I) CHAIN_RE = re.compile(r"digest_\{?[nk]\}?\s*\+?\s*1?|递推|链式|继续取", re.I) CONCAT_RE = re.compile(r"‖|\|\||\+\s*['\"]|\+\s*[A-Za-z_]\w*", re.I) NL_ONLY_RE = re.compile(r"按[^,。;\n]{0,8}文档|参见|见文档|按协议实现|按实现") VEC_EXPECT_HASH_RE = re.compile( r"(?:expect(?:ed)?(?:_value)?|期望|seed_digest(?:_sha256)?|reproduce)[^0-9a-f\n]{0,40}([0-9a-f]{64})", re.I) VEC_EXPECT_CARDS_RE = re.compile( r"(?:expect(?:ed)?|期望|牌面|cards)[^\n]{0,60}?((?:\b\d{1,2}\b[\s/、,,]+){2}\b\d{1,2}\b)") CLASS_ZERO_RE = re.compile(r"i\s*=\s*0|首值|零值|first\s+vector|初始向量", re.I) CLASS_LAST_RE = re.compile(r"末值|n\s*-\s*1|n−1|模最大|最大余数|last\s+vector|边界", re.I) CLASS_TRUNC_RE = re.compile(r"溢出|overflow|截断|truncat|2\^32|4294967296|\[0:4\]\s*(?:切|截)", re.I) Q_FIELD_RE = re.compile(r'question_Q"\s*:\s*"(.*?)"', re.S) W_FIELD_RE = re.compile(r'wallet_W"\s*:\s*"(.*?)"', re.S) CARDS_FIELD_RE = re.compile(r'"idx"\s*:\s*(\d{1,2})') def row(name, verdict, evidence=None, missing=None, note=None): return {"row": name, "verdict": verdict, "evidence": evidence or [], "missing": missing or [], "note": note or ""} def quote(text, m, width=60): s = max(0, m.start() - 10) return text[s:m.end() + width].replace("\n", " ")[:120] def detect_pointers(content): well_formed = [f"{scheme}{pid}" for scheme, pid in POINTERS_RE.findall(content)] pointerish = bool(POINTERISH_RE.search(content)) return well_formed, pointerish def row1_spec_pointer(content): pointers, pointerish = detect_pointers(content) if pointers: return row("R1 外部规格指针", "pass", evidence=[f"声明指针: {p}" for p in pointers]) if NO_DEP_RE.search(content): return row("R1 外部规格指针", "pass", evidence=["明写「无外部依赖」"], note="no-dep 声明") if pointerish: return row("R1 外部规格指针", "unparsed", missing=["完整 66 字符指针(文中出现 pin:///metafile:// 但无法解析出合法 pinId)"]) if EXTERNAL_BY_NAME_RE.search(content): m = EXTERNAL_BY_NAME_RE.search(content) return row("R1 外部规格指针", "fail", evidence=[f"仅有转述: 「{quote(content, m, 40)}」"], missing=["把「文档/技能」换成 pin:// 或 metafile:// 的完整指针,或明写无外部依赖"]) return row("R1 外部规格指针", "unparsed", missing=["规格指针声明,或「无外部依赖」声明;本件两者皆无且未提外部文档"]) def row2_pointer_reachable(content, evidence): pointers, pointerish = detect_pointers(content) if not pointers: if NO_DEP_RE.search(content): return row("R2 指针可达性", "pass", note="无外部指针,无需可达") return row("R2 指针可达性", "fail", missing=["R1 未给出指针,无可达性可言"]) hits, misses = [], [] for p in pointers: bare = p.split("://", 1)[1] val = evidence.get(p) or evidence.get(bare) or evidence.get("pin://" + bare) if val and str(val).strip(): hits.append(p) else: misses.append(p) if hits and not misses: return row("R2 指针可达性", "pass", evidence=[f"证据文件已带正文: {p}" for p in hits]) return row("R2 指针可达性", "unparsed", evidence=[f"结构合法待取: {p}" for p in misses], missing=["把指针目标正文取回并放进 evidence 再跑(本器无网络,不代取)"]) def row3_executable_formula(content): classes = [] for name, rx in (("hash调用", HASH_CALL_RE), ("整数解码", INT_DECODE_RE), ("切片", SLICE_RE), ("取模", MOD_RE), ("链式/递推", CHAIN_RE), ("拼接/计数", CONCAT_RE)): m = rx.search(content) if m: classes.append((name, quote(content, m, 40))) nl = NL_ONLY_RE.search(content) if len(classes) >= 3 and not nl: return row("R3 可执行派生式", "pass", evidence=[f"{n}: {q}" for n, q in classes]) if len(classes) >= 3 and nl: return row("R3 可执行派生式", "unparsed", evidence=[f"{n}: {q}" for n, q in classes], missing=[f"部分可执行:派生文本仍留有转述环节「{quote(content, nl, 40)}」," "该环节须写成可执行式后本行才可 pass"], note="v0 规则:原语类足够但文中残留「按…文档」转述 → 最高 unparsed,防跨环节误 pass") if nl and len(classes) <= 1: return row("R3 可执行派生式", "fail", evidence=[f"自然语言转述: 「{quote(content, nl, 40)}」"], missing=["把「按文档取流」写成输入→输出的表达式/伪代码"]) return row("R3 可执行派生式", "unparsed", evidence=[f"{n}: {q}" for n, q in classes], missing=["补齐缺失环节的可执行表达式(当前仅 %d/6 类原语可辨)" % len(classes), "典型缺口:digest 流→uint32 的完整取法"]) def row4_selfcheck_vectors(content): hashes = {h.lower() for h in VEC_EXPECT_HASH_RE.findall(content)} cards_lists = VEC_EXPECT_CARDS_RE.findall(content) n_vec = len(hashes) + len(cards_lists) cls = [] if CLASS_ZERO_RE.search(content): cls.append("零值/首值") if CLASS_LAST_RE.search(content): cls.append("末值/模最大") if CLASS_TRUNC_RE.search(content): cls.append("溢出/截断") if n_vec < 3: return row("R4 自检向量", "fail", evidence=[f"可机读向量 {n_vec} 条: 期望哈希 {sorted(hashes)}; 期望牌面 {cards_lists}"], missing=[f"至少 3 条 (输入,期望) 对,覆盖三类边界(已覆盖 {cls or '无'})"]) if len(cls) >= 3: return row("R4 自检向量", "pass", evidence=[f"向量 {n_vec} 条; 边界类 {cls}"]) return row("R4 自检向量", "unparsed", evidence=[f"向量 {n_vec} 条; 已见边界类 {cls}"], missing=["显式补齐缺失边界类的向量(" + "、".join(set(["零值/首值", "末值/模最大", "溢出/截断"]) - set(cls)) + ")"]) def eval_rws_stream(content): """v0 内置求值器。支持 FTM-RWS-0001 §3 文法: SEED = \"RWS-TAROT-v1|Q=\" + Q + \"|W=\" + W; digest_0=sha256(utf8(SEED)); digest 切 8 段×8hex 大端 uint32; c=v%78; 去重; digest_{k+1}=sha256(ascii(digest_k)).""" mq, mw = Q_FIELD_RE.search(content), W_FIELD_RE.search(content) mcards = CARDS_FIELD_RE.findall(content) if not mq or len(mcards) < 3: return None Q = mq.group(1) W = mw.group(1) if mw else "" Qn = unicodedata.normalize("NFC", Q).strip() Wn = unicodedata.normalize("NFC", W).strip() seed = "RWS-TAROT-v1|Q=" + Qn + "|W=" + Wn digest = hashlib.sha256(seed.encode("utf-8")).hexdigest() drawn, k = [], 0 while len(drawn) < 3 and k < 16: for j in range(8): v = int(digest[j * 8:(j + 1) * 8], 16) c = v % 78 if c not in drawn: drawn.append(c) if len(drawn) == 3: break if len(drawn) < 3: k += 1 digest = hashlib.sha256(digest.encode("ascii")).hexdigest() expected = [int(x) for x in mcards[:3]] return {"seed_bytes": len(seed.encode("utf-8")), "digest_0": hashlib.sha256(seed.encode("utf-8")).hexdigest(), "computed_cards": drawn, "expected_cards": expected, "match": drawn == expected, "grammar": "FTM-RWS-0001 §3 (RWS-TAROT-v1)"} def evidence_carries_spec(evidence): """证据里是否真的带有完整规格(哈希调用 + 整数解码 + 取模三类原语齐备)。""" hits = [] for pid, body in (evidence or {}).items(): if isinstance(body, str) and HASH_CALL_RE.search(body) and INT_DECODE_RE.search(body) and MOD_RE.search(body): hits.append(pid) return hits def row5_closed_form(content, evidence): res = eval_rws_stream(content) spec_holders = evidence_carries_spec(evidence) if res and res["match"]: if spec_holders: return row("R5 可闭式复算", "pass", evidence=[f"求值器执行: computed={res['computed_cards']} expected={res['expected_cards']}", f"SEED {res['seed_bytes']} B, digest_0={res['digest_0'][:16]}…", f"文法由证据承接: {', '.join(spec_holders)} ({res['grammar']})"]) return row("R5 可闭式复算", "unparsed", evidence=[f"求值器命中 computed={res['computed_cards']},但规格既不在件内也不在证据里"], missing=["命中依赖检测器内置文法(" + res["grammar"] + "),不构成该件自身的可复算性;" "把规格写进件内或放进证据(含完整 uint32 取流式)后重跑"]) if res and not res["match"]: return row("R5 可闭式复算", "fail", evidence=[f"computed={res['computed_cards']} expected={res['expected_cards']}"], missing=["核对 Q/W 规范化口径或派生式差异"]) return row("R5 可闭式复算", "unparsed", evidence=(["证据中疑似含完整规格: " + ", ".join(spec_holders)] if spec_holders else []), missing=["件内派生式不完整或不在 v0 求值器文法内;补全表达式或把规格正文放进件内/证据后重跑"]) def main(): if len(sys.argv) != 2: print(__doc__) sys.exit(2) inp = json.load(open(sys.argv[1], encoding="utf-8")) content = inp.get("content", "") evidence = inp.get("evidence", {}) or {} deriv = bool(DERIV_RE.search(content)) out = {"detector": "l3_precheck_detector.py v0", "piece_id": inp.get("piece_id", ""), "derivative_bearing": deriv} if not deriv: out["rows"] = [row("R1..R5", "unparsed", missing=["本件未检出派生字段,五行判据不适用(L1 层足够,勿外扩成教条)"])] out["overall"] = "L1-only" print(json.dumps(out, ensure_ascii=False, indent=2)) return rows = [row1_spec_pointer(content), row2_pointer_reachable(content, evidence), row3_executable_formula(content), row4_selfcheck_vectors(content), row5_closed_form(content, evidence)] out["rows"] = rows bad = [r for r in rows if r["verdict"] in ("fail", "unparsed")] out["overall"] = "L3-checkable" if not bad else "max-L2" out["blocking_rows"] = [f"{r['row']}={r['verdict']}" for r in bad] out["note"] = ("五行全过 → 该件可自称 L3 可复算;" "任一行 fail/unparsed → 最高自称 L2 可复算,须写明「payload/derivation 可复算,dependency 未上链」。") print(json.dumps(out, ensure_ascii=False, indent=2)) if __name__ == "__main__": main()