内容分四块: 1、产品规划产出 —— MCN 短视频整合营销工作台的①段五份(1a 需求/1b 竞品/1c 画像/1d 策略/1e 场景)、②段两份(2a 功能/2b 布局)、③段界面(DESIGN.md 契约与令牌表 + mcn-workbench.html 原型 + 实测/会诊/审查三份 + 23 张闸门截图)。 2、开源竞品调研 —— 5 个内容工作台项目的取证原始件与 1b 系列分析文档。 3、参考资料 —— 竞品视频抽帧 1145 张 + 2 个源视频 + 功能点截图。 4、机制侧 —— 协作脚本与状态台账、工作区记忆日志、抽帧/OCR 脚本。 .gitignore 只排运行时日志、脚本备份副本与一次性探针输出,其余按原样入库。
94 lines
3.9 KiB
Python
94 lines
3.9 KiB
Python
#!/usr/bin/env python
|
||
# -*- coding: utf-8 -*-
|
||
"""骨架单一可信源闸门:`prd/2b-界面布局.md` §二 ↔ 原型里的 FROZEN 表。
|
||
|
||
为什么单拎出来:页面里的自检只能证明「渲染出来的板块 == FROZEN 表」,
|
||
证明不了「FROZEN 表 == ②段原文」。两张表各改一边就没人发现,所以这条闸门
|
||
直接把②段那份 Markdown 当权威读,逐页逐位比。
|
||
|
||
判据:板块**数量**必须相等,**顺序**必须相等,名字**互为前缀**(②段的名字带
|
||
「(可点入)」这类注解,不是给界面抄的标题,⛔ 不逐字比)。
|
||
|
||
用法:python _tools/check_skeleton.py
|
||
"""
|
||
import argparse
|
||
import os, re, sys
|
||
|
||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||
UI = os.path.abspath(os.path.join(HERE, ".."))
|
||
GOAL = os.path.abspath(os.path.join(UI, ".."))
|
||
DOC = os.path.join(GOAL, "prd", "2b-界面布局.md")
|
||
PROTO = os.path.join(UI, "mcn-workbench.html")
|
||
|
||
Z2 = "\u3000\u3000" # 全角空格:注释切到右边界用
|
||
|
||
|
||
def from_doc(path=None):
|
||
s = open(path or DOC, encoding="utf-8").read()
|
||
if "## 二、八页骨架" not in s or "## 三、页面流转" not in s:
|
||
raise SystemExit("⛔ 读不到②段的 §二 八页骨架 —— 文件被改结构了?")
|
||
seg = s[s.index("## 二、八页骨架"):s.index("## 三、页面流转")]
|
||
out, cur = {}, None
|
||
for line in seg.split("\n"):
|
||
m = re.match(r"^### (P\d) ", line)
|
||
if m:
|
||
cur = m.group(1).lower()
|
||
out[cur] = []
|
||
continue
|
||
m2 = re.match(r"^\s*(\d+)\.\s*(.+)$", line)
|
||
if cur and m2:
|
||
out[cur].append(m2.group(2).split("——")[0].strip())
|
||
return out
|
||
|
||
|
||
def from_proto(path=None):
|
||
s = open(path or PROTO, encoding="utf-8").read()
|
||
m = re.search(r"^const FROZEN=\{(.*?)^\};", s, re.S | re.M)
|
||
if not m:
|
||
raise SystemExit("⛔ 原型里找不到 const FROZEN 表")
|
||
out = {}
|
||
# ⚠️ 逗号必须可选:表里最后一项后面没有逗号 —— 早前写成必选,p8 被静默漏掉
|
||
# (表现为「页面集合不同」,不是静默通过,但那也是漏;已修)
|
||
for k, body in re.findall(r"^\s*(p\d):\[(.*?)\],?\s*$", m.group(1), re.M):
|
||
items = re.findall(r"'([^']*)'", body)
|
||
out[k] = items[1:] # 第 0 项是页面标题
|
||
if len(out) != 8:
|
||
raise SystemExit("⛔ FROZEN 表只解析出 %d 页(应为 8):%s" % (len(out), sorted(out)))
|
||
return out
|
||
|
||
|
||
def norm(x):
|
||
return x.split("(")[0].strip()
|
||
|
||
|
||
def main():
|
||
ap = argparse.ArgumentParser(description="骨架单一可信源闸门(②段原文 ↔ 原型 FROZEN 表)")
|
||
ap.add_argument("--doc", default=None, help="②段界面布局文档(默认 prd/2b-界面布局.md)")
|
||
ap.add_argument("--proto", default=None, help="原型 HTML(默认 ui/mcn-workbench.html)")
|
||
a = ap.parse_args()
|
||
doc, proto = from_doc(a.doc), from_proto(a.proto)
|
||
bad = []
|
||
if set(doc) != set(proto):
|
||
bad.append("页面集合不同:②段 %s / 原型 %s" % (sorted(doc), sorted(proto)))
|
||
for p in sorted(set(doc) & set(proto)):
|
||
a, b = doc[p], proto[p]
|
||
if len(a) != len(b):
|
||
bad.append("%s 板块数 ②段 %d ≠ 原型 %d(②段 %s / 原型 %s)" % (p, len(a), len(b), a, b))
|
||
continue
|
||
for i, (x, y) in enumerate(zip(a, b)):
|
||
nx, ny = norm(x), norm(y)
|
||
if not (nx.startswith(ny) or ny.startswith(nx)):
|
||
bad.append("%s 第 %d 块 ②段「%s」≠ 原型「%s」" % (p, i + 1, x, y))
|
||
n = sum(len(v) for v in doc.values())
|
||
if bad:
|
||
print("FAIL 骨架与②段不一致(②段 %d 个板块):" % n)
|
||
for x in bad:
|
||
print(" · " + x)
|
||
return 1
|
||
print("PASS 骨架冻结:八页 %d 个板块,②段 2b-界面布局.md §二 与原型 FROZEN 表逐页逐位一致" % n)
|
||
return 0
|
||
|
||
|
||
if __name__ == "__main__":
|
||
sys.exit(main())
|