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>
706 lines
42 KiB
Python
706 lines
42 KiB
Python
#!/usr/bin/env python3
|
||
"""只读查看 Blender → Web Blender UI 对标执行状态与最新功能交接。
|
||
|
||
不会启动执行器、构建、部署或运行功能验证;候选源码对应关系仍明确标记为未知。
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
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 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=__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()
|
||
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__':
|
||
raise SystemExit(main())
|