diff --git a/.scratch/submission-limit-classification/issues/01-implementation.md b/.scratch/submission-limit-classification/issues/01-implementation.md new file mode 100644 index 0000000..2846d85 --- /dev/null +++ b/.scratch/submission-limit-classification/issues/01-implementation.md @@ -0,0 +1,11 @@ +# 分离 Alpha 检查和提交限制 +Type: task +Status: ready-for-agent + +用户已批准实现。保留 REGULAR_SUBMISSION 原始证据,将其从 Alpha 失败统计及待定判定中分离;HTTP/MCP 提供分类摘要,页面单独展示缓存限制;迁移仅重算历史 check_type。未知检查保持保守,不推断实时额度、恢复时间或正式提交资格。 + +验证:分类边界、模拟平台检查到持久化和 MCP/HTTP 返回、研究筛选、历史迁移、后端检查及前端构建。 + +## 完成记录 + +已实现并完成本地验证:后端 471 项全量测试通过;最终调整后 23 项专项测试通过;Ruff 与 git diff --check 通过;前端构建通过;Alpha 管理和工作空间 3 项浏览器回归通过。迁移 0018 在隔离 SQLite 中验证 503 条记录的升级、降级、再升级和原始证据保留。尚未部署或对业务数据库执行迁移;Docker 后端部署启动时按既有流程执行 alembic upgrade head。 diff --git a/backend/app/alphas.py b/backend/app/alphas.py index 3ccfebb..4e32de3 100644 --- a/backend/app/alphas.py +++ b/backend/app/alphas.py @@ -7,6 +7,7 @@ from datetime import datetime from sqlalchemy import or_, select, update from .models import Alpha, Research, ResearchTag, SelfCorrelation, now +from .platform_checks import split_checks, submission_limits from .research.provenance import source_alpha_ids METRIC_FIELDS = ( @@ -16,23 +17,24 @@ METRIC_FIELDS = ( def failed_checks(checks): - """Return failed platform check names; local correlation never changes this list.""" + """Return failed Alpha check names, excluding submission limits and local correlation.""" return [ check.get("name") if isinstance(check.get("name"), str) else "未命名检查" - for check in checks if isinstance(check, dict) and check.get("result") == "FAIL" + for check in split_checks(checks)[0] if isinstance(check, dict) and check.get("result") == "FAIL" ] if isinstance(checks, list) else [] def snapshot_columns(settings, metrics, checks): """Derive list fields from a platform snapshot, preserving missing metrics as null. - Only explicit FAIL results count. Empty, malformed and unfinished checks are + Submission limits are excluded. Only explicit Alpha FAIL results count. + Empty, malformed and unfinished Alpha checks are pending; all known checks passing without PROD_CORRELATION is only a pre-check. No submission eligibility or activity eligibility is inferred here. """ settings = settings if isinstance(settings, dict) else {} metrics = metrics if isinstance(metrics, dict) else {} - checks = checks if isinstance(checks, list) else [] + checks, _ = split_checks(checks) valid = [check for check in checks if isinstance(check, dict)] failures = len(failed_checks(checks)) by_name = {check["name"]: check for check in valid if isinstance(check.get("name"), str)} @@ -64,6 +66,15 @@ def snapshot_columns(settings, metrics, checks): } +def check_summary(checks): + """Separate cached Alpha findings from submission limits; infer no live eligibility.""" + return { + "check_type": snapshot_columns({}, {}, checks)["check_type"], + "failed_checks": failed_checks(checks), + "submission_limits": submission_limits(checks), + } + + def submission_condition(submission): """Match the platform list contract; a missing status is never assumed submitted.""" return Alpha.status == "UNSUBMITTED" if submission == "UNSUBMITTED" else Alpha.status != "UNSUBMITTED" diff --git a/backend/app/mcp_api/server.py b/backend/app/mcp_api/server.py index 3d43c56..a72c4be 100644 --- a/backend/app/mcp_api/server.py +++ b/backend/app/mcp_api/server.py @@ -24,7 +24,7 @@ TOOLS = { "search_data_preparations": (c.PreparationSearch, "preparations", "research:read", "分页查询数据准备集合,返回固定范围、字段数及版本。研究可使用多个集合,各集合范围独立。"), "get_data_preparation": (c.PreparationRead, "preparation", "research:read", "按集合 ID 与版本分页预览字段、类型、描述和数据集归属。提交回测时携带 preparation_refs,由服务端核对版本并固定独立输入快照;空集合不能用于研究。"), "create_research_template": (c.CreateTemplate, "create_template", "research:write", "将调用方大模型研究后自行总结的参数化模板保存到模板工坊,供用户后续批量回测。先用 get_backtest_results 阅读实际指标和检查,选择 1–20 个已完成采集的 source_item_ids,并说明 hypothesis;不要把 completed 当作检查通过。template 使用 {name} 占位符及逐一对应的 variables,字段变量须声明 MATRIX/VECTOR/GROUP,VECTOR 聚合须明确写入表达式。提供唯一名称和 idempotency_key,可附 reference。返回模板 ID、版本和理论组合数;仅核验结构及来源,不验证所有参数组合,不再次调用模型、不执行回测、不覆盖已有模板。"), - "get_submission_check": (c.SelfCorrelationReference, "submission_check_context", "research:read", "读取已导入 Alpha 的表达式、Description、snapshot 和缓存检查结果;不发起检查。先核对或生成三段 Description,再调用 check_submission。"), + "get_submission_check": (c.SelfCorrelationReference, "submission_check_context", "research:read", "读取已导入 Alpha 的表达式、Description、snapshot 和缓存检查结果;check_summary 分离 Alpha 检查和 REGULAR_SUBMISSION 提交限制,原始 checks 保留;限制不代表当前额度。不发起检查。先核对或生成三段 Description,再调用 check_submission。"), "check_submission": (c.SubmissionCheck, "check_submission", "research:refresh", "对单个待提交 Alpha 写回已获用户授权的 Description 并调用平台 GET /check,返回 job_id。须先用 get_submission_check 获取 snapshot;保留本地自相关门槛和冲突保护。通过 get_refresh_job 查进度、get_submission_check 读结果。无论检查结果如何,都不会调用 /submit 或正式提交 Alpha。"), "get_worldquant_connection": (c.ConnectionReference, "connection", "research:read", "读取 WorldQuant 连接状态及可选认证 job_id 的进度,不发起认证;人工验证在网页完成。"), "authenticate_worldquant": (c.Authentication, "authenticate", "research:refresh", "使用服务端已保存凭据连接或重新认证 WorldQuant,返回 job_id;action=connect(默认)或人工验证后 verify。用 get_worldquant_connection 查询,不接收密码,不修改账户配置。"), @@ -38,7 +38,7 @@ TOOLS = { "search_backtests": (c.History, "history", "research:read", "分页查历史候选与固定设置;candidates 按完整输入精确匹配,不推断数学等价。"), "submit_backtests": (c.Submit, "submit", "backtests:execute", "执行用户已授权的固定批次,自动留痕并立即返回运行 ID。可携带 preparation_refs 选择集合,版本变化须重新读取;每项必须完整设置;重复默认拒绝,rerun 明确重跑。不需要研究资产。"), "get_backtest": (c.RunReference, "run", "research:read", "读取真实运行进度、提交数量和可选增量事件;受理不等于成功。"), - "get_backtest_results": (c.Results, "results", "research:read", "分页读取固定快照指标、全部非通过检查及三层状态;缺失指标不补零。"), + "get_backtest_results": (c.Results, "results", "research:read", "分页读取固定快照指标、Alpha 非通过检查及三层状态;REGULAR_SUBMISSION 单列 submission_limits,不计入 Alpha 失败统计。缺失指标不补零。"), "get_backtest_artifact": (c.Artifact, "artifact", "research:read", "分页读取候选脱敏快照的顶层键值或独立采集的 PnL;缺缓存不自动刷新。"), "control_backtest": (c.Control, "control", "backtests:control", "对已授权运行暂停、继续、停止或恢复采集;不远程取消、不重提未知模拟。需要版本和幂等键。"), } diff --git a/backend/app/platform_checks.py b/backend/app/platform_checks.py new file mode 100644 index 0000000..bcfe0da --- /dev/null +++ b/backend/app/platform_checks.py @@ -0,0 +1,23 @@ +"""Classify platform evidence without discarding unknown checks or inferring eligibility.""" + + +def is_submission_limit(check): + """Recognize only the confirmed account-limit check; unknown names remain Alpha checks.""" + return isinstance(check, dict) and check.get("name") == "REGULAR_SUBMISSION" + + +def split_checks(checks): + """Return Alpha checks and submission limits, retaining malformed Alpha evidence.""" + items = checks if isinstance(checks, list) else [] + return ([c for c in items if not is_submission_limit(c)], + [c for c in items if is_submission_limit(c)]) + + +def submission_limits(checks): + """Summarize the observed limit, never the account's current allowance or reset time.""" + _, limits = split_checks(checks) + status = "blocked" if any(c.get("result") == "FAIL" for c in limits) else ( + "not_blocked" if limits and all(c.get("result") == "PASS" for c in limits) else "unknown" + ) + return {"status": status, "checks": limits, + "meaning": "仅反映缓存观测时的提交限制,不代表当前额度或正式提交资格"} diff --git a/backend/app/research/evaluations.py b/backend/app/research/evaluations.py index 06ee42e..1542c25 100644 --- a/backend/app/research/evaluations.py +++ b/backend/app/research/evaluations.py @@ -7,6 +7,7 @@ from sqlalchemy import select from ..backtests.service import Backtests, uid from ..models import Alpha, ResearchEvaluation, ResearchExperiment, SelfCorrelation +from ..platform_checks import split_checks, submission_limits from .experiments import Experiments from .serialization import encode_snapshot as jsonable_encoder @@ -30,13 +31,14 @@ def assess(snapshot, rules): evidence.append( {"metric": key, "value": value, "bound": bound, "direction": direction, "status": status} ) - checks = metrics.get("checks") or [] + raw_checks = metrics.get("checks") or [] + checks, _ = split_checks(raw_checks) if not checks: missing.append("platform_checks") for check in checks: - if check.get("result") == "FAIL": + if isinstance(check, dict) and check.get("result") == "FAIL": failed.append(f"platform:{check.get('name', 'unknown')}") - unknown_checks = [check for check in checks if check.get("result") not in ("PASS", "FAIL")] + unknown_checks = [check for check in checks if not isinstance(check, dict) or check.get("result") not in ("PASS", "FAIL")] if unknown_checks: missing.append("unresolved_platform_checks") return { @@ -44,7 +46,8 @@ def assess(snapshot, rules): "evidence": evidence, "failed": failed, "missing": missing, - "existing_platform_checks": checks, + "existing_platform_checks": raw_checks, + "submission_limits": submission_limits(raw_checks), "meaning": "本地研究筛选结果,不是官方提交资格", } diff --git a/backend/app/research_access/queries.py b/backend/app/research_access/queries.py index cdd20bd..fd2fc28 100644 --- a/backend/app/research_access/queries.py +++ b/backend/app/research_access/queries.py @@ -7,6 +7,7 @@ from sqlalchemy import func, select from ..alphas import number, sanitize from ..backtests.contracts import fingerprint from ..models import BacktestItem, BacktestResult, BacktestRun, Pnl +from ..platform_checks import is_submission_limit from ..research.serialization import encode_snapshot @@ -26,6 +27,8 @@ def checks_summary(snapshot): if "checks" in snapshot: raw = snapshot["checks"] checks.extend({"section": "root", "raw": c} for c in (raw if isinstance(raw, list) else [raw])) + submission_checks = [c for c in checks if is_submission_limit(c["raw"])] + checks = [c for c in checks if not is_submission_limit(c["raw"])] counts = Counter({key: 0 for key in ("PASS", "FAIL", "PENDING", "WARNING", "UNKNOWN")}) non_pass = [] for check in checks: @@ -36,7 +39,9 @@ def checks_summary(snapshot): if state != "PASS": non_pass.append({**check, "status": state}) return {"status": "unknown" if not checks else "reported", "counts": dict(counts), - "total": len(checks), "non_pass": non_pass} + "total": len(checks), "non_pass": non_pass, + "submission_limits": submission_checks, + "meaning": "Alpha 检查统计不含提交限制;限制为快照观测,不代表实时提交资格"} def item_summary(item, result): diff --git a/backend/app/research_access/service.py b/backend/app/research_access/service.py index 732b841..2bef551 100644 --- a/backend/app/research_access/service.py +++ b/backend/app/research_access/service.py @@ -9,6 +9,7 @@ from uuid import uuid4 from sqlalchemy import func, select +from ..alphas import check_summary from ..backtests.contracts import ControlInput, DraftInput, PreviewInput, Source, StartInput, fingerprint from ..backtests.service import Backtests from ..business import Business @@ -221,7 +222,7 @@ class ResearchAccess: return {"alpha_id": args.alpha_id, "snapshot": submission_fingerprint(context), **context, "descriptions": {key: item["description"] for key, item in context["sections"].items()}, "can_check": alpha.status == "UNSUBMITTED" and await correlation_allows_check(self.db, args.alpha_id), - "checks": alpha.checks, "source": "local_cache", "production_submission": False, + "checks": alpha.checks, "check_summary": check_summary(alpha.checks), "source": "local_cache", "production_submission": False, "job_id": job.id if job else None, "job_status": job.status if job else None, "checked_at": job.checkpoint.get("checked_at") if job else None} diff --git a/backend/app/submission.py b/backend/app/submission.py index 15c55fa..ff1d720 100644 --- a/backend/app/submission.py +++ b/backend/app/submission.py @@ -15,7 +15,7 @@ from pydantic_ai.usage import UsageLimits from sqlalchemy import select from .ai.provider import public_error -from .alphas import code, sanitize, snapshot_columns, submission_condition +from .alphas import check_summary, code, sanitize, snapshot_columns, submission_condition from .jobs import ACTIVE from .models import Account, AISettings, Alpha, Job, JobItem, SelfCorrelation, now from .schemas import Contract, JobOutput, valid_ids @@ -206,6 +206,8 @@ def router(runner, ai): ) return { "snapshot": fingerprint(context), + "checks": alpha.checks, + "check_summary": check_summary(alpha.checks), "sections": context["sections"], "descriptions": {key: item["description"] for key, item in context["sections"].items()}, "model": config.description_model, diff --git a/backend/migrations/versions/0018_submission_limit_classification.py b/backend/migrations/versions/0018_submission_limit_classification.py new file mode 100644 index 0000000..9ade333 --- /dev/null +++ b/backend/migrations/versions/0018_submission_limit_classification.py @@ -0,0 +1,55 @@ +"""Reclassify cached Alpha checks without changing upstream evidence.""" + +import sqlalchemy as sa +from alembic import op + +revision = "0018" +down_revision = "0017" +branch_labels = None +depends_on = None + + +def check_type(checks, separate_limits): + """Frozen classification for reversible data migration; never import mutable app code.""" + checks = checks if isinstance(checks, list) else [] + if separate_limits: + checks = [c for c in checks if not (isinstance(c, dict) and c.get("name") == "REGULAR_SUBMISSION")] + valid = [c for c in checks if isinstance(c, dict)] + failures = sum(c.get("result") == "FAIL" for c in valid) + if failures: + return "FAIL_1" if failures == 1 else "FAIL_2" + if not checks or len(valid) != len(checks) or any(c.get("result") != "PASS" for c in valid): + return "PENDING" + return "PASS" if any(c.get("name") == "PROD_CORRELATION" for c in valid) else "PRE_CHECK" + + +def reclassify(separate_limits): + """Update only affected derived columns, in bounded batches; raw snapshots stay intact.""" + table = sa.table("alphas", sa.column("id", sa.String()), sa.column("checks", sa.JSON()), + sa.column("check_type", sa.String())) + connection = op.get_bind() + last_id = None + while True: + query = sa.select(table.c.id, table.c.checks).order_by(table.c.id).limit(500) + if last_id is not None: + query = query.where(table.c.id > last_id) + rows = connection.execute(query).mappings().all() + if not rows: + break + updates = [ + {"snapshot_id": row["id"], "classification": check_type(row["checks"], separate_limits)} + for row in rows if isinstance(row["checks"], list) and any( + isinstance(c, dict) and c.get("name") == "REGULAR_SUBMISSION" for c in row["checks"]) + ] + if updates: + connection.execute(table.update().where(table.c.id == sa.bindparam("snapshot_id")) + .values(check_type=sa.bindparam("classification")), updates) + last_id = rows[-1]["id"] + + +def upgrade(): + reclassify(True) + + +def downgrade(): + reclassify(False) diff --git a/backend/tests/test_mcp_submission.py b/backend/tests/test_mcp_submission.py index 21d1418..f34e305 100644 --- a/backend/tests/test_mcp_submission.py +++ b/backend/tests/test_mcp_submission.py @@ -12,10 +12,13 @@ from tests.test_submission import FIELDS, Description, setup @pytest.mark.parametrize("kind", ["REGULAR", "SUPER"]) @pytest.mark.parametrize("result", ["PASS", "FAIL"]) -async def test_mcp_check_never_submits(mcp_app, kind, result, monkeypatch): +@pytest.mark.parametrize("limited", [False, True]) +async def test_mcp_check_never_submits(mcp_app, kind, result, limited, monkeypatch): from tests import test_submission checks = [{"name": "PROD_CORRELATION", "result": result}] + if limited: + checks.append({"name": "REGULAR_SUBMISSION", "result": "FAIL"}) monkeypatch.setattr(test_submission, "CHECKS", checks) sections = ["regular"] if kind == "REGULAR" else ["selection", "combo"] platform = await setup(mcp_app, alpha(type=kind, **{s: {"code": "rank(close)"} for s in sections})) @@ -35,6 +38,8 @@ async def test_mcp_check_never_submits(mcp_app, kind, result, monkeypatch): assert job["status"] == "completed", job data = await invoke(mcp_app, principal, "get_submission_check", {"alpha_id": "alpha1"}) assert data["checks"] == checks and data["checked_at"] + assert data["check_summary"]["check_type"] == ("PASS" if result == "PASS" else "FAIL_1") + assert data["check_summary"]["submission_limits"]["status"] == ("blocked" if limited else "unknown") assert data["production_submission"] is False assert platform.patches == [{s: {"description": args["descriptions"][s]} for s in sections}] assert platform.calls.count(("GET", "/alphas/alpha1/check")) == 2 diff --git a/backend/tests/test_submission_limit_migration.py b/backend/tests/test_submission_limit_migration.py new file mode 100644 index 0000000..e773cd2 --- /dev/null +++ b/backend/tests/test_submission_limit_migration.py @@ -0,0 +1,51 @@ +"""Upgrade cached classifications across batches while preserving platform evidence.""" + +from pathlib import Path + +import sqlalchemy as sa +from alembic import command +from alembic.config import Config +from cryptography.fernet import Fernet + +from app.models import now + + +def test_limit_migration_is_reversible_and_preserves_snapshots(tmp_path, monkeypatch): + path = tmp_path / "limits.db" + monkeypatch.setenv("DATABASE_URL", f"sqlite+aiosqlite:///{path}") + monkeypatch.setenv("ADMIN_PASSWORD", "migration-test-only") + monkeypatch.setenv("ENCRYPTION_KEY", Fernet.generate_key().decode()) + monkeypatch.setenv("WQ_EMAIL", "") + monkeypatch.setenv("WQ_PASSWORD", "") + root = Path(__file__).resolve().parents[1] + config = Config(str(root / "alembic.ini")) + config.set_main_option("script_location", str(root / "migrations")) + command.upgrade(config, "0017") + engine = sa.create_engine(f"sqlite:///{path}") + table = sa.Table("alphas", sa.MetaData(), autoload_with=engine) + limit = {"name": "REGULAR_SUBMISSION", "result": "FAIL"} + patterns = [ + ([{"name": "PROD_CORRELATION", "result": "PASS"}, limit], "FAIL_1", "PASS"), + ([{"name": "LOW_SHARPE", "result": "FAIL"}, limit], "FAIL_2", "FAIL_1"), + ([limit], "FAIL_1", "PENDING"), + ([{"name": "LOW_SHARPE", "result": "PASS"}, limit], "FAIL_1", "PRE_CHECK"), + ([{"name": "UNKNOWN", "result": "FAIL"}], "FAIL_1", "FAIL_1"), + ] + with engine.begin() as db: + db.execute(table.insert(), [ + {"id": f"old{i:04}", "hidden": False, "settings": {}, "os_metrics": {}, + "is_metrics": {"checks": patterns[i % 5][0]}, "checks": patterns[i % 5][0], + "check_type": patterns[i % 5][1], "synced_at": now(), + "raw": {"is": {"checks": patterns[i % 5][0]}}} + for i in range(503) + ]) + for version, position in [("0018", 2), ("0017", 1), ("0018", 2)]: + (command.upgrade if version == "0018" else command.downgrade)(config, version) + with engine.connect() as db: + rows = db.execute(sa.select(table).order_by(table.c.id)).mappings().all() + assert len(rows) == 503 + for i, row in enumerate(rows): + assert row["check_type"] == patterns[i % 5][position] + assert row["checks"] == row["is_metrics"]["checks"] == row["raw"]["is"]["checks"] == patterns[i % 5][0] + command.check(config) + engine.dispose() diff --git a/backend/tests/test_submission_limits.py b/backend/tests/test_submission_limits.py new file mode 100644 index 0000000..23f8882 --- /dev/null +++ b/backend/tests/test_submission_limits.py @@ -0,0 +1,76 @@ +"""Submission limits must not disqualify an otherwise passing Alpha.""" + +import pytest + +from app.alphas import failed_checks, snapshot_columns + +LIMIT = {"name": "REGULAR_SUBMISSION", "result": "FAIL", "value": 4, "limit": 4} + + +@pytest.mark.parametrize("checks,expected", [ + ([{"name": "PROD_CORRELATION", "result": "PASS"}], "PASS"), + ([{"name": "LOW_SHARPE", "result": "PASS"}], "PRE_CHECK"), + ([{"name": "LOW_SHARPE", "result": "FAIL"}], "FAIL_1"), + ([], "PENDING"), + ([None], "PENDING"), + ([{"name": "UNKNOWN_CHECK", "result": "FAIL"}], "FAIL_1"), + ([{"name": "PROD_CORRELATION", "result": "PENDING"}], "PENDING"), +]) +def test_limit_does_not_change_alpha_verdict(checks, expected): + original = [*checks, LIMIT] + assert snapshot_columns({}, {}, original)["check_type"] == expected + assert "REGULAR_SUBMISSION" not in failed_checks(original) + assert original[-1] == LIMIT + + +@pytest.mark.parametrize("result", ["PASS", "PENDING", "WARNING", "UNRECOGNIZED"]) +def test_all_limit_states_are_separate(result): + from app.alphas import check_summary + + checks = [{"name": "PROD_CORRELATION", "result": "PASS"}, {**LIMIT, "result": result}] + summary = check_summary(checks) + assert summary["check_type"] == "PASS" + assert summary["submission_limits"]["status"] == ("not_blocked" if result == "PASS" else "unknown") + + +def test_research_and_mcp_evidence_keep_limits_separate(): + from types import SimpleNamespace + + from app.research.evaluations import assess + from app.research_access.queries import checks_summary + + snapshot = {"is": {"sharpe": 2, "fitness": 2, "turnover": 0.1, + "checks": [{"name": "PROD_CORRELATION", "result": "PASS"}, LIMIT]}} + rules = SimpleNamespace(sharpe_min=1, fitness_min=1, turnover_max=0.5) + finding = assess(snapshot, rules) + assert finding["verdict"] == "pass" and finding["failed"] == [] + assert finding["existing_platform_checks"] == snapshot["is"]["checks"] + assert finding["submission_limits"]["status"] == "blocked" + summary = checks_summary(snapshot) + assert summary["counts"]["FAIL"] == 0 and summary["total"] == 1 + assert summary["non_pass"] == [] + assert summary["submission_limits"] == [{"section": "is", "raw": LIMIT}] + snapshot["is"]["checks"] = [LIMIT] + assert assess(snapshot, rules)["verdict"] == "review" + assert checks_summary(snapshot)["status"] == "unknown" + + +async def test_http_check_persists_limit_without_failing_alpha(app, logged_in, monkeypatch): + from app.models import Alpha + from tests import test_submission + + checks = [{"name": "PROD_CORRELATION", "result": "PASS"}, LIMIT] + monkeypatch.setattr(test_submission, "CHECKS", checks) + await test_submission.setup(app) + response = await test_submission.enqueue(logged_in) + assert response.status_code == 202 + await app.state.runner.execute(response.json()["id"]) + async with app.state.sessions() as db: + item = await db.get(Alpha, "alpha1") + assert item.check_type == "PASS" + assert item.checks == item.is_metrics["checks"] == item.raw["is"]["checks"] == checks + state = (await logged_in.get("/api/v1/alphas/alpha1/submission")).json() + assert state["job"]["status"] == "completed" + assert state["check_summary"]["submission_limits"]["status"] == "blocked" + rows = (await logged_in.get("/api/v1/alphas?check_type=PASS")).json()["items"] + assert rows[0]["id"] == "alpha1" and rows[0]["failed_checks"] == [] diff --git a/frontend/src/components/AlphaDetail.tsx b/frontend/src/components/AlphaDetail.tsx index 12e9548..b06b4bb 100644 --- a/frontend/src/components/AlphaDetail.tsx +++ b/frontend/src/components/AlphaDetail.tsx @@ -474,16 +474,46 @@ function MetricTable({ } /** IS and OS checks are separate observations; missing values never imply a pass. */ -function PlatformChecks({ title, checks }: { title: string; checks: unknown }) { +function PlatformChecks({ + title, + checks, + submissionLimit = false, +}: { + title: string; + checks: unknown; + submissionLimit?: boolean; +}) { const rows = Array.isArray(checks) ? checks.filter( (item): item is Record => item !== null && typeof item === "object" && !Array.isArray(item), ) : []; + const limits = rows.filter((check) => check.name === "REGULAR_SUBMISSION"); + const alphaChecks = rows.filter( + (check) => check.name !== "REGULAR_SUBMISSION", + ); + if (!submissionLimit && limits.length) { + return ( + <> + + + + ); + } return (

{title}

+ {submissionLimit && ( +

+ 以下为缓存观测时的提交限制,不计入 Alpha + 失败项;当前额度需重新检查确认。 +

+ )} {rows.length ? ( rows.map((check, i) => (
@@ -496,17 +526,23 @@ function PlatformChecks({ title, checks }: { title: string; checks: unknown }) { check.result === "PASS" ? "green" : check.result === "FAIL" - ? "red" + ? submissionLimit + ? "orange" + : "red" : check.result === "WARNING" ? "orange" : "grey" } > - {check.result === "WARNING" - ? "警告(WARNING)" - : check.result === "PENDING" - ? "待定(PENDING)" - : displayValue(check.result)} + {submissionLimit && check.result === "FAIL" + ? "提交受限(FAIL)" + : submissionLimit && check.result === "PASS" + ? "当时未受限(PASS)" + : check.result === "WARNING" + ? "警告(WARNING)" + : check.result === "PENDING" + ? "待定(PENDING)" + : displayValue(check.result)}
))