Add full capability readiness matrix

This commit is contained in:
2026-06-30 14:37:02 +08:00
parent d9ea249ff0
commit 0c239483a9
8 changed files with 570 additions and 11 deletions

View File

@@ -67,6 +67,10 @@ The runtime panel calls `GET /api/system/readiness` and verifies the conda
imports required for the backend/task environment and the full MMSeg/mmcv imports required for the backend/task environment and the full MMSeg/mmcv
environment. Command-line verification is available with environment. Command-line verification is available with
`PYTHONPATH=backend conda run -n seg_smp python scripts/verify_runtime_envs.py --refresh`. `PYTHONPATH=backend conda run -n seg_smp python scripts/verify_runtime_envs.py --refresh`.
The capability matrix calls `GET /api/capabilities` and summarizes readiness
for Dataset, SegModel, YOLO, MMSeg, visual tools, analysis, and system
operations, including task coverage, runtime status, uploaded datasets,
heatmap/segmentation artifacts, training curves, and weight manifest status.
The same panel can run `POST /api/acceptance/smoke`, a lightweight live smoke The same panel can run `POST /api/acceptance/smoke`, a lightweight live smoke
that creates an upload dataset, uploads a label, downloads it through the that creates an upload dataset, uploads a label, downloads it through the
@@ -144,6 +148,8 @@ Use `GET /api/catalog` to inspect supported models, algorithms, datasets, and
task types discovered from the existing `Seg/` workspace. task types discovered from the existing `Seg/` workspace.
Use `GET /api/coverage` to inspect script-to-task coverage and task Use `GET /api/coverage` to inspect script-to-task coverage and task
buildability. buildability.
Use `GET /api/capabilities` to inspect the grouped full-function readiness
matrix used by the web dashboard and agents.
Use `GET /api/results/curves` to inspect parsed training curves discovered Use `GET /api/results/curves` to inspect parsed training curves discovered
from YOLO, SegModel, MMSeg, visual-tool, and analysis output directories. from YOLO, SegModel, MMSeg, visual-tool, and analysis output directories.
@@ -155,10 +161,10 @@ Run the local evaluation and validation agents before publishing changes:
PYTHONPATH=backend conda run -n seg_smp python scripts/run_agents.py --build PYTHONPATH=backend conda run -n seg_smp python scripts/run_agents.py --build
``` ```
The validation agent checks catalog coverage, the `seg_smp` task env, the The validation agent checks catalog coverage, the grouped capability matrix,
`seg_mmcv` MMSeg env, runtime import readiness, GPU visibility, no-weight Git the `seg_smp` task env, the `seg_mmcv` MMSeg env, runtime import readiness,
safety, backend tests, frontend build, and live backend/frontend endpoints GPU visibility, no-weight Git safety, backend tests, frontend build, and live
when the services are running. With live validation enabled it also runs the backend/frontend endpoints when the services are running. With live validation
lightweight acceptance smoke above. By default it also runs the deep training enabled it also runs the lightweight acceptance smoke above. By default it
acceptance; set `SEG_VALIDATE_DEEP=0` when a quick non-training validation also runs the deep training acceptance; set `SEG_VALIDATE_DEEP=0` when a quick
pass is needed. non-training validation pass is needed.

View File

@@ -44,10 +44,12 @@ def evaluate_project() -> dict:
"loss_result_ui": "loss" in frontend_text.lower() and "heatmap" in frontend_text.lower() and "CurvePanel" in frontend_text, "loss_result_ui": "loss" in frontend_text.lower() and "heatmap" in frontend_text.lower() and "CurvePanel" in frontend_text,
"job_progress_ui": "JobProgressBar" in frontend_text and "progressTrack" in frontend_text, "job_progress_ui": "JobProgressBar" in frontend_text and "progressTrack" in frontend_text,
"runtime_readiness_ui": "runtimeReadiness" in frontend_text and "环境就绪" in frontend_text, "runtime_readiness_ui": "runtimeReadiness" in frontend_text and "环境就绪" in frontend_text,
"capability_matrix_ui": "capabilities" in frontend_text and "全功能矩阵" in frontend_text,
"dataset_api": "/api/datasets" in backend_text and "api_upload_dataset_files" in backend_text, "dataset_api": "/api/datasets" in backend_text and "api_upload_dataset_files" in backend_text,
"dataset_quality_api": "/api/datasets/{dataset_name}/validate" in backend_text and "/api/datasets/{dataset_name}/yolo-yaml" in backend_text, "dataset_quality_api": "/api/datasets/{dataset_name}/validate" in backend_text and "/api/datasets/{dataset_name}/yolo-yaml" in backend_text,
"job_progress_api": "progress_from_log_path" in backend_text and '"progress"' in backend_text, "job_progress_api": "progress_from_log_path" in backend_text and '"progress"' in backend_text,
"runtime_readiness_api": "/api/system/readiness" in backend_text, "runtime_readiness_api": "/api/system/readiness" in backend_text,
"capability_matrix_api": "/api/capabilities" in backend_text,
"runtime_bootstrap_scripts": (settings.project_root / "scripts" / "bootstrap_conda_envs.sh").exists() "runtime_bootstrap_scripts": (settings.project_root / "scripts" / "bootstrap_conda_envs.sh").exists()
and (settings.project_root / "scripts" / "verify_runtime_envs.py").exists(), and (settings.project_root / "scripts" / "verify_runtime_envs.py").exists(),
"curve_api": "/api/results/curves" in backend_text, "curve_api": "/api/results/curves" in backend_text,

View File

@@ -9,6 +9,7 @@ import urllib.request
from pathlib import Path from pathlib import Path
from ..acceptance import run_deep_acceptance, run_live_acceptance from ..acceptance import run_deep_acceptance, run_live_acceptance
from ..capabilities import get_capability_matrix
from ..catalog import get_catalog from ..catalog import get_catalog
from ..config import settings from ..config import settings
from ..coverage import get_coverage_report from ..coverage import get_coverage_report
@@ -66,6 +67,12 @@ def validate_project(run_build: bool = False) -> dict:
checks.append({"name": "mmseg_env_exists", "passed": settings.mmseg_conda_env in env_names, "detail": {"env": settings.mmseg_conda_env}}) checks.append({"name": "mmseg_env_exists", "passed": settings.mmseg_conda_env in env_names, "detail": {"env": settings.mmseg_conda_env}})
runtime_readiness = get_runtime_readiness(force=True) runtime_readiness = get_runtime_readiness(force=True)
checks.append({"name": "runtime_env_readiness", "passed": runtime_readiness["passed"], "detail": runtime_readiness}) checks.append({"name": "runtime_env_readiness", "passed": runtime_readiness["passed"], "detail": runtime_readiness})
capability_matrix = get_capability_matrix()
checks.append({
"name": "capability_matrix_ready",
"passed": capability_matrix["passed"] and capability_matrix["summary"]["ready_domains"] == capability_matrix["summary"]["total_domains"],
"detail": capability_matrix,
})
smoke = _run( smoke = _run(
[ [
@@ -105,6 +112,7 @@ def validate_project(run_build: bool = False) -> dict:
datasets = _fetch(f"{backend_url}/api/datasets") datasets = _fetch(f"{backend_url}/api/datasets")
live_jobs = _fetch(f"{backend_url}/api/jobs") live_jobs = _fetch(f"{backend_url}/api/jobs")
live_readiness = _fetch(f"{backend_url}/api/system/readiness") live_readiness = _fetch(f"{backend_url}/api/system/readiness")
live_capabilities = _fetch(f"{backend_url}/api/capabilities")
live_coverage = _fetch(f"{backend_url}/api/coverage") live_coverage = _fetch(f"{backend_url}/api/coverage")
live_curves = _fetch(f"{backend_url}/api/results/curves") live_curves = _fetch(f"{backend_url}/api/results/curves")
frontend = _fetch(frontend_url) frontend = _fetch(frontend_url)
@@ -124,6 +132,11 @@ def validate_project(run_build: bool = False) -> dict:
"passed": live_readiness["passed"] and '"passed":true' in live_readiness.get("body", "").replace(" ", ""), "passed": live_readiness["passed"] and '"passed":true' in live_readiness.get("body", "").replace(" ", ""),
"detail": live_readiness, "detail": live_readiness,
}) })
checks.append({
"name": "live_capability_matrix_api",
"passed": live_capabilities["passed"] and '"passed":true' in live_capabilities.get("body", "").replace(" ", ""),
"detail": live_capabilities,
})
checks.append({"name": "live_coverage_api", "passed": live_coverage["passed"] and '"task_build_passed":true' in live_coverage.get("body", "").replace(" ", ""), "detail": live_coverage}) checks.append({"name": "live_coverage_api", "passed": live_coverage["passed"] and '"task_build_passed":true' in live_coverage.get("body", "").replace(" ", ""), "detail": live_coverage})
checks.append({"name": "live_training_curves_api", "passed": live_curves["passed"] and live_curves.get("body", "").lstrip().startswith("["), "detail": live_curves}) checks.append({"name": "live_training_curves_api", "passed": live_curves["passed"] and live_curves.get("body", "").lstrip().startswith("["), "detail": live_curves})
checks.append({"name": "live_frontend_index", "passed": frontend["passed"] and "Seg Data Server" in frontend.get("body", ""), "detail": frontend}) checks.append({"name": "live_frontend_index", "passed": frontend["passed"] and "Seg Data Server" in frontend.get("body", ""), "detail": frontend})

299
backend/app/capabilities.py Normal file
View File

@@ -0,0 +1,299 @@
from __future__ import annotations
import time
from typing import Any
from .acceptance import latest_acceptance_report, latest_deep_acceptance_report
from .catalog import get_catalog
from .coverage import get_coverage_report
from .modules.dataset.service import list_uploaded_datasets
from .modules.results.service import scan_results, scan_training_curves
from .modules.system.service import get_gpus, get_runtime_readiness
from .modules.weights.service import load_manifest
CAPABILITY_GROUPS = [
{
"id": "dataset",
"label": "Dataset",
"description": "上传图片、label、mask并执行预处理、配对、叠加、拼接、视频抽帧和 YOLO 数据构建。",
"required_tasks": [
"dataset.rename",
"dataset.to_png",
"dataset.resize",
"dataset.pair",
"dataset.rebuild_labels",
"dataset.stack",
"dataset.stitch",
"dataset.video_frames",
"dataset.yolo_txt_sort",
"dataset.yolo_convert_png",
"dataset.yolo_resize",
],
"task_prefixes": ["dataset."],
"evidence_roles": ["segmentation"],
},
{
"id": "segmodel",
"label": "SegModel",
"description": "SMP/SegModel 单模型和批量训练、预测、raw mask、指标、FLOPs/FPS。",
"required_tasks": [
"segmodel.train",
"segmodel.batch_train",
"segmodel.predict",
"segmodel.batch_predict",
"segmodel.flops",
"segmodel.benchmark",
"segmodel.raw_mask_check",
"segmodel.metrics",
],
"task_prefixes": ["segmodel."],
"runtime_roles": ["backend_task"],
"families": ["segmodel"],
},
{
"id": "yolo",
"label": "YOLO",
"description": "YOLOv8/v9/v11/v12 分割训练、上传数据集训练、预测、热度图、对比、视频预测和 raw mask。",
"required_tasks": [
"yolo.train",
"yolo.train_custom",
"yolo.predict",
"yolo.batch_predict",
"yolo.heatmap",
"yolo.compare",
"yolo.raw_mask_check",
"yolo.video_visible",
"yolo.video_unvisible",
],
"task_prefixes": ["yolo."],
"runtime_roles": ["backend_task"],
"families": ["yolo"],
"evidence_roles": ["heatmap", "segmentation", "curve", "video"],
},
{
"id": "mmseg",
"label": "MMSeg",
"description": "MMSeg 数据/算法配置生成、31 个算法、训练、预测、指标、FLOPs/FPS、绘图和 loss/mIoU 提取。",
"required_tasks": [
"mmseg.generate_data",
"mmseg.generate_alg",
"mmseg.train",
"mmseg.predict_v1",
"mmseg.predict_v2",
"mmseg.metrics",
"mmseg.flops_fps",
"mmseg.draw",
"mmseg.extract_loss_miou",
],
"task_prefixes": ["mmseg."],
"runtime_roles": ["mmseg_full"],
"families": ["mmseg"],
},
{
"id": "visual",
"label": "Visual Tools",
"description": "可视化工具训练、推理、FPS、YOLO11 热度图、label 转换和 8-bit PNG 生成。",
"required_tasks": [
"visual.train",
"visual.inference",
"visual.fps",
"visual.yolo11_heatmap_v1",
"visual.yolo11_heatmap_v2",
"visual.deal_labels",
],
"task_prefixes": ["visual."],
"runtime_roles": ["backend_task"],
"families": ["tool"],
"evidence_roles": ["heatmap", "segmentation"],
},
{
"id": "analysis",
"label": "Analysis",
"description": "合并 SegModel/MMSeg/YOLO 指标,生成 CSV、表格、图表和性能摘要。",
"required_tasks": ["analysis.all"],
"task_prefixes": ["analysis."],
"families": ["analysis"],
},
{
"id": "system",
"label": "System",
"description": "GPU 查询、环境检查、权重同步、任务日志/进度、取消和备份入口。",
"required_tasks": ["system.backup", "system.check_graph_card"],
"task_prefixes": ["system.", "mock."],
"runtime_roles": ["backend_task", "mmseg_full"],
},
]
def _tasks_for_group(all_tasks: list[str], prefixes: list[str]) -> list[str]:
return sorted(task for task in all_tasks if any(task.startswith(prefix) for prefix in prefixes))
def _artifact_matches(item: dict[str, Any], group: dict[str, Any]) -> bool:
families = set(group.get("families", []))
roles = set(group.get("evidence_roles", []))
return (not families or item.get("family") in families) and (not roles or item.get("role") in roles)
def _coverage_build_task_set(coverage: dict[str, Any]) -> set[str]:
return {item["task"] for item in coverage.get("task_build_checks", []) if item.get("passed")}
def _runtime_map(readiness: dict[str, Any]) -> dict[str, dict[str, Any]]:
return {item["role"]: item for item in readiness.get("envs", [])}
def get_capability_matrix() -> dict[str, Any]:
catalog = get_catalog()
coverage = get_coverage_report()
readiness = get_runtime_readiness(force=False)
results = scan_results(limit=1000)
curves = scan_training_curves(limit=50)
datasets = list_uploaded_datasets()
manifest = load_manifest()
gpus = get_gpus()
acceptance = latest_acceptance_report()
deep_acceptance = latest_deep_acceptance_report()
all_tasks = catalog["task_types"]
buildable_tasks = _coverage_build_task_set(coverage)
runtime_by_role = _runtime_map(readiness)
domains = []
for group in CAPABILITY_GROUPS:
group_tasks = _tasks_for_group(all_tasks, group["task_prefixes"])
required_tasks = group["required_tasks"]
missing_required = [task for task in required_tasks if task not in all_tasks]
unbuildable_required = [task for task in required_tasks if task in all_tasks and task not in buildable_tasks]
runtime_roles = group.get("runtime_roles", [])
runtime_reports = [runtime_by_role.get(role) for role in runtime_roles if runtime_by_role.get(role)]
runtime_ready = all(item.get("passed") for item in runtime_reports) if runtime_roles else True
artifacts = [item for item in results if _artifact_matches(item, group)]
group_curves = [item for item in curves if not group.get("families") or item.get("family") in group["families"]]
evidence_count = len(artifacts) + len(group_curves)
if group["id"] == "dataset":
evidence_count += sum(dataset["counts"]["images"] + dataset["counts"]["labels"] + dataset["counts"]["masks"] for dataset in datasets)
if group["id"] == "mmseg":
evidence_count += len(catalog.get("mmseg_algorithms", []))
if group["id"] == "system":
evidence_count += int(bool(gpus.get("available"))) + int(manifest.get("count", 0) > 0)
gaps = []
if missing_required:
gaps.append(f"missing tasks: {', '.join(missing_required[:4])}")
if unbuildable_required:
gaps.append(f"unbuildable tasks: {', '.join(unbuildable_required[:4])}")
if runtime_roles and not runtime_ready:
gaps.append("runtime env check failed")
domains.append(
{
"id": group["id"],
"label": group["label"],
"description": group["description"],
"ready": not missing_required and not unbuildable_required and runtime_ready,
"tasks": {
"total": len(group_tasks),
"required": len(required_tasks),
"required_ready": len(required_tasks) - len(missing_required) - len(unbuildable_required),
"examples": group_tasks[:10],
"missing_required": missing_required,
"unbuildable_required": unbuildable_required,
},
"runtime": [
{"role": item["role"], "name": item["name"], "passed": item["passed"]}
for item in runtime_reports
],
"evidence": {
"count": evidence_count,
"artifacts": [
{
"name": item["name"],
"relative_path": item["relative_path"],
"role": item.get("role"),
"family": item.get("family"),
}
for item in artifacts[:6]
],
"curves": [
{
"name": item["name"],
"relative_path": item["relative_path"],
"family": item.get("family"),
"row_count": item.get("row_count"),
}
for item in group_curves[:4]
],
},
"gaps": gaps,
}
)
requirements = [
{
"id": "user_script_mapping",
"label": "用户侧脚本映射",
"passed": coverage["mapped_user_scripts"] == coverage["user_scripts_total"] and not coverage["unmapped_user_scripts"],
"detail": f"{coverage['mapped_user_scripts']}/{coverage['user_scripts_total']}",
},
{
"id": "runtime_readiness",
"label": "运行环境就绪",
"passed": readiness.get("passed", False),
"detail": ", ".join(f"{item['name']}={item['passed']}" for item in readiness.get("envs", [])),
},
{
"id": "dataset_upload",
"label": "数据集上传/Label/Mask",
"passed": len(datasets) >= 1,
"detail": f"{len(datasets)} uploaded dataset(s)",
},
{
"id": "yolo_heatmap",
"label": "YOLO 热度图证据",
"passed": any(item.get("family") == "yolo" and item.get("role") == "heatmap" for item in results),
"detail": "heatmap artifact discovered",
},
{
"id": "training_curves",
"label": "训练 loss/指标曲线",
"passed": len(curves) >= 1,
"detail": f"{len(curves)} curve file(s)",
},
{
"id": "deep_acceptance",
"label": "深度训练验收",
"passed": bool(deep_acceptance.get("passed")),
"detail": deep_acceptance.get("run_id", "not run"),
},
{
"id": "weights_manifest",
"label": "权重清单",
"passed": manifest.get("count", 0) >= 1,
"detail": f"{manifest.get('count', 0)} weights indexed",
},
]
ready_domains = sum(1 for item in domains if item["ready"])
return {
"generated_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
"passed": ready_domains == len(domains) and all(item["passed"] for item in requirements),
"summary": {
"ready_domains": ready_domains,
"total_domains": len(domains),
"mapped_user_scripts": coverage["mapped_user_scripts"],
"user_scripts_total": coverage["user_scripts_total"],
"task_build_passed": coverage["task_build_passed"],
"uploaded_datasets": len(datasets),
"artifacts": len(results),
"curves": len(curves),
"weights": manifest.get("count", 0),
"gpus_available": bool(gpus.get("available")),
"acceptance_passed": bool(acceptance.get("passed")),
"deep_acceptance_passed": bool(deep_acceptance.get("passed")),
},
"requirements": requirements,
"domains": domains,
}

View File

@@ -10,6 +10,7 @@ from fastapi.responses import FileResponse, StreamingResponse
from . import db from . import db
from .acceptance import latest_acceptance_report, latest_deep_acceptance_report, run_deep_acceptance, run_live_acceptance from .acceptance import latest_acceptance_report, latest_deep_acceptance_report, run_deep_acceptance, run_live_acceptance
from .capabilities import get_capability_matrix
from .catalog import get_catalog from .catalog import get_catalog
from .config import settings from .config import settings
from .coverage import get_coverage_report from .coverage import get_coverage_report
@@ -86,6 +87,11 @@ def api_coverage() -> dict:
return get_coverage_report() return get_coverage_report()
@app.get("/api/capabilities")
def api_capabilities() -> dict:
return get_capability_matrix()
@app.get("/api/acceptance/latest") @app.get("/api/acceptance/latest")
def api_acceptance_latest() -> dict: def api_acceptance_latest() -> dict:
return latest_acceptance_report() return latest_acceptance_report()

View File

@@ -0,0 +1,21 @@
from app.capabilities import get_capability_matrix
def test_capability_matrix_covers_core_domains():
matrix = get_capability_matrix()
domains = {item["id"]: item for item in matrix["domains"]}
assert {"dataset", "segmodel", "yolo", "mmseg", "visual", "analysis", "system"} <= set(domains)
assert domains["dataset"]["tasks"]["required_ready"] == domains["dataset"]["tasks"]["required"]
assert domains["yolo"]["ready"] is True
assert domains["mmseg"]["ready"] is True
def test_capability_matrix_tracks_user_requirements():
matrix = get_capability_matrix()
requirements = {item["id"]: item for item in matrix["requirements"]}
assert requirements["user_script_mapping"]["passed"] is True
assert requirements["runtime_readiness"]["passed"] is True
assert requirements["yolo_heatmap"]["passed"] is True
assert requirements["training_curves"]["passed"] is True

View File

@@ -193,6 +193,44 @@ type RuntimeReadinessPayload = {
}; };
}; };
type CapabilityDomain = {
id: string;
label: string;
ready: boolean;
tasks: {
total: number;
required: number;
required_ready: number;
examples: string[];
missing_required: string[];
unbuildable_required: string[];
};
runtime: Array<{ role: string; name: string; passed: boolean }>;
evidence: {
count: number;
artifacts: Array<{ name: string; relative_path: string; role?: string; family?: string }>;
curves: Array<{ name: string; relative_path: string; family?: string; row_count?: number }>;
};
gaps: string[];
};
type CapabilityPayload = {
passed: boolean;
generated_at: string;
summary: {
ready_domains: number;
total_domains: number;
mapped_user_scripts: number;
user_scripts_total: number;
uploaded_datasets: number;
artifacts: number;
curves: number;
weights: number;
};
requirements: Array<{ id: string; label: string; passed: boolean; detail: string }>;
domains: CapabilityDomain[];
};
async function api<T>(path: string, init?: RequestInit): Promise<T> { async function api<T>(path: string, init?: RequestInit): Promise<T> {
const res = await fetch(`${API_BASE}${path}`, { const res = await fetch(`${API_BASE}${path}`, {
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
@@ -266,14 +304,16 @@ function useData() {
const [acceptance, setAcceptance] = useState<AcceptancePayload | null>(null); const [acceptance, setAcceptance] = useState<AcceptancePayload | null>(null);
const [deepAcceptance, setDeepAcceptance] = useState<DeepAcceptancePayload | null>(null); const [deepAcceptance, setDeepAcceptance] = useState<DeepAcceptancePayload | null>(null);
const [runtimeReadiness, setRuntimeReadiness] = useState<RuntimeReadinessPayload | null>(null); const [runtimeReadiness, setRuntimeReadiness] = useState<RuntimeReadinessPayload | null>(null);
const [capabilities, setCapabilities] = useState<CapabilityPayload | null>(null);
const [error, setError] = useState<string>(""); const [error, setError] = useState<string>("");
async function refresh() { async function refresh() {
try { try {
const [catalogNext, gpusNext, readinessNext, jobsNext, resultsNext, curvesNext, datasetsNext, coverageNext, acceptanceNext, deepAcceptanceNext] = await Promise.all([ const [catalogNext, gpusNext, readinessNext, capabilitiesNext, jobsNext, resultsNext, curvesNext, datasetsNext, coverageNext, acceptanceNext, deepAcceptanceNext] = await Promise.all([
api<Catalog>("/api/catalog"), api<Catalog>("/api/catalog"),
api<GpuPayload>("/api/system/gpus"), api<GpuPayload>("/api/system/gpus"),
api<RuntimeReadinessPayload>("/api/system/readiness"), api<RuntimeReadinessPayload>("/api/system/readiness"),
api<CapabilityPayload>("/api/capabilities"),
api<Job[]>("/api/jobs"), api<Job[]>("/api/jobs"),
api<ResultItem[]>("/api/results"), api<ResultItem[]>("/api/results"),
api<TrainingCurve[]>("/api/results/curves"), api<TrainingCurve[]>("/api/results/curves"),
@@ -285,6 +325,7 @@ function useData() {
setCatalog(catalogNext); setCatalog(catalogNext);
setGpus(gpusNext); setGpus(gpusNext);
setRuntimeReadiness(readinessNext); setRuntimeReadiness(readinessNext);
setCapabilities(capabilitiesNext);
setJobs(jobsNext); setJobs(jobsNext);
setResults(resultsNext.slice(0, 80)); setResults(resultsNext.slice(0, 80));
setCurves(curvesNext.slice(0, 12)); setCurves(curvesNext.slice(0, 12));
@@ -316,7 +357,7 @@ function useData() {
return () => window.clearInterval(timer); return () => window.clearInterval(timer);
}, []); }, []);
return { catalog, gpus, runtimeReadiness, jobs, results, curves, datasets, datasetValidations, coverage, acceptance, deepAcceptance, error, refresh }; return { catalog, gpus, runtimeReadiness, capabilities, jobs, results, curves, datasets, datasetValidations, coverage, acceptance, deepAcceptance, error, refresh };
} }
function StatusPill({ status }: { status: string }) { function StatusPill({ status }: { status: string }) {
@@ -339,7 +380,7 @@ function JobProgressBar({ progress }: { progress?: JobProgress }) {
} }
function App() { function App() {
const { catalog, gpus, runtimeReadiness, jobs, results, curves, datasets, datasetValidations, coverage, acceptance, deepAcceptance, error, refresh } = useData(); const { catalog, gpus, runtimeReadiness, capabilities, jobs, results, curves, datasets, datasetValidations, coverage, acceptance, deepAcceptance, error, refresh } = useData();
const [taskType, setTaskType] = useState("mock.echo"); const [taskType, setTaskType] = useState("mock.echo");
const [params, setParams] = useState(JSON.stringify(defaultParams["mock.echo"], null, 2)); const [params, setParams] = useState(JSON.stringify(defaultParams["mock.echo"], null, 2));
const [selectedJob, setSelectedJob] = useState<Job | null>(null); const [selectedJob, setSelectedJob] = useState<Job | null>(null);
@@ -529,6 +570,7 @@ function App() {
</div> </div>
<nav> <nav>
<a href="#jobs"><Terminal size={18} /></a> <a href="#jobs"><Terminal size={18} /></a>
<a href="#capabilities"><Gauge size={18} /></a>
<a href="#datasets"><Boxes size={18} /></a> <a href="#datasets"><Boxes size={18} /></a>
<a href="#gpu"><Cpu size={18} />GPU</a> <a href="#gpu"><Cpu size={18} />GPU</a>
<a href="#runtime"><ShieldCheck size={18} /></a> <a href="#runtime"><ShieldCheck size={18} /></a>
@@ -579,6 +621,48 @@ function App() {
</div> </div>
</section> </section>
<section className="panel" id="capabilities">
<div className="panelHead">
<div>
<p className="eyebrow">Capability Matrix</p>
<h2></h2>
</div>
<StatusPill status={capabilities?.passed ? "success" : "queued"} />
</div>
<div className="capSummary">
<div><span></span><strong>{capabilities?.summary.ready_domains ?? 0}/{capabilities?.summary.total_domains ?? 0}</strong></div>
<div><span></span><strong>{capabilities?.summary.mapped_user_scripts ?? 0}/{capabilities?.summary.user_scripts_total ?? 0}</strong></div>
<div><span></span><strong>{capabilities?.summary.uploaded_datasets ?? 0}</strong></div>
<div><span></span><strong>{capabilities?.summary.artifacts ?? 0}</strong></div>
<div><span>线</span><strong>{capabilities?.summary.curves ?? 0}</strong></div>
<div><span></span><strong>{capabilities?.summary.weights ?? 0}</strong></div>
</div>
<div className="capabilityGrid">
{(capabilities?.domains ?? []).map((domain) => (
<div key={domain.id} className={`capCard ${domain.ready ? "ok" : "bad"}`}>
<div className="capHead">
<strong>{domain.label}</strong>
<span>{domain.ready ? "READY" : "CHECK"}</span>
</div>
<div className="capNumbers">
<span>{domain.tasks.required_ready}/{domain.tasks.required} required</span>
<span>{domain.evidence.count} evidence</span>
</div>
<div className="capTags">
{domain.tasks.examples.slice(0, 4).map((task) => <small key={task}>{task}</small>)}
</div>
</div>
))}
</div>
<div className="requirementStrip">
{(capabilities?.requirements ?? []).map((item) => (
<span key={item.id} className={item.passed ? "ok" : "bad"} title={item.detail}>
{item.label}
</span>
))}
</div>
</section>
<section className="grid two" id="jobs"> <section className="grid two" id="jobs">
<div className="panel taskPanel"> <div className="panel taskPanel">
<div className="panelHead"> <div className="panelHead">

View File

@@ -186,6 +186,131 @@ h2 {
margin-bottom: 16px; margin-bottom: 16px;
} }
.capSummary {
display: grid;
grid-template-columns: repeat(6, minmax(0, 1fr));
gap: 10px;
margin-bottom: 12px;
}
.capSummary div {
min-width: 0;
padding: 10px;
border-radius: 7px;
border: 1px solid var(--line);
background: #101310;
}
.capSummary span,
.capSummary strong {
display: block;
}
.capSummary span {
color: var(--muted);
font-size: 11px;
}
.capSummary strong {
margin-top: 3px;
font-size: 20px;
}
.capabilityGrid {
display: grid;
grid-template-columns: repeat(7, minmax(0, 1fr));
gap: 9px;
}
.capCard {
min-width: 0;
display: grid;
gap: 8px;
padding: 10px;
border-radius: 7px;
border: 1px solid var(--line);
background: #101310;
}
.capCard.ok {
border-color: rgba(157, 226, 111, 0.32);
}
.capCard.bad {
border-color: rgba(240, 113, 103, 0.55);
}
.capHead {
min-width: 0;
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 8px;
}
.capHead strong {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.capHead span {
color: var(--green);
font-size: 11px;
font-weight: 760;
}
.capCard.bad .capHead span {
color: var(--red);
}
.capNumbers {
display: grid;
gap: 3px;
color: var(--muted);
font-size: 11px;
}
.capTags {
display: grid;
gap: 4px;
}
.capTags small {
min-width: 0;
padding: 4px 5px;
border-radius: 4px;
background: #080a08;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.requirementStrip {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-top: 12px;
}
.requirementStrip span {
padding: 5px 7px;
border: 1px solid var(--line);
border-radius: 999px;
color: var(--muted);
background: #101310;
font-size: 11px;
}
.requirementStrip span.ok {
color: var(--green);
border-color: rgba(157, 226, 111, 0.32);
}
.requirementStrip span.bad {
color: var(--red);
border-color: rgba(240, 113, 103, 0.55);
}
.metric, .panel { .metric, .panel {
border: 1px solid var(--line); border: 1px solid var(--line);
background: rgba(21, 24, 21, 0.94); background: rgba(21, 24, 21, 0.94);
@@ -958,7 +1083,7 @@ meter {
} }
nav { nav {
grid-template-columns: repeat(6, minmax(0, 1fr)); grid-template-columns: repeat(4, minmax(0, 1fr));
align-items: center; align-items: center;
} }
@@ -985,8 +1110,11 @@ meter {
} }
.metrics, .metrics,
.capSummary,
.capabilityGrid,
.grid.two, .grid.two,
.grid.three, .grid.three,
.grid.four,
.taskColumns { .taskColumns {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }