This commit is contained in:
+13
-4
@@ -31,16 +31,20 @@ def snapshot_columns(settings, metrics, checks, *, checked=False):
|
||||
Sync snapshots with no failures are PRE_CHECK; a completed explicit /check
|
||||
with no failures is PASS. WARNING/PENDING do not count as failures, matching
|
||||
the legacy workflow. Empty, malformed or unknown results remain PENDING.
|
||||
No submission eligibility or activity eligibility is inferred here.
|
||||
A sole PURE_POWER_POOL_THEME failure is a PPAC candidate, not confirmation
|
||||
of current submission or activity eligibility. Raw failures stay available.
|
||||
"""
|
||||
settings = settings if isinstance(settings, dict) else {}
|
||||
metrics = metrics if isinstance(metrics, dict) else {}
|
||||
blocked = submission_limits(checks)["status"] == "blocked"
|
||||
checks, _ = split_checks(checks)
|
||||
valid = [check for check in checks if isinstance(check, dict)]
|
||||
failures = len(failed_checks(checks))
|
||||
failed_names = failed_checks(checks)
|
||||
failures = len(failed_names)
|
||||
by_name = {check["name"]: check for check in valid if isinstance(check.get("name"), str)}
|
||||
if failures:
|
||||
if failed_names == ["PURE_POWER_POOL_THEME"]:
|
||||
check_type = "PPAC_CANDIDATE"
|
||||
elif failures:
|
||||
check_type = "FAIL_1" if failures == 1 else "FAIL_2"
|
||||
elif not checks or len(valid) != len(checks) or any(check_result(check) not in ("PASS", "WARNING", "PENDING") for check in valid):
|
||||
check_type = "PENDING"
|
||||
@@ -75,7 +79,7 @@ def check_summary(checks, *, check_type):
|
||||
"check_type": check_type,
|
||||
"failed_checks": failed_checks(checks),
|
||||
"submission_limits": submission_limits(checks),
|
||||
"meaning": "PRE_CHECK 为同步无失败项;PASS 为主动检查完成且无失败项。PENDING/WARNING 不算失败,不代表全部检查项 PASS 或当前可提交",
|
||||
"meaning": "PRE_CHECK 为同步无失败项;PASS 为主动检查完成且无失败项;PPAC_CANDIDATE 为唯一失败项是 PURE_POWER_POOL_THEME 的候选。PENDING/WARNING 不算失败,不代表全部检查项 PASS 或当前可提交",
|
||||
}
|
||||
|
||||
|
||||
@@ -199,6 +203,11 @@ def list_statement(filters):
|
||||
query = query.where(Alpha.submission_blocked == filters.submission_blocked)
|
||||
if filters.submission_blocked:
|
||||
query = query.where(submission_condition("UNSUBMITTED"))
|
||||
if filters.ppac_candidate is not None:
|
||||
candidate = Alpha.check_type == "PPAC_CANDIDATE"
|
||||
query = query.where(candidate if filters.ppac_candidate else ~candidate)
|
||||
if filters.ppac_candidate:
|
||||
query = query.where(submission_condition("UNSUBMITTED"))
|
||||
if (filters.local_correlation_status is not None or filters.local_correlation_min is not None
|
||||
or filters.local_correlation_max is not None):
|
||||
# One cache row per Alpha keeps totals/export stable; stale overrides the displayed status.
|
||||
|
||||
@@ -9,7 +9,7 @@ from pydantic import BaseModel, ConfigDict, Field, field_validator, model_valida
|
||||
|
||||
ResearchState = Literal["inbox", "candidate", "optimizing", "archived"]
|
||||
Submission = Literal["UNSUBMITTED", "SUBMITTED"]
|
||||
CheckType = Literal["PENDING", "PRE_CHECK", "PASS", "FAIL_1", "FAIL_2"]
|
||||
CheckType = Literal["PENDING", "PRE_CHECK", "PASS", "PPAC_CANDIDATE", "FAIL_1", "FAIL_2"]
|
||||
SortField = Literal[
|
||||
"id",
|
||||
"name",
|
||||
@@ -74,6 +74,7 @@ class AlphaFilters(Contract):
|
||||
local_correlation_min: float | None = Field(default=None, ge=-1, le=1)
|
||||
local_correlation_max: float | None = Field(default=None, ge=-1, le=1)
|
||||
submission_blocked: bool | None = None
|
||||
ppac_candidate: bool | None = None
|
||||
submission: Submission | None = None
|
||||
source: str | None = Field(default=None, max_length=100)
|
||||
source_reference: str | None = Field(default=None, max_length=200)
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
"""Classify snapshots whose sole Alpha failure is the unopened PPAC theme."""
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
revision = "0022"
|
||||
down_revision = "0021"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# Reuse the indexed check_type column; preserve all raw evidence and check stages.
|
||||
table = sa.table("alphas", sa.column("id", sa.String()), sa.column("checks", sa.JSON()),
|
||||
sa.column("check_type", sa.String(20)))
|
||||
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
|
||||
candidates = []
|
||||
for row in rows:
|
||||
# Freeze the current interpretation instead of importing application code.
|
||||
checks = row["checks"] if isinstance(row["checks"], list) else []
|
||||
failures = [check for check in checks if isinstance(check, dict)
|
||||
and check.get("name") != "REGULAR_SUBMISSION"
|
||||
and isinstance(check.get("result"), str) and check["result"].upper() == "FAIL"]
|
||||
if len(failures) == 1 and failures[0].get("name") == "PURE_POWER_POOL_THEME":
|
||||
candidates.append(row["id"])
|
||||
if candidates:
|
||||
connection.execute(table.update().where(table.c.id.in_(candidates)).values(check_type="PPAC_CANDIDATE"))
|
||||
last_id = rows[-1]["id"]
|
||||
|
||||
|
||||
def downgrade():
|
||||
table = sa.table("alphas", sa.column("check_type", sa.String(20)))
|
||||
op.execute(table.update().where(table.c.check_type == "PPAC_CANDIDATE").values(check_type="FAIL_1"))
|
||||
@@ -0,0 +1,108 @@
|
||||
"""PPAC candidates retain their theme failure and wait for platform eligibility."""
|
||||
|
||||
import copy
|
||||
import csv
|
||||
import io
|
||||
|
||||
import pytest
|
||||
|
||||
from app.alphas import check_summary, snapshot_columns, upsert_alpha
|
||||
from tests import test_submission
|
||||
from tests.conftest import alpha
|
||||
|
||||
THEME = {"name": "PURE_POWER_POOL_THEME", "result": "FAIL"}
|
||||
LIMIT = {"name": "REGULAR_SUBMISSION", "result": "FAIL"}
|
||||
OTHER = {"name": "LOW_SHARPE", "result": "FAIL"}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("checked", [False, True])
|
||||
@pytest.mark.parametrize("checks,expected", [
|
||||
([THEME], "PPAC_CANDIDATE"),
|
||||
([{**THEME, "result": "fail"}], "PPAC_CANDIDATE"),
|
||||
([THEME, LIMIT, {"name": "MATCHES_THEMES", "result": "WARNING"},
|
||||
{"name": "PROD_CORRELATION", "result": "PENDING"}], "PPAC_CANDIDATE"),
|
||||
([THEME, OTHER], "FAIL_2"),
|
||||
([THEME, THEME], "FAIL_2"),
|
||||
([THEME, {"result": "FAIL"}], "FAIL_2"),
|
||||
([OTHER], "FAIL_1"),
|
||||
([{**THEME, "result": "PASS"}, OTHER], "FAIL_1"),
|
||||
([{**THEME, "result": False}], "PENDING"),
|
||||
([{**THEME, "value": False}], "PPAC_CANDIDATE"),
|
||||
([], "PENDING"),
|
||||
([{}], "PENDING"),
|
||||
])
|
||||
def test_only_a_single_explicit_theme_failure_is_a_candidate(checks, expected, checked):
|
||||
original = copy.deepcopy(checks)
|
||||
columns = snapshot_columns({}, {}, checks, checked=checked)
|
||||
assert columns["check_type"] == expected
|
||||
assert checks == original
|
||||
if expected == "PPAC_CANDIDATE":
|
||||
assert check_summary(checks, check_type=expected)["failed_checks"] == ["PURE_POWER_POOL_THEME"]
|
||||
|
||||
|
||||
async def test_ppac_scope_filters_before_pagination_and_export(app, logged_in):
|
||||
async with app.state.sessions.begin() as db:
|
||||
for name, checks, status, region, hidden in [
|
||||
("candidate1", [THEME], "UNSUBMITTED", "USA", False),
|
||||
("candidate2", [THEME, LIMIT], "UNSUBMITTED", "USA", True),
|
||||
("other_region", [THEME], "UNSUBMITTED", "CHN", False),
|
||||
("submitted", [THEME], "ACTIVE", "USA", False),
|
||||
("missing_status", [THEME], None, "USA", False),
|
||||
("two_failures", [THEME, OTHER], "UNSUBMITTED", "USA", False),
|
||||
("other_failure", [OTHER], "UNSUBMITTED", "USA", False),
|
||||
("passed", [{**THEME, "result": "PASS"}], "UNSUBMITTED", "USA", False),
|
||||
("missing", [], "UNSUBMITTED", "USA", False),
|
||||
]:
|
||||
await upsert_alpha(db, alpha(name, status=status, hidden=hidden, settings={"region": region},
|
||||
**{"is": {"checks": checks}}))
|
||||
query = "ppac_candidate=true®ion=USA&sort=id&direction=asc&limit=1&offset=1"
|
||||
response = await logged_in.get(f"/api/v1/alphas?{query}")
|
||||
assert response.status_code == 200, response.text
|
||||
result = response.json()
|
||||
assert result["total"] == 2
|
||||
assert [row["id"] for row in result["items"]] == ["candidate2"]
|
||||
assert result["items"][0]["check_type"] == "PPAC_CANDIDATE"
|
||||
assert result["items"][0]["failed_checks"] == ["PURE_POWER_POOL_THEME"]
|
||||
export = await logged_in.get(f"/api/v1/alphas/export?{query}")
|
||||
rows = list(csv.DictReader(io.StringIO(export.text.lstrip("\ufeff"))))
|
||||
assert [row["id"] for row in rows] == ["candidate1", "candidate2"]
|
||||
assert {row["check_type"] for row in rows} == {"PPAC_CANDIDATE"}
|
||||
assert (await logged_in.get("/api/v1/alphas?ppac_candidate=true&submission=SUBMITTED")).json()["total"] == 0
|
||||
assert (await logged_in.get("/api/v1/alphas?ppac_candidate=true&check_type=FAIL_1")).json()["total"] == 0
|
||||
assert (await logged_in.get("/api/v1/alphas?ppac_candidate=true&submission_blocked=true")).json()["total"] == 1
|
||||
assert (await logged_in.get("/api/v1/alphas?ppac_candidate=true&q=candidate1")).json()["total"] == 1
|
||||
assert (await logged_in.get("/api/v1/alphas?check_type=PPAC_CANDIDATE&submission=UNSUBMITTED")).json()["total"] == 3
|
||||
assert (await logged_in.get("/api/v1/alphas?ppac_candidate=false")).json()["total"] == 4
|
||||
|
||||
saved = await logged_in.post("/api/v1/research/assets", json={
|
||||
"kind": "view", "content": {"name": "候选PPAC", "filters": {
|
||||
"submission": "UNSUBMITTED", "ppac_candidate": True, "region": "USA",
|
||||
}, "columns": ["name", "check_type"]},
|
||||
})
|
||||
assert saved.status_code in (200, 201), saved.text
|
||||
assert saved.json()["content"]["filters"]["ppac_candidate"] is True
|
||||
|
||||
|
||||
async def test_check_and_sync_refresh_candidate_status_without_losing_evidence(app, logged_in, monkeypatch):
|
||||
await test_submission.setup(app, alpha(**{"is": {"checks": [OTHER]}}))
|
||||
for checks, expected in [([THEME, LIMIT], "PPAC_CANDIDATE"), ([{**THEME, "result": "PASS"}], "PASS")]:
|
||||
monkeypatch.setattr(test_submission, "CHECKS", checks)
|
||||
response = await test_submission.enqueue(logged_in)
|
||||
assert response.status_code == 202, response.text
|
||||
await app.state.runner.execute(response.json()["id"])
|
||||
state = (await logged_in.get("/api/v1/alphas/alpha1/submission")).json()
|
||||
assert state["job"]["status"] == "completed"
|
||||
assert state["check_summary"]["check_type"] == expected
|
||||
detail = (await logged_in.get("/api/v1/alphas/alpha1")).json()
|
||||
assert detail["checks"] == checks
|
||||
assert detail["check_type"] == expected
|
||||
assert detail["research"]["note"] == "preserve local research"
|
||||
assert (await logged_in.get("/api/v1/alphas?ppac_candidate=true")).json()["total"] == (expected == "PPAC_CANDIDATE")
|
||||
for checks, expected in [([THEME], "PPAC_CANDIDATE"), ([THEME, OTHER], "FAIL_2")]:
|
||||
async with app.state.sessions.begin() as db:
|
||||
await upsert_alpha(db, alpha(**{"is": {"checks": checks}}))
|
||||
detail = (await logged_in.get("/api/v1/alphas/alpha1")).json()
|
||||
assert detail["check_type"] == expected
|
||||
assert detail["checks"] == checks
|
||||
assert detail["research"]["note"] == "preserve local research"
|
||||
assert (await logged_in.get("/api/v1/alphas?ppac_candidate=true")).json()["total"] == (expected == "PPAC_CANDIDATE")
|
||||
@@ -0,0 +1,55 @@
|
||||
"""Historical PPAC classification changes only matching cached check types."""
|
||||
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import command
|
||||
from alembic.config import Config
|
||||
from cryptography.fernet import Fernet
|
||||
|
||||
|
||||
def test_ppac_backfill_preserves_evidence_across_batches_and_downgrade(tmp_path, monkeypatch):
|
||||
database = tmp_path / "ppac.db"
|
||||
monkeypatch.setenv("DATABASE_URL", f"sqlite+aiosqlite:///{database}")
|
||||
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, "0021")
|
||||
engine = sa.create_engine(f"sqlite:///{database}")
|
||||
alphas = sa.Table("alphas", sa.MetaData(), autoload_with=engine)
|
||||
theme = {"name": "PURE_POWER_POOL_THEME", "result": "FAIL"}
|
||||
other = {"name": "LOW_SHARPE", "result": "FAIL"}
|
||||
patterns = [
|
||||
([theme], "FAIL_1", "PPAC_CANDIDATE"),
|
||||
([{**theme, "result": "fail"}, {"name": "REGULAR_SUBMISSION", "result": "FAIL"}], "FAIL_1", "PPAC_CANDIDATE"),
|
||||
([theme, other], "FAIL_2", "FAIL_2"),
|
||||
([theme, theme], "FAIL_2", "FAIL_2"),
|
||||
([other], "FAIL_1", "FAIL_1"),
|
||||
([{**theme, "result": "PASS"}], "PASS", "PASS"),
|
||||
([{**theme, "result": "WARNING"}], "PRE_CHECK", "PRE_CHECK"),
|
||||
([{}], "PENDING", "PENDING"),
|
||||
(None, "PENDING", "PENDING"),
|
||||
]
|
||||
with engine.begin() as db:
|
||||
db.execute(alphas.insert(), [
|
||||
{"id": f"ppac{i:04}", "status": "UNSUBMITTED", "hidden": False, "settings": {}, "os_metrics": {},
|
||||
"is_metrics": {"checks": patterns[i % len(patterns)][0]}, "checks": patterns[i % len(patterns)][0],
|
||||
"check_type": patterns[i % len(patterns)][1], "synced_at": datetime(2026, 9, 13, tzinfo=timezone.utc),
|
||||
"raw": {"is": {"checks": patterns[i % len(patterns)][0]}}}
|
||||
for i in range(503)
|
||||
])
|
||||
original = db.execute(sa.select(alphas).order_by(alphas.c.id)).mappings().all()
|
||||
for target, position in [("0022", 2), ("0021", 1), ("0022", 2)]:
|
||||
(command.upgrade if target == "0022" else command.downgrade)(config, target)
|
||||
with engine.connect() as db:
|
||||
rows = db.execute(sa.select(alphas).order_by(alphas.c.id)).mappings().all()
|
||||
assert len(rows) == 503
|
||||
for i, row in enumerate(rows):
|
||||
assert dict(row) == {**original[i], "check_type": patterns[i % len(patterns)][position]}
|
||||
command.check(config)
|
||||
engine.dispose()
|
||||
Reference in New Issue
Block a user