chore: checkpoint web Blender migration source, tools and docs
Accumulated uncommitted work: GHOST Web backend, WebGL/GPU web layer, web workflow engine and protocol, tooling, docs and task records. Large generated evidence, user-testing archives, build snapshots and imported test assets remain untracked. Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -1,53 +1,705 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Read-only WBV2 migration progress dashboard.
|
||||
"""只读查看 Blender → Web Blender UI 对标执行状态与最新功能交接。
|
||||
|
||||
The dashboard deliberately separates implementation, build, and verification;
|
||||
build receipts never count as completed business behavior.
|
||||
不会启动执行器、构建、部署或运行功能验证;候选源码对应关系仍明确标记为未知。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import argparse, json, re
|
||||
|
||||
import argparse
|
||||
from datetime import datetime, timezone
|
||||
import json
|
||||
import math
|
||||
from pathlib import Path
|
||||
import re
|
||||
import sys
|
||||
import time
|
||||
from wbv2_gap_progress import resolve_gap
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
FOCUS_NAMES = {'editors': '桌面与编辑器', 'gpu': '原生绘制与计算', 'bpy': 'Python/操作入口'}
|
||||
STATE_NAMES = {
|
||||
'running': '执行中', 'starting': '正在启动工作轮次', 'handed_off': '已交接,准备继续',
|
||||
'recovering': '恢复中', 'waiting_retry': '等待重试', 'reloading': '安全交接后加载新执行逻辑',
|
||||
'stopped': '已停止', 'awaiting_user_verification': '执行器报告实现已交接,等待用户使用',
|
||||
}
|
||||
WORK_NAMES = {
|
||||
'reading_sources': '定位原生源码', 'source_changes_observed': '已有源码改动',
|
||||
'executing_command': '执行实现命令', 'waiting_executor': '等待执行器动作',
|
||||
'waiting_provider_retry': '服务退避后重试', 'waiting_business_retry': '业务轮次重试等待',
|
||||
}
|
||||
|
||||
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 obj(value):
|
||||
return value if isinstance(value, dict) else {}
|
||||
|
||||
|
||||
def rows(value):
|
||||
return [item for item in value if isinstance(item, dict)] if isinstance(value, list) else []
|
||||
|
||||
|
||||
def number(value):
|
||||
return value if isinstance(value, (int, float)) and not isinstance(value, bool) and math.isfinite(value) else None
|
||||
|
||||
|
||||
def read_record(path, notes, required=False):
|
||||
try:
|
||||
# Only small state/manifest/result files; never load full inventory or logs.
|
||||
with path.open(encoding='utf-8') as stream:
|
||||
raw = stream.read(2 * 1024 * 1024 + 1)
|
||||
if len(raw) > 2 * 1024 * 1024:
|
||||
raise ValueError('超过进度记录大小上限')
|
||||
data = json.loads(raw)
|
||||
if not isinstance(data, dict):
|
||||
raise ValueError('记录不是 JSON 对象')
|
||||
return data
|
||||
except FileNotFoundError:
|
||||
if required:
|
||||
notes.append(f'尚无记录:{path.relative_to(ROOT)}')
|
||||
except (OSError, ValueError) as error:
|
||||
notes.append(f'暂时无法读取 {path.relative_to(ROOT)}:{error}')
|
||||
return {}
|
||||
|
||||
|
||||
def timestamp(value):
|
||||
try:
|
||||
result = datetime.fromisoformat(value.replace('Z', '+00:00'))
|
||||
return result.timestamp() if result.tzinfo else None
|
||||
except (AttributeError, TypeError, ValueError, OverflowError):
|
||||
return None
|
||||
|
||||
|
||||
def local_time(value):
|
||||
stamp = timestamp(value)
|
||||
return datetime.fromtimestamp(stamp).astimezone().strftime('%m-%d %H:%M:%S %Z') if stamp is not None else '未知'
|
||||
|
||||
|
||||
def duration(seconds):
|
||||
seconds = number(seconds)
|
||||
if seconds is None:
|
||||
return '未知'
|
||||
value = max(0, int(seconds))
|
||||
if value >= 3600:
|
||||
return f'{value // 3600}小时{value % 3600 // 60}分'
|
||||
return f'{value // 60}分{value % 60}秒' if value >= 60 else f'{value}秒'
|
||||
|
||||
|
||||
def cycle_from_path(value):
|
||||
"""Extract a cycle number from a candidate/report path when available."""
|
||||
match = re.search(r'(?:cycle-|through-|candidate-)(\d+)', str(value or ''))
|
||||
return int(match.group(1)) if match else None
|
||||
|
||||
|
||||
def cadence_label(cadence):
|
||||
average = number(cadence.get('averageSeconds'))
|
||||
if average is None:
|
||||
return '暂无足够交接样本'
|
||||
return f"最近 {cadence.get('sampleCount', 0)} 轮平均 {duration(average)},上一轮 {duration(cadence.get('lastSeconds'))}"
|
||||
|
||||
|
||||
def assigned_ui_module(delivery_stage):
|
||||
"""Read the module explicitly named by the current delivery assignment."""
|
||||
assignment = obj(delivery_stage).get('assignment')
|
||||
modules = re.findall(r'UI-[A-Z]+-\d{3}', assignment or '')
|
||||
return modules[0] if len(set(modules)) == 1 else None
|
||||
|
||||
|
||||
def process_status(pid, expected_identity):
|
||||
if not isinstance(pid, int) or isinstance(pid, bool) or pid <= 0:
|
||||
return 'not_recorded'
|
||||
try:
|
||||
fields = Path(f'/proc/{pid}/stat').read_text().rsplit(')', 1)[1].split()
|
||||
if fields[0] in {'Z', 'X'}:
|
||||
return 'exited'
|
||||
if expected_identity is None:
|
||||
return 'identity_unknown'
|
||||
return 'alive' if fields[19] == str(expected_identity) else 'pid_reused'
|
||||
except FileNotFoundError:
|
||||
return 'exited' if Path('/proc').exists() else 'unknown'
|
||||
except (OSError, IndexError):
|
||||
return 'unknown'
|
||||
|
||||
|
||||
def snapshot(recent_limit=3):
|
||||
notes = []
|
||||
now = time.time()
|
||||
try:
|
||||
queue = (ROOT / 'docs/EXECUTION_QUEUE.md').read_text(encoding='utf-8')
|
||||
except OSError as error:
|
||||
queue = ''
|
||||
notes.append(f'执行队列无法读取:{error}')
|
||||
match = re.search(r'\| 当前任务 \| `([A-Z0-9_-]+)`', queue)
|
||||
task = match.group(1) if match else None
|
||||
data = {'observedAt': datetime.now(timezone.utc).isoformat(), 'task': task,
|
||||
'notes': notes, 'basis': '既有实现交接;只读查询,未执行任何功能验证'}
|
||||
if not task:
|
||||
notes.append('未找到当前任务指针,不能判断完成情况。')
|
||||
return data
|
||||
base = ROOT / 'tests/golden' / task
|
||||
state = read_record(base / 'unattended-implementation/state.json', notes, True)
|
||||
if state and state.get('task') != task:
|
||||
notes.append('执行状态所属任务与队列不一致,未将旧状态计入当前进度。')
|
||||
state = {}
|
||||
delivery = read_record(base / 'desktop-delivery-manifest.json', notes, True)
|
||||
for record in (delivery,):
|
||||
if record and record.get('task') != task:
|
||||
notes.append('计划所属任务不一致,忽略该计划。')
|
||||
record.clear()
|
||||
first_group = obj(delivery.get('firstGroupExecution'))
|
||||
gap_policy = obj(delivery.get('gapScheduling'))
|
||||
cadence_policy = obj(first_group.get('candidateCadence'))
|
||||
refresh_gap = cadence_policy.get('sourceIncrementGap') or gap_policy.get('candidateRefreshSourceGap') or 0
|
||||
first_group_view = {
|
||||
'stageId': first_group.get('stageId') or 'main-desktop',
|
||||
'status': first_group.get('status') or obj(delivery.get('stages')[0] if rows(delivery.get('stages')) else {}).get('status', 'in_progress'),
|
||||
'scope': first_group.get('scope') or 'DW01-DW03 concrete operator workflows',
|
||||
'planPath': first_group.get('planPath') or gap_policy.get('firstGroupScopeReport'),
|
||||
'currentWave': first_group.get('currentWave') or 'unspecified',
|
||||
'nextWaves': first_group.get('nextWaves') or [],
|
||||
'coherentFocusTurns': cadence_policy.get('coherentFocusTurns') or gap_policy.get('coherentFocusTurns') or 0,
|
||||
'candidateRefreshSourceGap': refresh_gap,
|
||||
'buildPolicy': first_group.get('buildPolicy') or {'mode': 'unknown'},
|
||||
}
|
||||
current = obj(state.get('deliveryStage'))
|
||||
progress = obj(state.get('deliveryStages'))
|
||||
stages = []
|
||||
for stage in rows(delivery.get('stages')):
|
||||
stage_id = stage.get('id')
|
||||
recorded = obj(obj(progress.get(stage_id)).get('focuses'))
|
||||
order = stage.get('focusOrder') if isinstance(stage.get('focusOrder'), list) else []
|
||||
order = [name for name in order if isinstance(name, str)]
|
||||
done = bool(order) and all(obj(recorded.get(name)).get('reportedImplemented') is True for name in order)
|
||||
status = 'reported_implemented_unverified' if done else (
|
||||
'in_progress' if recorded or current.get('id') == stage_id else 'no_stage_completion_record')
|
||||
stages.append({'id': stage_id, 'title': stage.get('title') or stage_id,
|
||||
'status': status, 'current': current.get('id') == stage_id,
|
||||
'focusesReportedImplemented': sum(obj(recorded.get(name)).get('reportedImplemented') is True for name in order),
|
||||
'focusCount': len(order)})
|
||||
supervisor = process_status(state.get('pid'), state.get('identity'))
|
||||
worker = process_status(state.get('childPid'), state.get('childIdentity'))
|
||||
updated = timestamp(state.get('updatedAt'))
|
||||
age = max(0, now - updated) if updated is not None else None
|
||||
recorded_state = state.get('state', 'not_started')
|
||||
health = 'normal'
|
||||
if not state:
|
||||
health = 'unknown'
|
||||
elif supervisor in {'exited', 'pid_reused', 'not_recorded'}:
|
||||
health = 'stopped' if recorded_state in {'stopped', 'awaiting_user_verification'} else 'process_missing'
|
||||
elif supervisor != 'alive':
|
||||
health = 'process_unknown'
|
||||
elif age is None or age > 30:
|
||||
health = 'heartbeat_stale'
|
||||
elif recorded_state == 'running' and worker == 'exited':
|
||||
health = 'worker_exited_pending_handoff'
|
||||
provider = obj(state.get('provider'))
|
||||
retry = number(provider.get('retryAt'))
|
||||
issues = [item for item in obj(obj(state.get('efficiency')).get('issues')).values()
|
||||
if isinstance(item, dict) and item.get('status') == 'open']
|
||||
focus_records = obj(obj(progress.get(current.get('id'))).get('focuses'))
|
||||
current_plan = next((item for item in rows(delivery.get('stages'))
|
||||
if item.get('id') == current.get('id')), {})
|
||||
concrete_gaps = {}
|
||||
findings = obj(obj(state.get('efficiency')).get('issues'))
|
||||
for name, seeds in obj(current_plan.get('gapItems')).items():
|
||||
concrete_gaps[name] = []
|
||||
focus_delivery = obj(obj(obj(progress.get(current.get('id'))).get('focuses')).get(name))
|
||||
for seed in rows(seeds):
|
||||
key = f"{current.get('id')}:{name}:{seed.get('id')}"
|
||||
finding = obj(findings.get(key))
|
||||
item = resolve_gap(seed, finding, focus_delivery)
|
||||
handoff = obj(seed.get('implementationHandoff'))
|
||||
# Supersede only explicitly named old residuals, below the recorded
|
||||
# cycle boundary. A later worker observation always remains visible.
|
||||
boundary = number(handoff.get('throughCycle'))
|
||||
latest_cycle = max(number(finding.get('cycle')) or 0,
|
||||
number(focus_delivery.get('cycle')) or 0)
|
||||
covered = set(handoff.get('supersededRoutes') or [])
|
||||
if handoff.get('reportPath') and boundary is not None and latest_cycle <= boundary:
|
||||
previous = item.get('remainingSubroutes') or []
|
||||
remaining = [route for route in previous if route not in covered]
|
||||
if covered and previous and len(remaining) < len(previous):
|
||||
remaining.extend(handoff.get('remainingRoutes') or [])
|
||||
item['remainingSubroutes'] = list(dict.fromkeys(remaining))
|
||||
item['completedSubroutes'] = list(dict.fromkeys(
|
||||
item.get('completedSubroutes', []) + handoff.get('completedRoutes', [])))
|
||||
item['status'] = 'open' if remaining else 'implemented_unverified'
|
||||
item['statusSource'] = 'explicit_implementation_handoff'
|
||||
item['latestFinding'] = {
|
||||
'cycle': None, 'reason': '\n'.join(item['remainingSubroutes']),
|
||||
'action': handoff.get('nextAction'),
|
||||
'sourcePaths': handoff.get('sourcePaths', []),
|
||||
}
|
||||
item['handoffReportPath'] = handoff['reportPath']
|
||||
handoff_cycles = [value for value in (handoff.get('throughCycles') or [])
|
||||
if isinstance(value, int) and not isinstance(value, bool)]
|
||||
handoff_boundary = max(handoff_cycles) if handoff_cycles else None
|
||||
if (handoff.get('reportPath') and handoff.get('candidatePath') and
|
||||
handoff_boundary is not None and latest_cycle <= handoff_boundary):
|
||||
residuals = list(dict.fromkeys(handoff.get('remainingRoutes') or []))
|
||||
item['completedSubroutes'] = list(dict.fromkeys(
|
||||
item.get('completedSubroutes', []) + (handoff.get('completedRoutes') or [])))
|
||||
item['remainingSubroutes'] = residuals
|
||||
item['missing'] = '\n'.join(residuals)
|
||||
item['nextAction'] = handoff.get('nextAction', item.get('nextAction', ''))
|
||||
item['status'] = 'open' if residuals else 'implemented_unverified'
|
||||
item['statusSource'] = 'explicit_candidate_implementation_handoff'
|
||||
item['latestFinding'] = {
|
||||
'cycle': handoff_boundary,
|
||||
'reason': item.get('missing'),
|
||||
'action': item.get('nextAction'),
|
||||
'sourcePaths': handoff.get('sourcePaths', []),
|
||||
}
|
||||
item['handoffReportPath'] = handoff['reportPath']
|
||||
concrete_gaps[name].append(item)
|
||||
focuses = []
|
||||
for name in dict.fromkeys([*obj(state.get('focusHandoffs')), *focus_records]):
|
||||
record = obj(focus_records.get(name))
|
||||
branch = obj(obj(state.get('focusHandoffs')).get(name))
|
||||
next_retry = number(branch.get('retryAt'))
|
||||
circuit_deadline = number(branch.get('circuitOpenUntil'))
|
||||
circuit_seconds = (max(0, int(circuit_deadline - now))
|
||||
if circuit_deadline is not None
|
||||
and branch.get('circuitStageId') == current.get('id') else 0)
|
||||
focuses.append({'id': name, 'title': FOCUS_NAMES.get(name, name), 'cycle': record.get('cycle'),
|
||||
'deferredPathSeconds': circuit_seconds,
|
||||
'deferredPathId': branch.get('stalledScope'),
|
||||
'completedRoutes': record.get('completedRoutes', []),
|
||||
'remainingRoutes': record.get('remainingRoutes', []),
|
||||
'reportPath': record.get('reportPath'),
|
||||
'blockingRoute': record.get('blockingRoute'), 'nextAction': record.get('nextAction'),
|
||||
'repeatedBlockerRounds': record.get('sameBusinessBlockerRounds', 0),
|
||||
'noProgressRounds': branch.get('noProgressRounds', 0),
|
||||
'retrySeconds': max(0, int(next_retry - now)) if next_retry is not None else 0})
|
||||
source_updates = []
|
||||
for stage in rows(delivery.get('stages')):
|
||||
for seeds in obj(stage.get('gapItems')).values():
|
||||
for seed in rows(seeds):
|
||||
handoff = obj(seed.get('implementationHandoff'))
|
||||
if handoff.get('reportPath'):
|
||||
source_updates.append({**handoff, 'gapId': seed.get('id'), 'stageId': stage.get('id')})
|
||||
source_updates.sort(key=lambda item: timestamp(item.get('recordedAt')) or 0, reverse=True)
|
||||
recent = []
|
||||
cycle = state.get('cycle')
|
||||
current_attempt = {}
|
||||
if isinstance(cycle, int) and not isinstance(cycle, bool):
|
||||
current_attempt = read_record(base / 'unattended-implementation' / f'cycle-{cycle:06d}' / 'attempt.json', notes)
|
||||
if (current_attempt.get('cycle') != cycle or current_attempt.get('focus') != state.get('focus')
|
||||
or current_attempt.get('deliveryStage') != current.get('id')):
|
||||
current_attempt = {}
|
||||
# Read only the requested recent reports, within a 24-cycle window.
|
||||
for index in range(cycle, max(0, cycle - 24), -1):
|
||||
if len(recent) >= recent_limit:
|
||||
break
|
||||
directory = base / 'unattended-implementation' / f'cycle-{index:06d}'
|
||||
result_path = directory / 'result.json'
|
||||
result = read_record(result_path, notes)
|
||||
if not result:
|
||||
continue
|
||||
process = read_record(directory / 'process.json', notes)
|
||||
recent.append({'cycle': index, 'status': result.get('status'),
|
||||
'businessEntry': result.get('businessEntry'), 'summary': result.get('summary'),
|
||||
'elapsedSeconds': process.get('elapsedSeconds'),
|
||||
'exitCode': process.get('exitCode'), 'reason': process.get('reason'),
|
||||
'changedProductionFiles': process.get('changedProductionFiles', []),
|
||||
'processRecorded': bool(process),
|
||||
'buildStatus': obj(result.get('build')).get('status'),
|
||||
'reportPath': str(result_path.relative_to(ROOT))})
|
||||
# A report mtime interval includes pauses, retries and skipped cycles. It is
|
||||
# not execution speed. Expose only explicitly recorded process durations.
|
||||
measured = [item['elapsedSeconds'] for item in recent if number(item.get('elapsedSeconds')) is not None]
|
||||
cadence = {'sampleCount': len(measured), 'averageSeconds': None,
|
||||
'lastSeconds': measured[0] if measured else None,
|
||||
'basis': 'process.json elapsedSeconds; no throughput estimate'}
|
||||
build = obj(state.get('sharedBuild'))
|
||||
# Only the current scheduler's explicit candidate is an application handoff.
|
||||
# A directory's mtime cannot associate an arbitrary candidate with this task.
|
||||
candidate = None
|
||||
recorded_candidate = obj(state.get('applicationCandidate'))
|
||||
preferred = recorded_candidate.get('path')
|
||||
preferred_path = (ROOT / preferred).resolve() if isinstance(preferred, str) and preferred else None
|
||||
chosen = None
|
||||
if preferred_path and preferred_path.is_file():
|
||||
try:
|
||||
chosen = (preferred_path.stat().st_mtime, preferred_path, 'state.applicationCandidate')
|
||||
except OSError:
|
||||
chosen = None
|
||||
if preferred and chosen is None:
|
||||
notes.append(f'当前候选记录不可读:{preferred};未用其它目录替代。')
|
||||
if chosen:
|
||||
modified, path, selection_basis = chosen
|
||||
record = read_record(path, notes)
|
||||
if record:
|
||||
latest_impl = obj(state.get('latestImplementation'))
|
||||
latest_at = timestamp(latest_impl.get('at'))
|
||||
latest_cycle = latest_impl.get('cycle') if isinstance(latest_impl.get('cycle'), int) else cycle
|
||||
candidate_cycle = (recorded_candidate.get('cycle')
|
||||
if selection_basis == 'state.applicationCandidate'
|
||||
and isinstance(recorded_candidate.get('cycle'), int) else cycle_from_path(path))
|
||||
cycle_gap = max(0, latest_cycle - candidate_cycle) if isinstance(latest_cycle, int) and isinstance(candidate_cycle, int) else None
|
||||
source_gap = None
|
||||
count_basis = 'not_recorded'
|
||||
pending = state.get('pendingSourceCycles')
|
||||
if selection_basis == 'state.applicationCandidate' and isinstance(pending, list):
|
||||
pending = sorted({n for n in pending if isinstance(n, int) and not isinstance(n, bool)})
|
||||
source_gap = len(pending)
|
||||
count_basis = 'recorded_source_change_rounds'
|
||||
candidate = {'path': str(path.relative_to(ROOT)), 'status': record.get('status'),
|
||||
'scopeComplete': record.get('scopeComplete'), 'validation': record.get('validation'),
|
||||
'deployment': record.get('deployment'),
|
||||
'recordedAt': datetime.fromtimestamp(modified, timezone.utc).isoformat(),
|
||||
'candidateCycle': candidate_cycle, 'latestImplementationCycle': latest_cycle,
|
||||
'sourceGap': source_gap,
|
||||
'pendingSourceCycles': pending if isinstance(pending, list) else None,
|
||||
'pendingHistoryIncomplete': bool(state.get('pendingSourceHistoryIncomplete')),
|
||||
'externalIntegrationUnconfirmed': [item for item in source_updates if not item.get('candidatePath')],
|
||||
'cycleGap': cycle_gap, 'countBasis': count_basis,
|
||||
'refreshGap': refresh_gap,
|
||||
'refreshDue': bool(source_gap is not None and refresh_gap and source_gap >= refresh_gap)
|
||||
or (selection_basis == 'state.applicationCandidate'
|
||||
and bool(state.get('pendingSourceHistoryIncomplete'))),
|
||||
'selectionBasis': selection_basis,
|
||||
'newerImplementationHandoff': ((source_gap > 0) if source_gap is not None
|
||||
else (latest_at is not None and latest_at > modified)),
|
||||
'sourceParity': 'unknown_not_checked', 'taskAssociation': 'candidate does not declare task; local directory reference only'}
|
||||
current_focus = next((focus for focus in focuses if focus.get('id') == state.get('focus')), None)
|
||||
source_idle = number(state.get('sourceIdleSeconds'))
|
||||
last_source_age = number(state.get('lastSourceChangeAgeSeconds'))
|
||||
alerts = []
|
||||
if health != 'normal':
|
||||
alerts.append(health)
|
||||
if provider.get('reconnecting') or (number(provider.get('consecutiveFailures')) or 0) > 0:
|
||||
alerts.append('provider_retry_or_reconnect')
|
||||
if runner_active := (state.get('activeArtifactCommandCount') or 0):
|
||||
alerts.append('artifact_command_active')
|
||||
if candidate and candidate.get('refreshDue'):
|
||||
alerts.append('candidate_refresh_due')
|
||||
if (source_idle is not None and source_idle > 180
|
||||
and state.get('activity') not in {'command_execution', 'artifact_generation'}):
|
||||
alerts.append('source_idle_over_3m')
|
||||
if last_source_age is not None and last_source_age > 600:
|
||||
alerts.append('source_progress_stale')
|
||||
if current_focus and ((number(current_focus.get('noProgressRounds')) or 0) >= 2 or
|
||||
(number(current_focus.get('repeatedBlockerRounds')) or 0) >= 2):
|
||||
alerts.append('focus_repeated_no_progress')
|
||||
done_count = sum(stage['status'] == 'reported_implemented_unverified' for stage in stages)
|
||||
ui_plan = obj(delivery.get('uiParityExecution'))
|
||||
ui_modules = []
|
||||
ownership = obj(ui_plan.get('moduleGapOwnership'))
|
||||
for module in rows(delivery.get('uiParityModules')):
|
||||
handoff = obj(module.get('progressHandoff'))
|
||||
latest = obj(handoff.get('latestImplementation'))
|
||||
if latest.get('reportPath'):
|
||||
source_updates.append({**latest, 'moduleId': module.get('id'),
|
||||
'stageId': current.get('id'), 'summary': latest.get('completedRoutes', [''])[0]})
|
||||
focus = module.get('focus')
|
||||
items = concrete_gaps.get(focus, [])
|
||||
gap_ids = ownership.get(module.get('id'), module.get('gapIds', [])) or []
|
||||
selected = [item for item in items if item.get('id') in set(gap_ids)]
|
||||
handoff = obj(module.get('progressHandoff'))
|
||||
counts = {
|
||||
'implemented_unverified': sum(item.get('status') == 'implemented_unverified' for item in selected),
|
||||
'locate': sum(item.get('phase') == 'locate' and item.get('status') != 'implemented_unverified' for item in selected),
|
||||
'open': sum(item.get('status') != 'implemented_unverified' and item.get('phase') != 'locate' for item in selected),
|
||||
}
|
||||
if not selected:
|
||||
if handoff:
|
||||
status = handoff.get('status', '已登记,状态未完成')
|
||||
counts = {
|
||||
'implemented_unverified': len(handoff.get('completedRoutes') or []),
|
||||
'locate': 1 if handoff.get('phase') == 'locate' else 0,
|
||||
'open': len(handoff.get('remainingRoutes') or []),
|
||||
}
|
||||
else:
|
||||
status = '尚无模块专属交接'
|
||||
elif counts['open'] == 0 and counts['locate'] == 0:
|
||||
status = '登记子项已补漏,整模块未确认'
|
||||
elif counts['implemented_unverified'] or counts['locate']:
|
||||
status = '部分已补漏/定位中'
|
||||
else:
|
||||
status = '待处理'
|
||||
related = obj(focus_records.get(focus))
|
||||
ui_modules.append({**module, 'status': status, 'counts': counts, 'gaps': selected,
|
||||
'progressHandoff': handoff or None,
|
||||
'scopeComplete': False, 'visualParity': '未有样式对标交接',
|
||||
'relatedHandoffCycle': related.get('cycle') if selected else None,
|
||||
'relatedHandoffPath': related.get('reportPath') if selected else None,
|
||||
'gapStatuses': [{'id': item.get('id'), 'phase': item.get('phase', 'implement'),
|
||||
'status': item.get('status'), 'entry': item.get('entry')}
|
||||
for item in selected]})
|
||||
source_updates.sort(key=lambda item: timestamp(item.get('recordedAt')) or 0, reverse=True)
|
||||
current_module = current.get('moduleId') or assigned_ui_module(current)
|
||||
concrete_open_count = sum(
|
||||
1 for items in concrete_gaps.values() for item in items
|
||||
if item.get('status') != 'implemented_unverified' or item.get('remainingSubroutes')
|
||||
)
|
||||
ui_parity = {**ui_plan, 'modules': ui_modules,
|
||||
# Focus names are implementation roles, not unique UI modules.
|
||||
'currentModule': current_module,
|
||||
'currentModuleSource': ('deliveryStage.assignment' if not current.get('moduleId') and current_module else 'state.moduleId'),
|
||||
'associatedModules': [module.get('id') for module in ui_modules
|
||||
if module.get('focus') == state.get('focus') and module.get('gaps')],
|
||||
'routeFields': ui_plan.get('routeFields', []),
|
||||
'validation': 'NOT_RUN', 'acceptance': 'NOT_CLAIMED'}
|
||||
data.update({'runner': {'recordedState': recorded_state, 'health': health, 'pid': state.get('pid'),
|
||||
'supervisor': supervisor, 'childPid': state.get('childPid'), 'worker': worker,
|
||||
'cycle': cycle, 'focus': state.get('focus'), 'workState': state.get('workState'),
|
||||
'startedAt': current_attempt.get('startedAt'),
|
||||
'assignment': current.get('assignment'),
|
||||
'executionMode': state.get('executionMode'),
|
||||
'sourceProgressDeadlineSeconds': state.get('sourceProgressDeadlineSeconds'),
|
||||
'activity': state.get('activity'), 'noProgressCycles': state.get('noProgressCycles', 0),
|
||||
'elapsedSeconds': state.get('elapsedSeconds'),
|
||||
'lastActionAgeSeconds': state.get('lastActionAgeSeconds'),
|
||||
'activeCommandCount': state.get('activeCommandCount', 0),
|
||||
'observedProductionFiles': state.get('observedProductionFiles', []),
|
||||
'sourceIdleSeconds': state.get('sourceIdleSeconds'),
|
||||
'sourceReadCommands': state.get('sourceReadCommands'),
|
||||
'sourceReadCommandBudget': state.get('sourceReadCommandBudget'),
|
||||
'implementationProgressDeadlineSeconds': state.get('implementationProgressDeadlineSeconds'),
|
||||
'lastSourceChangeAgeSeconds': state.get('lastSourceChangeAgeSeconds'),
|
||||
'workflowContinuation': state.get('workflowContinuation'),
|
||||
'updatedAt': state.get('updatedAt'), 'heartbeatAgeSeconds': age,
|
||||
'reason': state.get('reason'), 'activeArtifactCommands': state.get('activeArtifactCommandCount', 0)},
|
||||
'integrationUrgent': state.get('integrationUrgent'),
|
||||
'progress': {'stagesReportedImplemented': done_count, 'stageTotal': len(stages),
|
||||
'allStagesReportedImplemented': bool(stages) and done_count == len(stages),
|
||||
'featureCompletionPercent': None, 'stages': stages,
|
||||
'currentStage': current.get('id'), 'focuses': focuses,
|
||||
'concreteGaps': concrete_gaps,
|
||||
'openRecordedIssueCount': len(issues),
|
||||
'openConcreteGapCount': concrete_open_count,
|
||||
'scope': '仅统计当前 manifest 固定缺口与最新交接,不代表全部功能比例'},
|
||||
'provider': {'error': provider.get('errorKind'), 'consecutiveFailures': provider.get('consecutiveFailures', 0),
|
||||
'retrySeconds': max(0, int(retry - now)) if retry is not None else 0,
|
||||
'reconnecting': provider.get('reconnecting', False)},
|
||||
'firstGroup': first_group_view, 'latestImplementation': obj(state.get('latestImplementation')), 'recent': recent,
|
||||
'uiParity': ui_parity,
|
||||
'cadence': cadence, 'alerts': alerts, 'sourceUpdates': source_updates,
|
||||
'build': {key: build.get(key) for key in ['cycle', 'status', 'logPath', 'artifactPaths', 'blockingSymbols']},
|
||||
'candidate': candidate, 'validation': state.get('validation', 'UNKNOWN'),
|
||||
'acceptance': state.get('acceptance', 'UNKNOWN')})
|
||||
return data
|
||||
|
||||
|
||||
HEALTH_NAMES = {
|
||||
'normal': '进程存活、心跳更新(不代表功能完成)',
|
||||
'unknown': '缺少运行记录', 'stopped': '执行器已停止',
|
||||
'process_missing': '执行器退出或 PID 已复用',
|
||||
'process_unknown': '无法确认进程身份',
|
||||
'heartbeat_stale': '心跳过期;下方活动为旧记录',
|
||||
'worker_exited_pending_handoff': '工作进程已退出,等待交接',
|
||||
}
|
||||
|
||||
|
||||
def lines(value):
|
||||
return list(dict.fromkeys(line.strip() for line in str(value or '').splitlines() if line.strip()))
|
||||
|
||||
|
||||
def display(data, details=False):
|
||||
print(f"桌面 Blender UI → Web Blender UI | {local_time(data['observedAt'])}")
|
||||
print(f"任务:{data.get('task') or '未知'};实现状态:尚未完整交付")
|
||||
runner = data.get('runner') or {}
|
||||
if not runner:
|
||||
for note in data.get('notes', []):
|
||||
print(f'提示:{note}')
|
||||
return
|
||||
health = runner.get('health')
|
||||
print(f"执行:{STATE_NAMES.get(runner.get('recordedState'), runner.get('recordedState'))};"
|
||||
f"{HEALTH_NAMES.get(health, health)}")
|
||||
print(f"第 {runner.get('cycle')} 轮 | {FOCUS_NAMES.get(runner.get('focus'), runner.get('focus'))} | "
|
||||
f"{WORK_NAMES.get(runner.get('workState'), runner.get('workState') or '活动未记录')}")
|
||||
print(f"本轮记录用时 {duration(runner.get('elapsedSeconds'))};最近动作距心跳 {duration(runner.get('lastActionAgeSeconds'))};"
|
||||
f"心跳距今 {duration(runner.get('heartbeatAgeSeconds'))}")
|
||||
print(f"活动命令 {runner.get('activeCommandCount', 0)};构建/打包 {runner.get('activeArtifactCommands', 0)};"
|
||||
f"本轮观察到生产文件修改 {len(runner.get('observedProductionFiles') or [])} 个")
|
||||
if runner.get('sourceIdleSeconds') is not None:
|
||||
print(f"未观察到源码修改 {duration(runner['sourceIdleSeconds'])}(截至心跳,不单独判为空转)")
|
||||
if runner.get('sourceReadCommands') is not None:
|
||||
print(f"读取观察 {runner.get('sourceReadCommands')} 次;阈值 {runner.get('sourceReadCommandBudget')} 次且无进展计时达 "
|
||||
f"{duration(runner.get('implementationProgressDeadlineSeconds'))} 后,在命令结束处收束;读取数不是硬上限")
|
||||
if runner.get('executionMode'):
|
||||
print('本轮工作:' + {'INTEGRATE_PENDING': '先组合已有源码并交接候选',
|
||||
'IMPLEMENT_FAMILY': '实现同一业务族的具体缺口'}.get(
|
||||
runner['executionMode'], runner['executionMode']))
|
||||
if details:
|
||||
print(f"进程:{runner.get('pid')} [{runner.get('supervisor')}];工作进程:"
|
||||
f"{runner.get('childPid')} [{runner.get('worker')}];记录时间 {local_time(runner.get('updatedAt'))}")
|
||||
if runner.get('startedAt'):
|
||||
print(f"本轮开始:{local_time(runner['startedAt'])}")
|
||||
for path in runner.get('observedProductionFiles') or []:
|
||||
print(f" 本轮改动:{path}")
|
||||
if runner.get('reason'):
|
||||
print(f"执行原因:{runner['reason']}")
|
||||
urgent = data.get('integrationUrgent')
|
||||
if urgent:
|
||||
reason = {'coherent_batch_ready': '业务批次源码已闭合',
|
||||
'source_batch_limit': '待组合批次数到限',
|
||||
'pending_delivery_deferred': '待交付跨轮积压',
|
||||
'source_history_needs_reconciliation': '部分源码交接待对齐'}.get(
|
||||
urgent.get('reason'), '候选刷新到期')
|
||||
print(f"集成优先:{reason};待组合源码轮次 {urgent.get('pendingSourceCycles', [])};先生成匹配候选")
|
||||
ui = data.get('uiParity') or {}
|
||||
module_by_id = {module.get('id'): module for module in ui.get('modules', [])}
|
||||
module_id = ui.get('currentModule')
|
||||
if module_id:
|
||||
print(f"当前模块:{module_id} {module_by_id.get(module_id, {}).get('title', '')}(当前任务指派)")
|
||||
else:
|
||||
print('当前精确模块未记录;不由执行方向推断模块完成度。')
|
||||
progress = data.get('progress') or {}
|
||||
focus = next((item for item in progress.get('focuses', []) if item.get('id') == runner.get('focus')), {})
|
||||
if focus.get('nextAction'):
|
||||
print(f"本批接续(第 {focus.get('cycle')} 轮交接):{focus['nextAction']}")
|
||||
elif runner.get('assignment'):
|
||||
print(f"任务指派:{runner['assignment']}")
|
||||
if details and runner.get('assignment') and focus.get('nextAction'):
|
||||
print(f"任务指派:{runner['assignment']}")
|
||||
|
||||
provider = data.get('provider') or {}
|
||||
if provider.get('retrySeconds') or provider.get('reconnecting') or provider.get('consecutiveFailures'):
|
||||
print(f"服务等待:退避 {duration(provider.get('retrySeconds'))};重连 {'是' if provider.get('reconnecting') else '否'};"
|
||||
f"连续失败 {provider.get('consecutiveFailures', 0)};分类 {provider.get('error') or '未记录'}")
|
||||
for item in progress.get('focuses', []):
|
||||
if item.get('deferredPathSeconds') or item.get('retrySeconds'):
|
||||
print(f"路径暂缓:{item.get('title')} / {item.get('deferredPathId') or '未记录路径'};"
|
||||
f"暂缓剩余 {duration(item.get('deferredPathSeconds', 0))},重试剩余 {duration(item.get('retrySeconds', 0))}")
|
||||
if (number(focus.get('noProgressRounds')) or 0) >= 2 or (number(focus.get('repeatedBlockerRounds')) or 0) >= 2:
|
||||
print(f"推进警示:当前方向连续无源码进展 {focus.get('noProgressRounds', 0)} 轮;"
|
||||
f"重复阻塞 {focus.get('repeatedBlockerRounds', 0)} 轮。")
|
||||
|
||||
print('\n最新交接(源码实现报告,未验证):')
|
||||
latest = data.get('latestImplementation') or {}
|
||||
if latest:
|
||||
stamp = timestamp(latest.get('at'))
|
||||
age = (timestamp(data.get('observedAt')) or 0) - stamp if stamp is not None else None
|
||||
print(f" 最近调度器源码交接:第 {latest.get('cycle')} 轮,{local_time(latest.get('at'))},距今 {duration(age)}")
|
||||
print(f" {latest.get('businessEntry') or latest.get('workflow') or '未提供业务入口'}")
|
||||
else:
|
||||
print(' 尚无调度器源码交接记录。')
|
||||
for item in data.get('recent', []):
|
||||
measured = duration(item.get('elapsedSeconds'))
|
||||
if item.get('processRecorded') and (item.get('exitCode') != 0 or item.get('reason')):
|
||||
label = f"退出 {item.get('exitCode')} / {item.get('reason') or '原因未记录'};报告未视为完成"
|
||||
else:
|
||||
label = '已写交接' if item.get('processRecorded') else '仅结果记录,进程交接未记录'
|
||||
print(f" [{item['cycle']}] {item.get('businessEntry') or item.get('summary') or '未提供业务入口'};{label};工作进程用时 {measured}")
|
||||
if details:
|
||||
print(f" {item.get('summary') or '未提供摘要'}")
|
||||
print(f" 记录:{item.get('reportPath')}")
|
||||
for item in data.get('sourceUpdates', []):
|
||||
print(f" [根会话补缺 {local_time(item.get('recordedAt'))}] {item.get('summary')}")
|
||||
if item.get('candidatePath'):
|
||||
print(f" 候选:{item['candidatePath']};部署:{item.get('deployment', 'not_performed')};验证:{item.get('validation', 'NOT_RUN')}")
|
||||
if details:
|
||||
print(f" 记录:{item.get('reportPath')}")
|
||||
|
||||
print('\n最新记录余项(不是全功能缺口总表):')
|
||||
displayed = False
|
||||
# Show the active batch first. Completed fixed IDs remain in JSON/details;
|
||||
# an old fixed inventory is not the current executable work list.
|
||||
focuses = sorted(progress.get('focuses', []), key=lambda item: item.get('id') != runner.get('focus'))
|
||||
for item in focuses:
|
||||
gaps = progress.get('concreteGaps', {}).get(item.get('id'), [])
|
||||
open_gaps = [gap for gap in gaps if gap.get('status') != 'implemented_unverified' or gap.get('remainingSubroutes')]
|
||||
known_routes = []
|
||||
for gap in open_gaps:
|
||||
finding = obj(gap.get('latestFinding'))
|
||||
residuals = gap.get('remainingSubroutes') or lines(finding.get('reason') or gap.get('missing'))
|
||||
known_routes.extend(residuals)
|
||||
displayed = True
|
||||
origin = f"第 {finding['cycle']} 轮" if finding.get('cycle') is not None else gap.get('statusSource', 'manifest')
|
||||
print(f" [{gap.get('id')}] {gap.get('entry')}(来源:{origin})")
|
||||
for residual in residuals:
|
||||
for line in lines(residual):
|
||||
print(f" {line}")
|
||||
if details:
|
||||
print(f" 下一步:{finding.get('action') or gap.get('nextAction') or '未记录'}")
|
||||
if gap.get('handoffReportPath'):
|
||||
print(f" 交接:{gap['handoffReportPath']}")
|
||||
# Native drawing/region work often uses DW IDs outside the old 7-item
|
||||
# inventory. Keep these current residuals instead of showing only G01.
|
||||
for route in item.get('remainingRoutes') or []:
|
||||
if route in known_routes:
|
||||
continue
|
||||
if any(gap.get('id') and gap['id'] in route for gap in gaps):
|
||||
continue
|
||||
displayed = True
|
||||
print(f" [{item.get('title')},第 {item.get('cycle')} 轮] {route}")
|
||||
if details:
|
||||
for gap in gaps:
|
||||
if gap not in open_gaps:
|
||||
print(f" [{gap.get('id')}] 已报告实现、未验证:{gap.get('entry')}")
|
||||
if not displayed:
|
||||
print(' 当前交接未列明确余项;不能据此认定整条 UI 主线完成。')
|
||||
no_handoff = [module for module in ui.get('modules', [])
|
||||
if not module.get('gaps') and not module.get('progressHandoff')]
|
||||
if no_handoff:
|
||||
print(' 尚无模块专属进度记录:' + '、'.join(module['id'] for module in no_handoff) + '(状态未知,不显示零进度)')
|
||||
if details:
|
||||
for module in ui.get('modules', []):
|
||||
print(f" 模块 {module.get('id')} / {module.get('title')}:{module.get('status')};完整样式与入口对标未确认")
|
||||
|
||||
candidate = data.get('candidate')
|
||||
print('\n应用候选与源码集成:')
|
||||
if candidate:
|
||||
print(f" 调度器指定候选:{candidate.get('path')}")
|
||||
pending = candidate.get('pendingSourceCycles')
|
||||
if pending is None:
|
||||
print(' 待组合源码批次:未记录;不使用轮次差估算。')
|
||||
else:
|
||||
cycle_list = '、'.join(map(str, pending)) or '无'
|
||||
print(f" 调度器待组合 {len(pending)} 批:{cycle_list}")
|
||||
if candidate.get('pendingHistoryIncomplete'):
|
||||
print(' 待组合历史不完整,不能认定候选已包含所有源码。')
|
||||
external = candidate.get('externalIntegrationUnconfirmed') or []
|
||||
if external:
|
||||
print(' 另有根会话补缺未记录候选归属:' + '、'.join(item.get('id', '未知') for item in external))
|
||||
if candidate.get('refreshDue'):
|
||||
print(' 已达到记录中的候选刷新条件。')
|
||||
print(f" 部署记录:{candidate.get('deployment') or '未知'};源码包含关系及运行结果未验证。")
|
||||
else:
|
||||
print(' 当前任务尚无可读取候选记录。')
|
||||
build = data.get('build') or {}
|
||||
if details or build.get('blockingSymbols') or build.get('status') in {'failed', 'blocked'}:
|
||||
print(f" 最近构件交接:第 {build.get('cycle', '—')} 轮 / {build.get('status') or '未记录'}")
|
||||
if build.get('logPath'):
|
||||
print(f" 构件日志:{build['logPath']}")
|
||||
for symbol in (build.get('blockingSymbols') or [])[:8]:
|
||||
print(f" 构建卡点:{symbol}")
|
||||
print(f"验证:{data.get('validation')};验收:{data.get('acceptance')}。轮次、心跳和构建不计作功能完成。")
|
||||
for note in data.get('notes', []):
|
||||
print(f'提示:{note}')
|
||||
|
||||
|
||||
def interval_value(value):
|
||||
interval = float(value)
|
||||
if not math.isfinite(interval) or interval < 1:
|
||||
raise argparse.ArgumentTypeError('刷新间隔必须是至少 1 秒的有限数字')
|
||||
return interval
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="查看 Blender Web 移植真实进度")
|
||||
parser.add_argument("--json", action="store_true", help="输出机器可读 JSON")
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('--details', action='store_true', help='展开源码/进程/交接/已实现条目;默认只显示当前执行与实际余项')
|
||||
parser.add_argument('--json', action='store_true', help='输出 JSON;配合 --watch 时每行一个快照')
|
||||
parser.add_argument('--watch', action='store_true', help='持续刷新,Ctrl+C 退出')
|
||||
parser.add_argument('--interval', type=interval_value, default=5, metavar='秒', help='刷新间隔,默认 5 秒')
|
||||
parser.add_argument('--recent', type=int, choices=range(0, 11), default=3, metavar='0..10', help='显示最近交接条数,默认 3,最多回看 24 轮')
|
||||
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}")
|
||||
try:
|
||||
while True:
|
||||
data = snapshot(args.recent)
|
||||
if args.json:
|
||||
print(json.dumps(data, ensure_ascii=False, indent=None if args.watch else 2), flush=True)
|
||||
else:
|
||||
if args.watch and sys.stdout.isatty():
|
||||
print('\033[2J\033[H', end='')
|
||||
display(data, details=args.details)
|
||||
sys.stdout.flush()
|
||||
if not args.watch:
|
||||
return 0 if data.get('task') else 1
|
||||
time.sleep(args.interval)
|
||||
except (KeyboardInterrupt, BrokenPipeError):
|
||||
return 0
|
||||
|
||||
if __name__ == "__main__": main()
|
||||
|
||||
if __name__ == '__main__':
|
||||
raise SystemExit(main())
|
||||
|
||||
Reference in New Issue
Block a user