# -*- coding: utf-8 -*- """取证脚本(第 2 棒):Postiz ↔ PostSider 文件树差异量化。 输入:api/postiz.tree.json 与 api/postsider.tree.json(第 1 棒已拉,本脚本只读复用)。 输出:api/fork/fork-diff.json —— 逐路径比对结果 + 子树级统计,供分析文档引用(可复算)。 口径:GitHub tree API 的 blob `sha`(内容哈希)。 sha 相同 ⇒ 两个文件**逐字节相同**;sha 不同 ⇒ 内容不同(可能只是换牌字符串,也可能是真改动)。 ⛔ 本脚本**不判**"改动是好是坏",只给计数与清单;⛔ 也不做"改动行数"——tree API 不给行数。 """ import json, os, sys, time, collections sys.stdout.reconfigure(encoding="utf-8", errors="replace") HERE = os.path.dirname(os.path.abspath(__file__)) API = os.path.join(HERE, "api") OUT = os.path.join(API, "fork") os.makedirs(OUT, exist_ok=True) def blobs(name): with open(os.path.join(API, name), encoding="utf-8") as f: d = json.load(f) return {e["path"]: e["sha"] for e in d["tree"] if e["type"] == "blob"} P = blobs("postiz.tree.json") S = blobs("postsider.tree.json") common = set(P) & set(S) identical = sorted(k for k in common if P[k] == S[k]) content_diff = sorted(k for k in common if P[k] != S[k]) only_postiz = sorted(set(P) - set(S)) only_postsider = sorted(set(S) - set(P)) def subtree(pref): p = {k for k in P if k.startswith(pref)} s = {k for k in S if k.startswith(pref)} b = p & s return { "pref": pref, "postiz": len(p), "postsider": len(s), "common": len(b), "byte_identical": len([k for k in b if P[k] == S[k]]), "content_diff": len([k for k in b if P[k] != S[k]]), "only_postiz": len(p - s), "only_postsider": len(s - p), } PREF = [ "apps/", "apps/backend/", "apps/frontend/", "apps/orchestrator/", "apps/sdk/", "apps/commands/", "apps/extension/", "apps/mcp/", "libraries/", "libraries/nestjs-libraries/src/integrations/", "libraries/nestjs-libraries/src/database/prisma/", "libraries/helpers/src/", "libraries/react-shared-libraries/", ] social = "libraries/nestjs-libraries/src/integrations/social/" def providers(m): return sorted(k[len(social):] for k in m if k.startswith(social) and k.endswith(".provider.ts")) def cnt(m, pred): return sum(1 for k in m if pred(k)) res = { "generated_at": time.strftime("%Y-%m-%dT%H:%M:%S"), "source_trees": { "postiz": {"branch": "main", "tree_sha": "3128e870e653b1763c47392784df20f5e58cc087", "blobs": len(P)}, "postsider": {"branch": "main", "tree_sha": "d5ecaf019acf2b8b38da1768c8b27bfd376de8eb", "blobs": len(S)}, }, "totals": { "postiz_blobs": len(P), "postsider_blobs": len(S), "common_paths": len(common), "byte_identical": len(identical), "content_diff": len(content_diff), "only_postiz": len(only_postiz), "only_postsider": len(only_postsider), }, "subtrees": [subtree(p) for p in PREF], "signals": { "test_files": { "postiz_spec_ts": cnt(P, lambda k: k.endswith((".spec.ts", ".test.ts"))), "postsider_spec_ts": cnt(S, lambda k: k.endswith((".spec.ts", ".test.ts"))), }, "root_jest_configs": { "postiz": cnt(P, lambda k: k.startswith("jest.") and "/" not in k), "postsider": cnt(S, lambda k: k.startswith("jest.") and "/" not in k), }, "social_providers": { "postiz": providers(P), "postsider": providers(S), "only_postsider": sorted(set(providers(S)) - set(providers(P))), "only_postiz": sorted(set(providers(P)) - set(providers(S))), }, "prisma_migrations": { "postiz_count": cnt(P, lambda k: "/database/prisma/migrations/" in k), "postsider_count": cnt(S, lambda k: "/database/prisma/migrations/" in k), }, }, "paths": { "only_postsider_top_dirs": sorted({k.split("/")[0] for k in only_postsider}), "only_postiz_top_dirs": sorted({k.split("/")[0] for k in only_postiz}), "only_postsider_root_files": [k for k in only_postsider if "/" not in k], "content_diff_by_top": dict(collections.Counter(k.split("/")[0] for k in content_diff).most_common()), }, } with open(os.path.join(OUT, "fork-diff.json"), "w", encoding="utf-8") as f: json.dump(res, f, ensure_ascii=False, indent=1) print(json.dumps(res["totals"], ensure_ascii=False)) print(json.dumps(res["signals"]["test_files"], ensure_ascii=False)) print("DONE -> " + os.path.join(OUT, "fork-diff.json"))