157 lines
6.2 KiB
Python
157 lines
6.2 KiB
Python
#!/usr/bin/env python3
|
|
"""M89 view -> probe -> marker coverage audit (read-only, docs-grade evidence).
|
|
|
|
Discovery is by preload: a probe is the tool/test file that preloads the view's
|
|
script. Markers are read out of the probe source, so a renamed probe is caught
|
|
instead of being silently trusted.
|
|
"""
|
|
import argparse
|
|
import pathlib
|
|
import re
|
|
import sys
|
|
|
|
ROOT = pathlib.Path(__file__).resolve().parents[1]
|
|
RUNTIME = ROOT / "runtime"
|
|
VIEWS = RUNTIME / "entry" / "views"
|
|
SEARCH_DIRS = [RUNTIME / "tools", RUNTIME / "tests"]
|
|
LEDGER = ROOT / "docs" / "EVIDENCE_LEDGER.md"
|
|
BATTLE_DRIVEN = "m60+m79+battle_ink"
|
|
BATTLE_VIEWS = {"battle_live_page_view", "battle_opening_page_view"}
|
|
|
|
|
|
def probe_for(view_stem: str):
|
|
needle = "res://entry/views/%s.gd" % view_stem
|
|
for base in SEARCH_DIRS:
|
|
for path in sorted(base.rglob("*.gd")):
|
|
if path.name.endswith(".uid"):
|
|
continue
|
|
try:
|
|
text = path.read_text(encoding="utf-8", errors="replace")
|
|
except OSError:
|
|
continue
|
|
if needle in text:
|
|
return path, text
|
|
return None, ""
|
|
|
|
|
|
def markers(text: str):
|
|
found = re.findall(r'(ABG_[A-Z0-9_]+)', text)
|
|
primaries = [m for m in found if m.endswith("_OK") and "SELF_TEST" not in m]
|
|
return primaries
|
|
|
|
|
|
LABEL_RE = re.compile(r"`(m\d+-[a-z0-9-]+)`")
|
|
ID_RE = re.compile(r"^\d{8}T\d{6}Z-\d+$")
|
|
TOKEN_RE = re.compile(r"`([^`]+)`")
|
|
|
|
|
|
def ledger_bad_ids(text: str):
|
|
"""The recurring defect class: a label used where the artifact id belongs.
|
|
|
|
Columns vary across rows, so the gate only fires on unambiguous shapes:
|
|
the same label twice in one row, or a label immediately followed by
|
|
another label. A truncated id can never pass silently because every label
|
|
occurrence is checked against its neighbour.
|
|
"""
|
|
bad = []
|
|
for number, line in enumerate(text.splitlines(), 1):
|
|
labels = LABEL_RE.findall(line)
|
|
if not labels:
|
|
continue
|
|
counts = {}
|
|
for label in labels:
|
|
counts[label] = counts.get(label, 0) + 1
|
|
for label, count in counts.items():
|
|
if count > 1:
|
|
bad.append("line %d: label repeated %d times (%s)" % (number, count, label))
|
|
tokens = TOKEN_RE.findall(line)
|
|
for index, token in enumerate(tokens[:-1]):
|
|
if LABEL_RE.fullmatch("`%s`" % token) and LABEL_RE.fullmatch("`%s`" % tokens[index + 1]):
|
|
bad.append("line %d: label %s followed by label %s" % (number, token, tokens[index + 1]))
|
|
return bad
|
|
|
|
|
|
def self_test() -> int:
|
|
"""Negative self-test for the ledger-id gate (ui_ink_check precedent)."""
|
|
bad = "| x | `m89-page-coverage` `m89-page-coverage` |"
|
|
clean = "| x | `m89-page-coverage` `20260918T060232Z-36554` |"
|
|
also_bad = "| y | `m84-notice-semantic` `m85-market-locale-copy` |"
|
|
bad_hits = ledger_bad_ids(bad)
|
|
clean_hits = ledger_bad_ids(clean)
|
|
pair_hits = ledger_bad_ids(also_bad)
|
|
if bad_hits and pair_hits and not clean_hits:
|
|
print("ABG_M89_LEDGER_SELFTEST_OK duplicate=%d pair=%d clean=0" % (len(bad_hits), len(pair_hits)))
|
|
return 0
|
|
print("ABG_M89_LEDGER_SELFTEST_FAIL duplicate=%d pair=%d clean=%d" % (
|
|
len(bad_hits), len(pair_hits), len(clean_hits)))
|
|
return 1
|
|
|
|
|
|
def styleboxflat_count(view_text: str) -> int:
|
|
return len(re.findall(r"StyleBoxFlat", view_text))
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("--matrix", help="write the refreshed matrix markdown here")
|
|
parser.add_argument("--self-test", action="store_true", help="run the ledger-id gate self-test")
|
|
args = parser.parse_args()
|
|
|
|
if args.self_test:
|
|
return self_test()
|
|
ledger = LEDGER.read_text(encoding="utf-8") if LEDGER.exists() else ""
|
|
views = sorted(p for p in VIEWS.glob("*.gd") if not p.name.endswith(".uid"))
|
|
rows, uncovered, no_marker, no_ledger = [], [], [], []
|
|
probes = set()
|
|
for view in views:
|
|
stem = view.stem
|
|
probe_path, probe_text = probe_for(stem)
|
|
view_text = view.read_text(encoding="utf-8", errors="replace")
|
|
flats = styleboxflat_count(view_text)
|
|
if stem in BATTLE_VIEWS and probe_path is None:
|
|
rows.append((stem, "driven", "-", flats, "battle ink (driver)", BATTLE_DRIVEN))
|
|
continue
|
|
if probe_path is None:
|
|
uncovered.append(stem)
|
|
rows.append((stem, "?", "-", flats, "NONE", "-"))
|
|
continue
|
|
probes.add(str(probe_path.relative_to(ROOT)))
|
|
found = markers(probe_text)
|
|
if not found:
|
|
no_marker.append(stem)
|
|
in_ledger = [m for m in found if m in ledger]
|
|
if found and not in_ledger:
|
|
no_ledger.append(stem)
|
|
rows.append((stem, "shared", str(probe_path.relative_to(ROOT)), flats,
|
|
found[0] if found else "NO_MARKER",
|
|
"ledger" if in_ledger else "not_in_ledger"))
|
|
|
|
if args.matrix:
|
|
out = ["# M62 页面一致性矩阵(M89 刷新)", "",
|
|
"> 由 `tools/m89_page_coverage.py --matrix` 生成;StyleBoxFlat 数为视图源码 grep 实测。", "",
|
|
"| 视图 | chrome 族 | 探针 | 页面私有 StyleBoxFlat | marker | 账本 |",
|
|
"| --- | --- | --- | --- | --- | --- |"]
|
|
for row in rows:
|
|
out.append("| `%s` | %s | `%s` | %d | `%s` | %s |" % row)
|
|
pathlib.Path(args.matrix).write_text("\n".join(out) + "\n", encoding="utf-8")
|
|
|
|
bad_ids = ledger_bad_ids(ledger)
|
|
ok = not uncovered and not no_marker and not no_ledger and not bad_ids
|
|
detail = "uncovered=%s markers_missing=%s ledger_missing=%s" % (
|
|
",".join(uncovered) or "none", ",".join(no_marker) or "none", ",".join(no_ledger) or "none")
|
|
if bad_ids:
|
|
detail = "%s ledger_bad_ids=%d" % (detail, len(bad_ids))
|
|
if ok:
|
|
print("ABG_M89_PAGE_COVERAGE_OK views=%d probes=%d uncovered=none battle=driven:%s" % (
|
|
len(views), len(probes), BATTLE_DRIVEN))
|
|
else:
|
|
print("ABG_M89_PAGE_COVERAGE_FAIL views=%d probes=%d %s" % (len(views), len(probes), detail))
|
|
print("ABG_M89_PAGE_COVERAGE_DETAIL " + detail)
|
|
for problem in bad_ids:
|
|
print("ABG_M89_PAGE_COVERAGE_BADID " + problem)
|
|
return 0 if ok else 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|