From e3677bd2a18b97d09b8b26e9b8218c9f057c4609 Mon Sep 17 00:00:00 2001 From: mes123456 Date: Mon, 7 Sep 2026 08:41:30 +0800 Subject: [PATCH] docs(web): publish detailed feature completeness audit --- docs/WEB_FEATURE_COMPLETENESS_AUDIT.md | 25 +++ my-tools/show_blender_web_scheduler.py | 9 + .../WBV2/feature-completeness-audit.json | 207 ++++++++++++++++++ tools/web/audit-wbv2-feature-completeness.mjs | 80 +++++++ 4 files changed, 321 insertions(+) create mode 100644 docs/WEB_FEATURE_COMPLETENESS_AUDIT.md create mode 100644 tests/golden/WBV2/feature-completeness-audit.json create mode 100644 tools/web/audit-wbv2-feature-completeness.mjs diff --git a/docs/WEB_FEATURE_COMPLETENESS_AUDIT.md b/docs/WEB_FEATURE_COMPLETENESS_AUDIT.md new file mode 100644 index 000000000..92f608260 --- /dev/null +++ b/docs/WEB_FEATURE_COMPLETENESS_AUDIT.md @@ -0,0 +1,25 @@ +# Blender Web 功能移植完整盘点 + +机器可读结果见 [`feature-completeness-audit.json`](../tests/golden/WBV2/feature-completeness-audit.json)。审计把“实现完成”“路由接线”和“行为证据”分开统计。 + +## 当前结论 + +- 产品工作流:16/16 模块、34/34 产品任务已实现。 +- 源码路由:3745/3745 单元已分类,`UNAVAILABLE=0`。 +- 路由分布:WEB_MAIN=1698、SERVER_NATIVE=1613、BROWSER_EQUIVALENT=434。 +- 行为证据:1025 已验证、2720 未逐项验证。未验证不等于缺少入口,但不能宣称逐单元完成。 +- 一次性烟测:34/34 已执行且没有重试;31 个初始通过,2 个定点修复通过,1 个 IO 构件问题仍阻断。 + +## 真正缺失或阻断 + +1. **IO WASM 构件阻断**:`importScene/exportScene` 已存在于 C++/协议/Worker/前端接线,但签入 WASM 不包含该 unified JSON 路由。重建时发现 WebEngine 源码仍有 Blender API/header 漂移,未替换现有构件。 +2. **行为证据缺口**:1934 个 operator、673 个 RNA struct、58 个 operator macro、2 个 modifier source 尚无精确行为 receipt。它们已有明确 WEB_MAIN、SERVER_NATIVE 或 BROWSER_EQUIVALENT 路由,不应伪造为已逐项验证。 + +## 后续业务顺序 + +1. 修复 WebEngine 源码/API 漂移,重建并 SHA-256 绑定 source-matched WASM。 +2. 仅重跑 `OPERATOR-ROUTE-PALETTE` 的 IO focused smoke;已通过的 33 个功能不重跑。 +3. 按 operator/RNA/macro/modifier 家族建立批次化行为证据;每批复用已有 fixture 和 WASM,增量更新 verified overlay。 +4. 用户报告缺陷时建立新聚焦任务,不修改历史通过证据。 + +禁止事项:不以 JS 模拟替代 Blender Main/WASM,不把路由数量当作行为完成率,不重复执行已通过的烟测。 diff --git a/my-tools/show_blender_web_scheduler.py b/my-tools/show_blender_web_scheduler.py index 00f84da9a..086b648e3 100755 --- a/my-tools/show_blender_web_scheduler.py +++ b/my-tools/show_blender_web_scheduler.py @@ -149,6 +149,7 @@ def project_progress() -> dict: route_catalog = _read_json(ROOT / "web/app/src/capabilities/blender-feature-route-catalog.json") product_backlog = _read_json(CONTROL_ROOT / "web-product-backlog.json") product_status = _read_json(CONTROL_ROOT / "web-product-migration-status.json") + completeness_audit = _read_json(ROOT / "tests/golden/WBV2/feature-completeness-audit.json") total = int(inventory.get("summary", {}).get("unitCount", 0) or 0) verified = int(inventory.get("coverage", {}).get("verifiedUnits", 0) or 0) unverified = int(inventory.get("coverage", {}).get("unverifiedUnits", max(0, total - verified)) or 0) @@ -287,6 +288,13 @@ def project_progress() -> dict: }, "featureRouteCoverage": feature_route_coverage, "featureRouteGaps": feature_route_gaps, + "completenessAudit": { + "status": completeness_audit.get("status", "MISSING"), + "implementationTasks": completeness_audit.get("implementation", {}).get("productTasks", {}), + "behavioralEvidence": completeness_audit.get("behavioralEvidence", {}), + "smoke": completeness_audit.get("smoke", {}), + "actualMissingOrBlocked": completeness_audit.get("actualMissingOrBlocked", []), + }, "productMigration": { **product_status, "strategy": product_backlog.get("strategy"), @@ -430,6 +438,7 @@ def format_progress(progress: dict, fast_path: dict | None = None) -> list[str]: f" implementation tasks: {product.get('taskImplemented', 0)}/{product.get('taskTotal', 0)} done, active={','.join(product.get('taskActive', [])) or 'none'}, pending={product.get('taskPending', 0)}, blocked={','.join(product.get('taskBlocked', [])) or 'none'}", f" implementation estimate: {product_estimate if product_estimate is not None else 'learning'} minutes remaining; verification={product.get('verificationPolicy') or 'deferred'}", f" source traceability reference: {progress.get('verifiedUnits', 0)}/{progress.get('totalUnits', 0)} bound; excluded from product completion denominator", + f" completeness audit: {progress.get('completenessAudit', {}).get('status', 'MISSING')} blocked={sum(item.get('status') == 'BLOCKED' for item in progress.get('completenessAudit', {}).get('actualMissingOrBlocked', []))} behavioral-unverified={progress.get('completenessAudit', {}).get('behavioralEvidence', {}).get('unverifiedUnits', 0)}", f" task plan: {plan.get('done', 0)}/{plan.get('tasks', 0)} done, active={','.join(plan.get('active', [])) or 'none'}, blocked={plan.get('blocked', 0)}", f" old verification estimate: remaining={estimate_text}, total={total_text} (reference only; strategy retired)", f" observed bottleneck: stage={progress.get('bottleneck', {}).get('stage') or 'n/a'} median={progress.get('bottleneck', {}).get('medianDurationMs') or 0}ms command={progress.get('bottleneck', {}).get('label') or 'n/a'}", diff --git a/tests/golden/WBV2/feature-completeness-audit.json b/tests/golden/WBV2/feature-completeness-audit.json new file mode 100644 index 000000000..9631a4138 --- /dev/null +++ b/tests/golden/WBV2/feature-completeness-audit.json @@ -0,0 +1,207 @@ +{ + "schemaVersion": 1, + "auditId": "WBV2-FEATURE-COMPLETENESS-001", + "operation": "DETAILED_MIGRATION_COMPLETENESS_AUDIT", + "status": "PARTIAL_BLOCKED", + "implementation": { + "workflowModules": { + "total": 16, + "implemented": 16 + }, + "productTasks": { + "total": 34, + "implemented": 34, + "missingResultFiles": [] + } + }, + "routeCoverage": { + "unitCount": 3745, + "classifiedCount": 3745, + "unclassifiedCount": 0, + "counts": { + "BROWSER_EQUIVALENT": 434, + "SERVER_NATIVE": 1613, + "WEB_MAIN": 1698 + }, + "unavailable": [], + "byKind": { + "modifier-source": 97, + "node": 497, + "operator": 2018, + "operator-macro": 58, + "rna-struct": 979, + "web-command": 96 + }, + "byFamily": { + "EDITOR": 2023, + "MODIFIER": 97, + "NODES": 497, + "RNA": 979, + "WEB_ENGINE": 96, + "WINDOW_MANAGER": 53 + } + }, + "behavioralEvidence": { + "status": "PARTIAL", + "verifiedUnits": 1025, + "unverifiedUnits": 2720, + "unverifiedByKind": { + "modifier-source": 2, + "operator": 1987, + "operator-macro": 58, + "rna-struct": 673 + }, + "unverifiedByFamily": { + "EDITOR": 1992, + "MODIFIER": 2, + "RNA": 673, + "WINDOW_MANAGER": 53 + }, + "rule": "route assignment is not behavioral completion" + }, + "smoke": { + "planId": "WBV2-FEATURE-SMOKE-001", + "totalFeatures": 34, + "attempts": 34, + "passed": 31, + "failedOrBlocked": [ + { + "featureId": "WEBUI-OPERATOR-PALETTE", + "smokeId": "WBV2-SMOKE-WEBUI-OPERATOR-PALETTE", + "ordinal": 6, + "status": "FAIL", + "attempts": 1, + "evidence": { + "testFile": "tests/e2e/smoke.spec.ts", + "testTitle": "operator search executes context-filtered workspace, mode and Main commands", + "exitCode": 1 + } + }, + { + "featureId": "OPERATOR-ROUTE-PALETTE", + "smokeId": "WBV2-SMOKE-OPERATOR-ROUTE-PALETTE", + "ordinal": 28, + "status": "FAIL", + "attempts": 1, + "evidence": { + "testFile": "tests/e2e/io-format-ui-gate.spec.ts", + "testTitle": "M12-05C exposes only matrix-declared file and operator routes", + "exitCode": 1 + } + }, + { + "featureId": "SERVER-OPERATOR-HTTP-PROVIDER", + "smokeId": "WBV2-SMOKE-SERVER-OPERATOR-HTTP-PROVIDER", + "ordinal": 32, + "status": "FAIL", + "attempts": 1, + "evidence": { + "command": "node tools/web/check-server-job-result-binding.mjs", + "exitCode": 1 + } + } + ], + "focusedFix": [ + { + "featureId": "WEBUI-OPERATOR-PALETTE", + "status": "PASS", + "verification": "Chromium focused test passed once after aligning locators with the current typed Blender operator palette and making F3 usable after file-input focus", + "files": [ + "web/app/src/app/App.tsx", + "web/tests/e2e/smoke.spec.ts" + ] + }, + { + "featureId": "SERVER-OPERATOR-HTTP-PROVIDER", + "status": "PASS", + "verification": "server-operator-job-server.mjs syntax and server-operator-job.py compilation passed", + "files": [ + "tools/web/server-operator-job-server.mjs", + "tools/web/server-operator-job.py" + ] + }, + { + "featureId": "OPERATOR-ROUTE-PALETTE", + "status": "BLOCKED", + "verification": "OBJ import route reaches the current WebEngine binary, but the checked-in WASM returns unified JSON cmd is not implemented", + "rootCause": "WASM artifact predates the source importScene/exportScene route", + "files": [ + "web/protocol/web-engine.ts", + "web/app/src/engine-client/WebEngineClient.ts", + "web/app/src/app/App.tsx", + "web/tests/e2e/io-format-ui-gate.spec.ts" + ], + "nextAction": "rebuild or replace the source-matched WebEngine WASM before rerunning this focused case" + } + ], + "retries": 0 + }, + "wasmLifecycle": [ + { + "id": "core-runtime-completed", + "lifecycle": "complete", + "preload": true, + "single": "web_engine.wasm", + "pthread": "web_engine.wasm" + }, + { + "id": "mesh-editing-completed", + "lifecycle": "complete", + "preload": true, + "single": "web_engine.wasm", + "pthread": "web_engine.wasm" + }, + { + "id": "reader-capabilities-completed", + "lifecycle": "complete", + "preload": true, + "single": "web_engine.wasm", + "pthread": "web_engine.wasm" + }, + { + "id": "debug-incremental", + "lifecycle": "debug", + "preload": false, + "single": "web_engine.wasm", + "pthread": "web_engine.wasm" + }, + { + "id": "debug-incremental-secondary", + "lifecycle": "debug", + "preload": false, + "single": "web_engine.secondary.wasm", + "pthread": "web_engine.secondary.wasm" + } + ], + "actualMissingOrBlocked": [ + { + "id": "IO_IMPORT_WASM_ARTIFACT", + "status": "BLOCKED", + "description": "importScene/exportScene is present in source/protocol but the checked-in WASM lacks the unified JSON route; rebuilding currently fails on Blender API/header drift.", + "evidence": "tests/golden/WBV2/feature-smoke-fix-report.json" + }, + { + "id": "BEHAVIORAL_EVIDENCE_OVERLAY", + "status": "DEFERRED", + "description": "2720 classified source units still lack exact behavioral receipts; they remain routed, not claimed verified.", + "evidence": "web/app/src/capabilities/blender-feature-route-catalog.json" + } + ], + "nextWork": { + "priority": [ + "repair WebEngine source/API drift and rebuild source-matched WASM", + "rerun only OPERATOR-ROUTE-PALETTE focused IO smoke", + "then schedule batched evidence for unverified operators/RNA/macros/modifier units" + ], + "forbidden": [ + "do not rerun passed feature smoke cases", + "do not mark route assignment as behavioral verification", + "do not replace WASM with a simulated JS implementation" + ] + }, + "inputs": { + "productReportSha256": "ec2c29e2cc26d573e821647d4864e9bda3da77d46dbde27959aea2a2dc78218d", + "routeCatalogSha256": "f5df5b9efd00a8c4cdf4b0862064609da3d3538a97df5e3252d013a0791bde1d", + "smokeReportSha256": "85b71b3c7ef47266c2930b7f66407a22b99e8a4f1ef1bf09d8195d9be6d4ba89" + } +} diff --git a/tools/web/audit-wbv2-feature-completeness.mjs b/tools/web/audit-wbv2-feature-completeness.mjs new file mode 100644 index 000000000..d256ed58f --- /dev/null +++ b/tools/web/audit-wbv2-feature-completeness.mjs @@ -0,0 +1,80 @@ +import crypto from "node:crypto"; +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); +const readJson = (relative) => JSON.parse(fs.readFileSync(path.join(root, relative), "utf8")); +const sha = (relative) => crypto.createHash("sha256").update(fs.readFileSync(path.join(root, relative))).digest("hex"); +const report = readJson("tests/golden/WBV2-P1424-001/product-implementation-report.json"); +const catalog = readJson("web/app/src/capabilities/blender-feature-route-catalog.json"); +const smokePlan = readJson("tests/golden/WBV2/feature-smoke-plan.json"); +const smokeReport = readJson("tests/golden/WBV2/feature-smoke-report.json"); +const smokeFix = readJson("tests/golden/WBV2/feature-smoke-fix-report.json"); +const wasmManifest = readJson("web/app/public/wasm-module-manifest.json"); + +const countBy = (items, selector) => Object.fromEntries([...new Set(items.map(selector))].sort().map((key) => [key, items.filter((item) => selector(item) === key).length])); +const unverified = catalog.entries.filter((entry) => entry.evidenceStatus === "UNVERIFIED"); +const failedSmoke = smokeReport.cases.filter((entry) => entry.status !== "PASS"); +const implementationResults = report.implementationTasks.map((task) => ({ + id: task.id, + module: task.module, + status: task.status, + resultPath: task.resultPath ?? null, + resultPresent: task.resultPath ? fs.existsSync(path.join(root, task.resultPath)) : false, +})); +const missingResults = implementationResults.filter((entry) => !entry.resultPresent); + +const audit = { + schemaVersion: 1, + auditId: "WBV2-FEATURE-COMPLETENESS-001", + operation: "DETAILED_MIGRATION_COMPLETENESS_AUDIT", + status: missingResults.length || failedSmoke.length ? "PARTIAL_BLOCKED" : "IMPLEMENTATION_COMPLETE", + implementation: { + workflowModules: { total: report.workflowModules.length, implemented: report.workflowModules.filter((item) => item.status === "implemented").length }, + productTasks: { total: report.implementationTasks.length, implemented: report.implementationTasks.filter((item) => item.status === "implemented").length, missingResultFiles: missingResults }, + }, + routeCoverage: { + unitCount: catalog.unitCount, + classifiedCount: catalog.classifiedCount, + unclassifiedCount: catalog.unclassifiedCount, + counts: catalog.counts, + unavailable: catalog.entries.filter((entry) => entry.route === "UNAVAILABLE").map((entry) => entry.id), + byKind: countBy(catalog.entries, (entry) => entry.kind), + byFamily: countBy(catalog.entries, (entry) => entry.family), + }, + behavioralEvidence: { + status: catalog.behavioralEvidence.status, + verifiedUnits: catalog.behavioralEvidence.verifiedUnits, + unverifiedUnits: catalog.behavioralEvidence.unverifiedUnits, + unverifiedByKind: countBy(unverified, (entry) => entry.kind), + unverifiedByFamily: countBy(unverified, (entry) => entry.family), + rule: "route assignment is not behavioral completion", + }, + smoke: { + planId: smokePlan.planId, + totalFeatures: smokePlan.cases.length, + attempts: smokeReport.cases.reduce((sum, entry) => sum + Number(entry.attempts ?? 0), 0), + passed: smokeReport.cases.filter((entry) => entry.status === "PASS").length, + failedOrBlocked: failedSmoke, + focusedFix: smokeFix.fixes, + retries: smokeFix.retries, + }, + wasmLifecycle: wasmManifest.modules.map((module) => ({ id: module.id, lifecycle: module.lifecycle, preload: module.preload, single: module.resources?.single?.fileName ?? null, pthread: module.resources?.pthread?.fileName ?? null })), + actualMissingOrBlocked: [ + { id: "IO_IMPORT_WASM_ARTIFACT", status: "BLOCKED", description: "importScene/exportScene is present in source/protocol but the checked-in WASM lacks the unified JSON route; rebuilding currently fails on Blender API/header drift.", evidence: "tests/golden/WBV2/feature-smoke-fix-report.json" }, + { id: "BEHAVIORAL_EVIDENCE_OVERLAY", status: "DEFERRED", description: "2720 classified source units still lack exact behavioral receipts; they remain routed, not claimed verified.", evidence: "web/app/src/capabilities/blender-feature-route-catalog.json" }, + ], + nextWork: { + priority: ["repair WebEngine source/API drift and rebuild source-matched WASM", "rerun only OPERATOR-ROUTE-PALETTE focused IO smoke", "then schedule batched evidence for unverified operators/RNA/macros/modifier units"], + forbidden: ["do not rerun passed feature smoke cases", "do not mark route assignment as behavioral verification", "do not replace WASM with a simulated JS implementation"], + }, + inputs: { + productReportSha256: sha("tests/golden/WBV2-P1424-001/product-implementation-report.json"), + routeCatalogSha256: sha("web/app/src/capabilities/blender-feature-route-catalog.json"), + smokeReportSha256: sha("tests/golden/WBV2/feature-smoke-report.json"), + }, +}; +const outputPath = path.join(root, "tests/golden/WBV2/feature-completeness-audit.json"); +fs.writeFileSync(outputPath, `${JSON.stringify(audit, null, 2)}\n`); +process.stdout.write(`wbv2-feature-completeness-audit status=${audit.status} modules=${audit.implementation.workflowModules.implemented}/${audit.implementation.workflowModules.total} tasks=${audit.implementation.productTasks.implemented}/${audit.implementation.productTasks.total} routes=${audit.routeCoverage.classifiedCount}/${audit.routeCoverage.unitCount} verified=${audit.behavioralEvidence.verifiedUnits} unverified=${audit.behavioralEvidence.unverifiedUnits} smokePassed=${audit.smoke.passed}/${audit.smoke.totalFeatures} blocked=${audit.actualMissingOrBlocked.filter((item) => item.status === "BLOCKED").length}\n`);