fix: guard STEP-113 empty annotation and expose PBF blockers
Some checks failed
validate-scripts / validate-scripts (push) Has been cancelled

This commit is contained in:
tei
2026-10-11 11:50:03 +09:00
parent e2d202b560
commit c48a72a9d4
14 changed files with 726 additions and 12 deletions

View File

@@ -114,7 +114,7 @@ MAPLIBRE_VALIDATION_OUT_DIR=/tmp/my-validation \
## STEP-113 原 PBF 专用 Test App Fast Path 复测
本测试不使用 STEP-114 的简化 FAST_SMOKE。保持原 Step113PbfGateScene、PBF URL、63 个正式基础层与原 Native 诊断不变。需要用户手工完成新源码的标准 APK 编译和安装。工具先检查 build-info 中的 Git commit 与当前 HEAD、APK SHA、设备已安装 APK SHA 是否一致;任何不一致即拒绝并说明原因。
本测试不使用 STEP-114 的简化 FAST_SMOKE。保留原 Step113PbfGateScene 的 PBF URL、63 个正式基础层与 Native 真实计数。STEP-116 仅对 STATIC_RC 的空 SDK Annotation 辅助层做一次移除及生命周期保护,并增加诊断;其余四场景不做此调整。需要用户在 RDC 独立审计并锁定源码后手工完成标准 APK 编译和安装。工具检查 tracked clean、build-info Git commit 与 HEAD、APK SHA、设备安装 SHA;不一致即拒绝。原 STEP-110/111 等未跟踪任务书可以保留。
在 Mac 的 MapLibreParticleLab 根目录依序执行:
@@ -129,3 +129,9 @@ MAPLIBRE_VALIDATION_OUT_DIR=/tmp/my-validation \
前两项为用户手工 Android 构建和更新安装,不得在未授权情况下代劳。后两项由 RDC 或用户执行。脚本使用明确 ADB serial 启动原有 Activity 的 STEP-113 原场景,按当前 PID 还原 STEP113Gate 的 Base64 Logcat summary,计算实际 Fast/Full 计数差及 R Build/Reuse,保存到忽略 Git 的 artifacts/maplibre-particle/android-validation/step113-retest-时间/场景/ 目录。
FAST_OBSERVED 仅表示实际 Fast 数字增长,不代表像素等价、性能或所有安全回退已验收;NO_FAST_OBSERVED 表示仍未进入,记录完整 R fallback 和 24 Gate 聚合。STALE_APK / INSUFFICIENT_SAMPLES 不是 PASS。依次需要时用 --scenario PBF63、PARTICLE_OFF、PARTIAL_RCRR、SOURCE_CHANGE。SOURCE_CHANGE 的手动 Source mutation 仍须通过 App 原菜单做独立验证,脚本不会自行修改 Source。阶段事实详见 docs/50_Step记录/STEP-115_STEP113原PBF专用App复测准备_v1.md。
## STEP-116 当前交接
本轮只交源码,缺 `tide` 与 Symbol R 缓存准入继续 fail-closed,不能宣称原 STATIC_RC 已出现 Fast。Native AnnotationManager 在后续真实 Symbol Annotation add/update 与 Style loaded 时同步恢复缺失 points;Test App stop 重加原空层,后续 Annotation/Style 变化撤销空层验收条件,不周期删除新内容。原63层和Source定义保留,内部Annotation Source不删除;源码预期STATIC_RC由66层变65层,须以最新设备manifest核验。
复测脚本现要求至少75秒,在PID出现后等待完整请求时长;建议 `--scenario STATIC_RC --seconds 90`。同generation/lifecycle、plan/base parity、24Gate、mismatch0、计数单调及blocked撤销均为必要条件。summary新增缺源ID/Placement/commands/Source表;120帧仍需用户手动导出。精确命令、用户一次手工build/install、RDC Native定向测试及阴性结果解释见 [STEP-116 专用APP验收](../../docs/60_测试/STEP-116_STEP113原PBF专用APP验收_v1.md)。历史STEP115记录保留为当时事实,不代表本轮设备已验收。

View File

@@ -45,6 +45,9 @@ def inspect(apk, adb, serial):
info = dict(line.split("=", 1) for line in BUILD_INFO.read_text(encoding="utf-8").splitlines()
if "=" in line)
source_head = call(["git", "-C", str(ROOT), "rev-parse", "HEAD"])
tracked_changes = call(["git", "-C", str(ROOT), "status", "--porcelain", "--untracked-files=no"])
if tracked_changes:
raise RuntimeError("SOURCE_NOT_LOCKED: tracked源码有未提交变更;RDC须先独立审计并scoped提交,用户再构建最新HEAD")
if info.get("commit") != source_head:
raise RuntimeError(f"STALE_APK: build-info commit={info.get('commit')}, "
f"current HEAD={source_head}; 禁止用旧APK验收本轮修复")
@@ -76,7 +79,7 @@ def extract(logcat, pid, expected):
continue
try:
item = json.loads(found.group(2))
if item.get("encoding") != "base64-utf8" or item.get("kind") != "summary":
if item.get("encoding") != "base64-utf8" or item.get("kind") not in ("summary", "blocked"):
continue
ident = (item["exportId"], item["kind"])
record = chunks.setdefault(ident, {"count": item["chunkCount"], "items": {}})
@@ -85,13 +88,18 @@ def extract(logcat, pid, expected):
except (KeyError, ValueError, TypeError):
continue
result = []
for (ident, _), record in chunks.items():
for (ident, kind), record in chunks.items():
if len(record["items"]) != record["count"]:
continue
try:
packed = "".join(record["items"][i] for i in range(record["count"]))
sample = json.loads(base64.b64decode(packed, validate=True).decode("utf-8"))
if sample.get("scenario") == expected and sample.get("sceneReady") and not sample.get("blockedReason"):
if kind == "blocked":
sample["blockedReason"] = sample.get("reason") or "SCENE_BLOCKED"
sample["sceneReady"] = False
# Keep revoked/blocked summaries as negative evidence too; an
# earlier Fast counter must not conceal a later Annotation guard.
if sample.get("scenario") == expected:
result.append((int(ident.split("-", 1)[0]), sample))
except (KeyError, ValueError, TypeError, UnicodeError):
continue
@@ -107,8 +115,8 @@ def run():
parser.add_argument("--seconds", type=int, default=75)
parser.add_argument("--preflight-only", action="store_true")
args = parser.parse_args()
if args.seconds < 65 and not args.preflight_only:
parser.error("至少65秒以取得首次及下一次30秒周期摘要;不是FPS测量")
if args.seconds < 75 and not args.preflight_only:
parser.error("原STEP-113场景至少75秒;摘要与完整120帧导出仍须分别验收")
if not Path(args.adb).is_file():
raise RuntimeError(f"adb not found: {args.adb}")
info, local_sha, head, device = inspect(Path(args.apk), args.adb, args.serial)
@@ -136,12 +144,17 @@ def run():
if process_id is None:
raise RuntimeError("APP_PROCESS_NOT_RUNNING after launch")
print(f"APP_PID={process_id}")
time.sleep(max(0, args.seconds - 12))
# PID acquisition can take less than 12 seconds. Never subtract the
# maximum poll duration from the actual scenario observation period.
observation_started = time.monotonic()
time.sleep(args.seconds)
observation_elapsed = time.monotonic() - observation_started
raw_log = device("logcat", "-d", "-v", "brief", "-s", "STEP113Gate:I", "*:S")
output.joinpath("logcat.txt").write_text(raw_log + "\n", encoding="utf-8")
samples = extract(raw_log, process_id, args.scenario)
all_samples = extract(raw_log, process_id, args.scenario)
samples = [s for s in all_samples if s.get("sceneReady") and not s.get("blockedReason")]
if not samples:
status = "NO_READY_SUMMARY"
status = "SCENE_BLOCKED_OR_REVOKED" if any(s.get("blockedReason") for s in all_samples) else "NO_READY_SUMMARY"
fast_delta = full_delta = 0
else:
start, end = samples[0], samples[-1]
@@ -149,8 +162,20 @@ def run():
full_delta = end["fullRendered"] - start["fullRendered"]
if len(samples) < 2:
status = "INSUFFICIENT_SAMPLES"
elif any(s.get("blockedReason") for s in all_samples):
status = "SCENE_BLOCKED_OR_REVOKED"
elif any((s.get("sceneGeneration"), s.get("lifecycle")) !=
(start.get("sceneGeneration"), start.get("lifecycle")) for s in samples):
status = "SUMMARY_LIFECYCLE_CHANGED"
elif any(not s.get("planValid") or not s.get("basePreserved") for s in samples):
status = "INVALID_PLAN_OR_BASE_PARITY"
elif any(len(s.get("gateFailureCounts", [])) != 24 for s in samples):
status = "INCOMPLETE_GATE_SUMMARY"
elif any(s["decisionMismatch"] != 0 for s in samples):
status = "DECISION_MISMATCH"
elif any(b["fastRendered"] < a["fastRendered"] or b["fullRendered"] < a["fullRendered"]
for a, b in zip(samples, samples[1:])):
status = "COUNTER_RESET"
elif fast_delta > 0:
status = "FAST_OBSERVED"
else:
@@ -165,7 +190,9 @@ def run():
"scenario": args.scenario,
"sourceReport": "STEP-113 original PBF test scene; not STEP-114 smoke",
"durationSecondsRequested": args.seconds,
"durationSecondsObservedAfterPid": observation_elapsed,
"completeSummarySamples": len(samples),
"blockedSummarySamples": [s for s in all_samples if s.get("blockedReason")],
"status": status,
"fastDelta": fast_delta,
"fullDelta": full_delta,