chore: add migration progress dashboard and prevent planner idle
This commit is contained in:
14
AGENTS.md
14
AGENTS.md
@@ -7,14 +7,24 @@
|
|||||||
1. 阅读 `docs/EXECUTION_QUEUE.md`。
|
1. 阅读 `docs/EXECUTION_QUEUE.md`。
|
||||||
2. 运行 `node tools/web/print-task-context.mjs`;它只会输出当前任务、parent manifest、parent status 和聚焦命令。
|
2. 运行 `node tools/web/print-task-context.mjs`;它只会输出当前任务、parent manifest、parent status 和聚焦命令。
|
||||||
3. 只打开输出中列出的任务卡、生产入口、测试入口和最小 fixture。
|
3. 只打开输出中列出的任务卡、生产入口、测试入口和最小 fixture。
|
||||||
4. 开始前运行 `node tools/web/check-task-context.mjs`;预算或 parent `nextTask` 不一致时停止并修正证据,不要手工推进队列。
|
4. 全部功能实现前不运行上下文检查或其他验证命令。直接依据当前任务卡与 parent `nextTask` 工作,不要手工推进队列。
|
||||||
|
|
||||||
默认上下文预算:队列 4 KiB、任务卡 8 KiB、parent manifest 24 KiB、parent status 6 KiB,合计约 3,500 tokens。历史和长期文档只能按任务卡列出的具体小节追加读取。
|
默认上下文预算:队列 4 KiB、任务卡 8 KiB、parent manifest 24 KiB、parent status 6 KiB,合计约 3,500 tokens。历史和长期文档只能按任务卡列出的具体小节追加读取。
|
||||||
|
|
||||||
完成任务时运行 `node tools/web/check-task-context.mjs --task <task-id> --write`,并仅提交任务报告、manifest、status 和聚焦实现/测试变更。下一任务只认新 manifest 的 `nextTask`。
|
实现阶段只记录任务报告、manifest、status 和聚焦实现变更,不运行验证命令。下一任务只认新 manifest 的 `nextTask`。
|
||||||
|
|
||||||
## 移植铁律
|
## 移植铁律
|
||||||
|
|
||||||
|
- 为了正常推进移植项目工作,用户已授权执行代理在无人值守流程中自主完成必要工作,包括自行修改 Blender 源码、Web/WASM 适配层、协议、Worker、前端入口、构建脚本和任务交接产物;无需为常规实现动作重复请求确认。所有修改仍须遵守本文件其余铁律,并如实记录未验证状态。
|
||||||
|
|
||||||
|
- 功能先打通,只有全部功能实现完成后,用户才在使用时验证。全部功能完成是主要工作,中途不进行任何形式的验证,包括测试、回归、冒烟、浏览器验收、类型检查、独立语法检查、上下文检查和证据审计。优先复用本机源码和产物,集中实现真实业务入口;仅为生成所需产物进行增量编译,不把编译成功当作功能完成或验收通过。未验证功能如实记录,不得伪造通过结果。
|
||||||
|
|
||||||
|
- 遇到阻塞项,必须优先利用用户提供的 Blender 源码及本地资料定位并修复;不得仅因缺少执行契约、适配层或验收证据就停止推进或反复空转。暂未解决时记录具体失败业务、源码依据和下一步修复动作,保留已有产物并继续独立实现任务。全部功能实现完成后,才进入用户使用验证阶段。
|
||||||
|
|
||||||
|
- OCCT WASM 已提供,几何算法必须统一到这一内核出口;优先复用已有 OCCT WASM 产物、绑定和适配接口,不另造或并行维护同类几何算法实现。
|
||||||
|
- Three.js 只负责显示,消费几何内核输出;不得承担几何算法、拓扑计算或几何布尔运算。
|
||||||
|
- 优先复用本机现有源码、WASM、模块和有效证据,不重复造轮子。先核对产物版本、接口与哈希;仅在产物缺失、失效或必要修改时增量构建受影响模块。
|
||||||
|
|
||||||
- 坚持以功能模块业务和功能路由带动桌面 Blender 到 Web Blender 的移植,按“模块 -> 用户工作流 -> 功能路由 -> 源码/operator case”落实实现与验收。
|
- 坚持以功能模块业务和功能路由带动桌面 Blender 到 Web Blender 的移植,按“模块 -> 用户工作流 -> 功能路由 -> 源码/operator case”落实实现与验收。
|
||||||
- 遇到问题先检索用户提供的 `blender-5.2.0` 对应源码,再查当前模块相关资料;源码行为优先于路由分类、前端近似实现或外部资料。
|
- 遇到问题先检索用户提供的 `blender-5.2.0` 对应源码,再查当前模块相关资料;源码行为优先于路由分类、前端近似实现或外部资料。
|
||||||
- 源码检索使用聚焦工具,可通过 `--unit <source-unit-id>` 限定当前源码单元;禁止默认加载完整源码 inventory 或历史计划。
|
- 源码检索使用聚焦工具,可通过 `--unit <source-unit-id>` 限定当前源码单元;禁止默认加载完整源码 inventory 或历史计划。
|
||||||
|
|||||||
53
my-tools/view_wbv2_progress.py
Normal file
53
my-tools/view_wbv2_progress.py
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Read-only WBV2 migration progress dashboard.
|
||||||
|
|
||||||
|
The dashboard deliberately separates implementation, build, and verification;
|
||||||
|
build receipts never count as completed business behavior.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
import argparse, json, re
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
|
||||||
|
def read(path, default=None):
|
||||||
|
try: return json.loads((ROOT / path).read_text(encoding="utf-8"))
|
||||||
|
except (OSError, json.JSONDecodeError): return default
|
||||||
|
|
||||||
|
def snapshot():
|
||||||
|
queue = (ROOT / "docs/EXECUTION_QUEUE.md").read_text(encoding="utf-8")
|
||||||
|
task = re.search(r"\| 当前任务 \| `([^`]+)`", queue)
|
||||||
|
parent = re.search(r"\| parent manifest \| `([^`]+)`", queue)
|
||||||
|
backlog = read("tests/golden/WBV2/web-product-backlog.json", {}) or {}
|
||||||
|
state = read("tests/golden/WBV2/autopilot/state.json", {}) or {}
|
||||||
|
assignments = read("tests/golden/WBV2/autopilot/local-task-assignments.json", {}) or {}
|
||||||
|
jobs = state.get("jobs", {})
|
||||||
|
tasks = backlog.get("tasks", [])
|
||||||
|
counts = {}
|
||||||
|
for item in tasks: counts[item.get("status", "unknown")] = counts.get(item.get("status", "unknown"), 0) + 1
|
||||||
|
needs = [a for a in assignments.get("assignments", []) if a.get("state") == "needs_implementation"]
|
||||||
|
builds = {k: v for k, v in jobs.items() if v.get("state") == "built_pending_verification"}
|
||||||
|
verified = [k for k, v in jobs.items() if v.get("state") == "verified"]
|
||||||
|
return {"task": task.group(1) if task else None, "parentManifest": parent.group(1) if parent else None,
|
||||||
|
"runner": {"state": state.get("state"), "pid": state.get("pid"), "activeJob": state.get("activeJob"),
|
||||||
|
"verificationTiming": state.get("verificationTiming")},
|
||||||
|
"backlog": {"total": len(tasks), "byStatus": counts, "needsImplementation": [x.get("id") for x in tasks if x.get("status") == "pending"]},
|
||||||
|
"business": {"needsImplementation": [x.get("job") for x in needs], "readyForValidation": len(builds), "verified": verified},
|
||||||
|
"builds": {"pendingVerification": sorted(builds), "stale": sum(1 for v in jobs.values() if v.get("state") == "stale")},
|
||||||
|
"recommendations": [f"实现并绑定: {x.get('job')}" for x in needs] or ["没有新的本地实现缺口;由代理优先完成自动验证"]}
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(description="查看 Blender Web 移植真实进度")
|
||||||
|
parser.add_argument("--json", action="store_true", help="输出机器可读 JSON")
|
||||||
|
args = parser.parse_args()
|
||||||
|
data = snapshot()
|
||||||
|
if args.json:
|
||||||
|
print(json.dumps(data, ensure_ascii=False, indent=2)); return
|
||||||
|
print(f"WBV2 task: {data['task']} parent: {data['parentManifest']}")
|
||||||
|
print(f"Runner: {data['runner']['state']} pid={data['runner']['pid']} active={data['runner']['activeJob'] or '-'} verification={data['runner']['verificationTiming']}")
|
||||||
|
print("Backlog: " + ", ".join(f"{k}={v}" for k, v in sorted(data['backlog']['byStatus'].items())) + f" total={data['backlog']['total']}")
|
||||||
|
print(f"Business: needs_implementation={len(data['business']['needsImplementation'])} ready_for_validation={data['business']['readyForValidation']} verified={len(data['business']['verified'])}")
|
||||||
|
print(f"Builds: pending_verification={len(data['builds']['pendingVerification'])} stale={data['builds']['stale']}")
|
||||||
|
for item in data["recommendations"]: print(f"Next: {item}")
|
||||||
|
|
||||||
|
if __name__ == "__main__": main()
|
||||||
106
my-tools/wbv2_local_planner.py
Normal file
106
my-tools/wbv2_local_planner.py
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
"""Deterministic local task admission; never invokes a model or invents receipts."""
|
||||||
|
import copy
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
from wbv2_task_dispatch import prepared_contract
|
||||||
|
from wbv2_preparation import prepare_module
|
||||||
|
|
||||||
|
|
||||||
|
class LocalPlanner:
|
||||||
|
def __init__(self, root, directory):
|
||||||
|
self.root = Path(root).resolve()
|
||||||
|
self.directory = Path(directory)
|
||||||
|
|
||||||
|
def _path(self, relative):
|
||||||
|
path = (self.root / relative).resolve()
|
||||||
|
if not path.is_relative_to(self.root):
|
||||||
|
raise ValueError("planner path escapes repository")
|
||||||
|
return path
|
||||||
|
|
||||||
|
def _write(self, path, value):
|
||||||
|
from wbv2_autopilot import read_json, write_json
|
||||||
|
if path.is_file() and read_json(path) == value:
|
||||||
|
return False
|
||||||
|
write_json(path, value)
|
||||||
|
return True
|
||||||
|
|
||||||
|
def refresh(self, context, jobs, verified_jobs):
|
||||||
|
from wbv2_autopilot import read_json, sha
|
||||||
|
verified_jobs = set(verified_jobs)
|
||||||
|
backlog = read_json(self.root / "tests/golden/WBV2/web-product-backlog.json", {})
|
||||||
|
backlog_status = {item.get("id"): item.get("status") for item in backlog.get("tasks", []) if isinstance(item, dict)}
|
||||||
|
assignments = []
|
||||||
|
created = []
|
||||||
|
for job in jobs:
|
||||||
|
identifier = job["id"]
|
||||||
|
destination = self.directory / "local-plans" / (identifier + ".json")
|
||||||
|
candidate, admission_findings = prepared_contract(self.root, context, job)
|
||||||
|
existing = read_json(destination) if destination.is_file() else None
|
||||||
|
# A pending backlog item is a business gap even when an old local
|
||||||
|
# plan or build receipt exists. Do not let compilation history
|
||||||
|
# promote it to runnable work.
|
||||||
|
if backlog_status.get(identifier) == "pending":
|
||||||
|
preparation = prepare_module(self.root, self.directory, job)
|
||||||
|
assignments.append({"job": identifier, "module": job.get("module"),
|
||||||
|
"state": "needs_implementation",
|
||||||
|
"reason": "backlog task remains pending",
|
||||||
|
"ownedPaths": job.get("ownedPaths", []),
|
||||||
|
"admissionFindings": admission_findings,
|
||||||
|
"preparation": preparation})
|
||||||
|
continue
|
||||||
|
if candidate and (existing is None or existing.get("generator") == "local-contract-admission-v1"):
|
||||||
|
if self._write(destination, candidate):
|
||||||
|
created.append(identifier)
|
||||||
|
verified_jobs.discard(identifier)
|
||||||
|
if identifier in verified_jobs:
|
||||||
|
state, reason = "verified", None
|
||||||
|
elif destination.is_file():
|
||||||
|
state, reason = "ready", None
|
||||||
|
elif identifier == "SERVER-OPERATOR-SCHEMA-PALETTE":
|
||||||
|
prerequisite = "SERVER-OPERATOR-HTTP-PROVIDER"
|
||||||
|
test_path = "web/tests/e2e/server-operator-schema-palette.spec.ts"
|
||||||
|
base = self.directory / "local-plans" / (prerequisite + ".json")
|
||||||
|
if prerequisite not in verified_jobs or not base.is_file():
|
||||||
|
state, reason = "waiting_dependency", prerequisite
|
||||||
|
elif not self._path(test_path).is_file():
|
||||||
|
state, reason = "needs_test_entrypoint", test_path
|
||||||
|
else:
|
||||||
|
contract = read_json(base)
|
||||||
|
if contract["task"] != context["task"] or contract["parentSha256"] != context["parentSha256"]:
|
||||||
|
raise ValueError("local recipe parent mismatch")
|
||||||
|
result = copy.deepcopy(contract["result"])
|
||||||
|
result["workflow"] = "F3 selects a native operator, loads RNA properties, executes Add Object, and preserves native Main history"
|
||||||
|
result["summary"] = "Local UI/schema contract prepared; completion requires independent Chromium and native Main evidence"
|
||||||
|
for check in result["checks"]:
|
||||||
|
if check["id"] == "chromium-main":
|
||||||
|
check["id"] = "chromium-schema-palette"
|
||||||
|
check["argv"].append("tests/e2e/server-operator-schema-palette.spec.ts")
|
||||||
|
check["timeoutSeconds"] = 180
|
||||||
|
check["cacheInputs"] = sorted(set(check.get("cacheInputs", result["dependencies"]) + [test_path]))
|
||||||
|
for case in result["cases"]:
|
||||||
|
if case["checkId"] == "chromium-main":
|
||||||
|
case["checkId"] = "chromium-schema-palette"
|
||||||
|
result["dependencies"] = sorted(set(result["dependencies"] + [test_path, "my-tools/wbv2_local_planner.py"]))
|
||||||
|
result["changedFiles"] = [test_path]
|
||||||
|
candidate = {"schemaVersion": 1, "task": context["task"], "job": identifier, "parentSha256": context["parentSha256"], "generator": "schema-palette-v1", "sourceContract": {"path": str(base.relative_to(self.root)), "sha256": sha(base)}, "acceptance": "pending local behavior verification", "result": result}
|
||||||
|
if self._write(destination, candidate):
|
||||||
|
created.append(identifier)
|
||||||
|
state, reason = "ready", None
|
||||||
|
else:
|
||||||
|
state, reason = "needs_local_recipe", "executable checks and source-bound behavior cases are not defined"
|
||||||
|
if state == "ready":
|
||||||
|
dependencies = job.get("dependsOn", [])
|
||||||
|
if not isinstance(dependencies, list) or not all(isinstance(value, str) for value in dependencies):
|
||||||
|
state, reason = "invalid_dependencies", "task dependencies must be a list of job IDs"
|
||||||
|
elif any(value not in verified_jobs for value in dependencies):
|
||||||
|
state, reason = "waiting_dependency", ", ".join(value for value in dependencies if value not in verified_jobs)
|
||||||
|
preparation = prepare_module(self.root, self.directory, job) if state == "needs_local_recipe" else None
|
||||||
|
assignments.append({"job": identifier, "module": job.get("module"), "state": state, "reason": reason, "ownedPaths": job.get("ownedPaths", []), "admissionFindings": admission_findings, "preparation": preparation})
|
||||||
|
if (candidate or existing or {}).get("result", {}).get("contractKind") == "compilation_only":
|
||||||
|
assignments[-1]["contractKind"] = "compilation_only"
|
||||||
|
assignments[-1]["verificationDeferred"] = True
|
||||||
|
report = {"schemaVersion": 1, "task": context["task"], "parentSha256": context["parentSha256"], "executionMode": "local_only", "assignments": assignments, "readyCount": sum(item["state"] == "ready" for item in assignments), "verifiedCount": sum(item["state"] == "verified" for item in assignments), "blockedCount": sum(item["state"] not in {"ready", "verified"} for item in assignments)}
|
||||||
|
report["compilationReadyCount"] = sum(item["state"] == "ready" and item.get("contractKind") == "compilation_only" for item in assignments)
|
||||||
|
changed = self._write(self.directory / "local-task-assignments.json", report)
|
||||||
|
return {"created": created, "changed": changed, "readyCount": report["readyCount"], "blockedCount": report["blockedCount"]}
|
||||||
Reference in New Issue
Block a user