|
|
@@ -12,6 +12,7 @@ ROOT = Path(__file__).resolve().parents[1]
|
|
|
DATA_DIR = ROOT / "tech_documents/数据接口与来源"
|
|
|
RULE_PACK_PATH = ROOT / "product_documents/规则包/douyin_rule_packs.v1.json"
|
|
|
WALK_STRATEGY_PATH = ROOT / "product_documents/抖音游走策略/douyin_walk_strategy.v1.json"
|
|
|
+WALK_EDGE_CATALOG_PATH = ROOT / "tech_documents/数据接口与来源/walk_edge_catalog.json"
|
|
|
LEGACY_FIELD_BLOCKLIST = {
|
|
|
"fit_senior_50plus",
|
|
|
"fit_confidence",
|
|
|
@@ -27,7 +28,7 @@ ENDPOINT_STATUSES = {
|
|
|
"missing",
|
|
|
}
|
|
|
M2_PLATFORM_PROFILES = {"douyin", "kuaishou", "shipinhao"}
|
|
|
-PROFILE_EDGE_STATUSES = {"supported", "blocked"}
|
|
|
+PROFILE_EDGE_STATUSES = {"supported", "blocked", "no_interface", "planned"}
|
|
|
PROFILE_ENDPOINT_STATUSES = {"verified", "verified_unstable", "blocked", "source_only", "missing"}
|
|
|
OBSERVABLE_FIELDS = {
|
|
|
"statistics.digg_count",
|
|
|
@@ -39,6 +40,12 @@ OBSERVABLE_FIELDS = {
|
|
|
MISSING_OBSERVABLE_TYPES = {"natural_platform_missing", "runtime_missing"}
|
|
|
M4_WALK_GATE_EDGES = {"hashtag_to_query", "author_to_works"}
|
|
|
M4_WALK_GATE_RAW_FIELDS = {"decision_id", "allow_walk", "allow_walk_reason", "walk_gate_snapshot"}
|
|
|
+V4_BASE_ACTIVE_DIMENSIONS = {"query_relevance", "platform_performance"}
|
|
|
+V4_REQUIRED_SCORE_WEIGHT_PROFILES = {
|
|
|
+ "default_two_dimension",
|
|
|
+ "douyin_fifty_plus_ok",
|
|
|
+ "douyin_fifty_plus_not_attempted",
|
|
|
+}
|
|
|
|
|
|
|
|
|
def main() -> int:
|
|
|
@@ -53,11 +60,41 @@ def validate_v4_config_contract(root: Path = ROOT) -> list[dict[str, str]]:
|
|
|
findings: list[dict[str, str]] = []
|
|
|
|
|
|
walk_graph = _load_json(data_dir / "walk_graph.json", findings)
|
|
|
+ graph_edges: set[str] = set()
|
|
|
if walk_graph:
|
|
|
_check_no_legacy_fields(findings, walk_graph, "walk_graph.json")
|
|
|
_check_value(findings, "walk_graph_schema", walk_graph.get("schema_version"), "walk_graph.v2")
|
|
|
_check_count(findings, "walk_graph_nodes", "walk_graph.nodes", walk_graph.get("nodes"), 8)
|
|
|
_check_count(findings, "walk_graph_edges", "walk_graph.edges", walk_graph.get("edges"), 9)
|
|
|
+ graph_edges = {
|
|
|
+ edge.get("edge_id")
|
|
|
+ for edge in walk_graph.get("edges", [])
|
|
|
+ if isinstance(edge, dict) and edge.get("edge_id")
|
|
|
+ }
|
|
|
+
|
|
|
+ walk_edge_catalog = _load_json(data_dir / "walk_edge_catalog.json", findings)
|
|
|
+ catalog_edges: set[str] = set()
|
|
|
+ if walk_edge_catalog:
|
|
|
+ _check_no_legacy_fields(findings, walk_edge_catalog, "walk_edge_catalog.json")
|
|
|
+ _check_value(
|
|
|
+ findings,
|
|
|
+ "walk_edge_catalog_schema",
|
|
|
+ walk_edge_catalog.get("schema_version"),
|
|
|
+ "walk_edge_catalog.v1",
|
|
|
+ )
|
|
|
+ rows = walk_edge_catalog.get("walk_edge_catalog")
|
|
|
+ _check_count(findings, "walk_edge_catalog_edges", "walk_edge_catalog", rows, 9)
|
|
|
+ catalog_edges = {
|
|
|
+ row.get("edge_id")
|
|
|
+ for row in rows or []
|
|
|
+ if isinstance(row, dict) and row.get("edge_id")
|
|
|
+ }
|
|
|
+ if walk_graph and graph_edges != catalog_edges:
|
|
|
+ _fail(
|
|
|
+ findings,
|
|
|
+ "walk_graph_catalog_mismatch",
|
|
|
+ f"walk_graph edges and walk_edge_catalog differ: graph_only={sorted(graph_edges - catalog_edges)}, catalog_only={sorted(catalog_edges - graph_edges)}",
|
|
|
+ )
|
|
|
|
|
|
walk_policy = _load_json(data_dir / "walk_policy.json", findings)
|
|
|
if walk_policy:
|
|
|
@@ -90,7 +127,7 @@ def validate_v4_config_contract(root: Path = ROOT) -> list[dict[str, str]]:
|
|
|
if not isinstance(endpoints, list):
|
|
|
_fail(findings, "crawler_endpoints_invalid", "endpoints must be a list")
|
|
|
else:
|
|
|
- _check_count(findings, "crawler_endpoints_count", "endpoints", endpoints, 26)
|
|
|
+ _check_count(findings, "crawler_endpoints_count", "endpoints", endpoints, 27)
|
|
|
for endpoint in endpoints:
|
|
|
if not isinstance(endpoint, dict):
|
|
|
_fail(findings, "crawler_endpoint_invalid", "endpoint row must be object")
|
|
|
@@ -153,6 +190,7 @@ def validate_v4_config_contract(root: Path = ROOT) -> list[dict[str, str]]:
|
|
|
for key in ["platform", "status", "runtime", "endpoints", "edges"]:
|
|
|
if key not in profile:
|
|
|
_fail(findings, "platform_profile_missing_key", f"{profile_path.name} missing {key}")
|
|
|
+ _check_platform_profile_edges(findings, profile, profile_path.name, graph_edges or catalog_edges)
|
|
|
if profile.get("platform") in M2_PLATFORM_PROFILES:
|
|
|
_check_m2_platform_profile(findings, profile, profile_path.name)
|
|
|
|
|
|
@@ -163,7 +201,7 @@ def validate_v4_config_contract(root: Path = ROOT) -> list[dict[str, str]]:
|
|
|
walk_strategy = _load_json(WALK_STRATEGY_PATH, findings)
|
|
|
if walk_strategy:
|
|
|
_check_no_legacy_fields(findings, walk_strategy, "douyin_walk_strategy.v1.json")
|
|
|
- _check_v4_walk_strategy_contract(findings, walk_strategy)
|
|
|
+ _check_v4_walk_strategy_contract(findings, walk_strategy, catalog_edges or graph_edges)
|
|
|
|
|
|
return findings
|
|
|
|
|
|
@@ -244,27 +282,37 @@ def _check_count(
|
|
|
_fail(findings, check_id, f"{label} expected {expected}, got {actual}")
|
|
|
|
|
|
|
|
|
-def _check_m2_platform_profile(
|
|
|
+def _check_platform_profile_edges(
|
|
|
findings: list[dict[str, str]],
|
|
|
profile: dict[str, Any],
|
|
|
label: str,
|
|
|
+ edge_ids: set[str],
|
|
|
) -> None:
|
|
|
- platform = str(profile.get("platform") or "")
|
|
|
edges = profile.get("edges")
|
|
|
if not isinstance(edges, dict):
|
|
|
_fail(findings, "platform_profile_edges_invalid", f"{label} edges must be an object")
|
|
|
- else:
|
|
|
- for edge_id, edge in edges.items():
|
|
|
- if not isinstance(edge, dict):
|
|
|
- _fail(findings, "platform_profile_edge_invalid", f"{label}.{edge_id} must be object")
|
|
|
- continue
|
|
|
- status = edge.get("status")
|
|
|
- if status not in PROFILE_EDGE_STATUSES:
|
|
|
- _fail(
|
|
|
- findings,
|
|
|
- "platform_profile_edge_status_unknown",
|
|
|
- f"{label}.{edge_id} status must be supported/blocked, got {status}",
|
|
|
- )
|
|
|
+ return
|
|
|
+ for edge_id, edge in edges.items():
|
|
|
+ if edge_ids and edge_id not in edge_ids:
|
|
|
+ _fail(findings, "platform_profile_edge_unknown", f"{label}.{edge_id} unknown edge")
|
|
|
+ if not isinstance(edge, dict):
|
|
|
+ _fail(findings, "platform_profile_edge_invalid", f"{label}.{edge_id} must be object")
|
|
|
+ continue
|
|
|
+ status = edge.get("status")
|
|
|
+ if status not in PROFILE_EDGE_STATUSES:
|
|
|
+ _fail(
|
|
|
+ findings,
|
|
|
+ "platform_profile_edge_status_unknown",
|
|
|
+ f"{label}.{edge_id} status must be supported/blocked/no_interface/planned, got {status}",
|
|
|
+ )
|
|
|
+
|
|
|
+
|
|
|
+def _check_m2_platform_profile(
|
|
|
+ findings: list[dict[str, str]],
|
|
|
+ profile: dict[str, Any],
|
|
|
+ label: str,
|
|
|
+) -> None:
|
|
|
+ platform = str(profile.get("platform") or "")
|
|
|
|
|
|
endpoints = profile.get("endpoints")
|
|
|
if not isinstance(endpoints, dict):
|
|
|
@@ -374,8 +422,8 @@ def _check_m2_platform_specifics(
|
|
|
endpoints = profile.get("endpoints") if isinstance(profile.get("endpoints"), dict) else {}
|
|
|
|
|
|
if platform == "kuaishou":
|
|
|
- _check_nested_status(findings, label, edges, "author_to_works", "blocked")
|
|
|
- _check_nested_status(findings, label, edges, "author_work_to_content", "blocked")
|
|
|
+ _check_nested_status(findings, label, edges, "author_to_works", "supported")
|
|
|
+ _check_nested_status(findings, label, edges, "author_work_to_content", "supported")
|
|
|
if platform == "shipinhao":
|
|
|
_check_nested_status(findings, label, endpoints, "account_info", "blocked")
|
|
|
_check_nested_status(findings, label, edges, "author_to_works", "blocked")
|
|
|
@@ -406,7 +454,15 @@ def _check_v4_walk_gate_config(
|
|
|
def _check_v4_walk_strategy_contract(
|
|
|
findings: list[dict[str, str]],
|
|
|
strategy: dict[str, Any],
|
|
|
+ edge_ids: set[str],
|
|
|
) -> None:
|
|
|
+ for binding in strategy.get("walk_rule_pack_binding") or []:
|
|
|
+ if isinstance(binding, dict) and edge_ids and binding.get("edge_id") not in edge_ids:
|
|
|
+ _fail(
|
|
|
+ findings,
|
|
|
+ "v4_walk_strategy_edge_unknown",
|
|
|
+ f"walk_rule_pack_binding unknown edge_id: {binding.get('edge_id')}",
|
|
|
+ )
|
|
|
rows = strategy.get("v4_walk_gate")
|
|
|
if not isinstance(rows, list) or not rows:
|
|
|
_fail(findings, "v4_walk_strategy_gate_missing", "douyin_walk_strategy.v1.json missing v4_walk_gate")
|
|
|
@@ -417,6 +473,9 @@ def _check_v4_walk_strategy_contract(
|
|
|
_fail(findings, "v4_walk_strategy_gate_missing", "allow_walk_required gate missing")
|
|
|
return
|
|
|
_check_v4_walk_gate_config(findings, gate, "douyin_walk_strategy.v4_walk_gate.allow_walk_required")
|
|
|
+ for edge_id in gate.get("applies_to_edges") or []:
|
|
|
+ if edge_ids and edge_id not in edge_ids:
|
|
|
+ _fail(findings, "v4_walk_strategy_gate_edge_unknown", f"v4_walk_gate unknown edge_id: {edge_id}")
|
|
|
|
|
|
|
|
|
def _check_nested_status(
|
|
|
@@ -465,14 +524,9 @@ def _check_v4_rule_pack_contract(
|
|
|
"v4_scorecard_schema_invalid",
|
|
|
f"{pack.get('rule_pack_id')} scorecard.schema_version must be v4_scorecard.v1",
|
|
|
)
|
|
|
- dimensions = [row for row in scorecard.get("dimensions", []) if row.get("runtime_status") == "active"]
|
|
|
- keys = [row.get("key") for row in dimensions]
|
|
|
- if keys != ["query_relevance", "platform_performance"]:
|
|
|
- _fail(
|
|
|
- findings,
|
|
|
- "v4_scorecard_dimensions_invalid",
|
|
|
- f"{pack.get('rule_pack_id')} active dimensions must be query_relevance/platform_performance",
|
|
|
- )
|
|
|
+ _check_v4_scorecard_dimensions(findings, pack.get("rule_pack_id"), scorecard)
|
|
|
+ _check_v4_score_weight_profiles(findings, pack.get("rule_pack_id"), scorecard)
|
|
|
+ _check_v4_scorecard_rule_refs(findings, pack.get("rule_pack_id"), scorecard)
|
|
|
required_fields = set((pack.get("input_contract") or {}).get("required_fields") or [])
|
|
|
for field in [
|
|
|
"pattern_match_result.query_relevance_score",
|
|
|
@@ -487,6 +541,199 @@ def _check_v4_rule_pack_contract(
|
|
|
)
|
|
|
|
|
|
|
|
|
+def _check_v4_scorecard_dimensions(
|
|
|
+ findings: list[dict[str, str]],
|
|
|
+ rule_pack_id: Any,
|
|
|
+ scorecard: dict[str, Any],
|
|
|
+) -> None:
|
|
|
+ dimensions = scorecard.get("dimensions")
|
|
|
+ if not isinstance(dimensions, list):
|
|
|
+ _fail(findings, "v4_scorecard_dimensions_invalid", f"{rule_pack_id} dimensions must be a list")
|
|
|
+ return
|
|
|
+ keys: list[Any] = []
|
|
|
+ for row in dimensions:
|
|
|
+ if not isinstance(row, dict):
|
|
|
+ _fail(findings, "v4_scorecard_dimension_invalid", f"{rule_pack_id} dimension row must be object")
|
|
|
+ continue
|
|
|
+ key = row.get("key")
|
|
|
+ if not key:
|
|
|
+ _fail(findings, "v4_scorecard_dimension_key_invalid", f"{rule_pack_id} dimension key must be non-empty")
|
|
|
+ keys.append(key)
|
|
|
+ if row.get("runtime_status") != "active":
|
|
|
+ continue
|
|
|
+ active = row.get("active")
|
|
|
+ if active is not None and active is not True:
|
|
|
+ _fail(
|
|
|
+ findings,
|
|
|
+ "v4_scorecard_dimension_active_invalid",
|
|
|
+ f"{rule_pack_id}/{key} active must match runtime_status=active",
|
|
|
+ )
|
|
|
+ platform_scope = row.get("platform_scope")
|
|
|
+ if platform_scope is not None and (
|
|
|
+ not isinstance(platform_scope, list)
|
|
|
+ or not platform_scope
|
|
|
+ or any(not isinstance(item, str) or not item for item in platform_scope)
|
|
|
+ ):
|
|
|
+ _fail(
|
|
|
+ findings,
|
|
|
+ "v4_scorecard_dimension_platform_scope_invalid",
|
|
|
+ f"{rule_pack_id}/{key} platform_scope must be a non-empty string list",
|
|
|
+ )
|
|
|
+ score_source_path = row.get("score_source_path")
|
|
|
+ if score_source_path is not None and (not isinstance(score_source_path, str) or not score_source_path):
|
|
|
+ _fail(
|
|
|
+ findings,
|
|
|
+ "v4_scorecard_dimension_score_source_path_invalid",
|
|
|
+ f"{rule_pack_id}/{key} score_source_path must be non-empty string",
|
|
|
+ )
|
|
|
+ if not _positive_number(row.get("max_score")):
|
|
|
+ _fail(findings, "v4_scorecard_dimension_score_invalid", f"{rule_pack_id}/{key} max_score must be positive")
|
|
|
+ if not _positive_number(row.get("weight_percent")):
|
|
|
+ _fail(findings, "v4_scorecard_dimension_weight_invalid", f"{rule_pack_id}/{key} weight_percent must be positive")
|
|
|
+ duplicates = sorted({key for key in keys if key and keys.count(key) > 1})
|
|
|
+ if duplicates:
|
|
|
+ _fail(findings, "v4_scorecard_dimension_duplicate", f"{rule_pack_id} duplicate dimensions: {duplicates}")
|
|
|
+ active_keys = {row.get("key") for row in dimensions if isinstance(row, dict) and row.get("runtime_status") == "active"}
|
|
|
+ missing = sorted(V4_BASE_ACTIVE_DIMENSIONS - active_keys)
|
|
|
+ if missing:
|
|
|
+ _fail(findings, "v4_scorecard_dimensions_invalid", f"{rule_pack_id} active dimensions missing base keys: {missing}")
|
|
|
+
|
|
|
+
|
|
|
+def _check_v4_score_weight_profiles(
|
|
|
+ findings: list[dict[str, str]],
|
|
|
+ rule_pack_id: Any,
|
|
|
+ scorecard: dict[str, Any],
|
|
|
+) -> None:
|
|
|
+ profiles = scorecard.get("score_weight_profiles")
|
|
|
+ if not isinstance(profiles, dict):
|
|
|
+ _fail(findings, "v4_score_weight_profiles_invalid", f"{rule_pack_id} score_weight_profiles must be an object")
|
|
|
+ return
|
|
|
+ dimension_keys = {
|
|
|
+ row.get("key")
|
|
|
+ for row in scorecard.get("dimensions", [])
|
|
|
+ if isinstance(row, dict) and row.get("key")
|
|
|
+ }
|
|
|
+ if isinstance(profiles.get("profiles"), list):
|
|
|
+ _check_v4_generic_score_weight_profiles(findings, rule_pack_id, profiles["profiles"], dimension_keys)
|
|
|
+ return
|
|
|
+ missing = sorted(V4_REQUIRED_SCORE_WEIGHT_PROFILES - set(profiles))
|
|
|
+ if missing:
|
|
|
+ _fail(findings, "v4_score_weight_profiles_invalid", f"{rule_pack_id} missing score weight profiles: {missing}")
|
|
|
+ for profile_name, weights in profiles.items():
|
|
|
+ if not isinstance(weights, dict) or not weights:
|
|
|
+ _fail(findings, "v4_score_weight_profile_invalid", f"{rule_pack_id}/{profile_name} must be a non-empty object")
|
|
|
+ continue
|
|
|
+ for key, weight in weights.items():
|
|
|
+ if key not in dimension_keys:
|
|
|
+ _fail(
|
|
|
+ findings,
|
|
|
+ "v4_score_weight_profile_dimension_unknown",
|
|
|
+ f"{rule_pack_id}/{profile_name} unknown dimension: {key}",
|
|
|
+ )
|
|
|
+ if not _positive_number(weight):
|
|
|
+ _fail(
|
|
|
+ findings,
|
|
|
+ "v4_score_weight_profile_weight_invalid",
|
|
|
+ f"{rule_pack_id}/{profile_name}/{key} weight must be positive",
|
|
|
+ )
|
|
|
+
|
|
|
+
|
|
|
+def _check_v4_generic_score_weight_profiles(
|
|
|
+ findings: list[dict[str, str]],
|
|
|
+ rule_pack_id: Any,
|
|
|
+ profiles: list[Any],
|
|
|
+ dimension_keys: set[Any],
|
|
|
+) -> None:
|
|
|
+ aliases: set[str] = set()
|
|
|
+ priority_groups: dict[tuple[Any, Any, Any, int], list[str]] = {}
|
|
|
+ for profile in profiles:
|
|
|
+ if not isinstance(profile, dict):
|
|
|
+ _fail(findings, "v4_score_weight_profile_invalid", f"{rule_pack_id} profile row must be object")
|
|
|
+ continue
|
|
|
+ profile_id = profile.get("profile_id")
|
|
|
+ if not profile_id:
|
|
|
+ _fail(findings, "v4_score_weight_profile_invalid", f"{rule_pack_id} profile_id must be non-empty")
|
|
|
+ continue
|
|
|
+ aliases.update(profile.get("historical_alias_ids") or [])
|
|
|
+ if profile.get("approval_status") != "approved":
|
|
|
+ _fail(findings, "v4_score_weight_profile_approval_invalid", f"{rule_pack_id}/{profile_id} must be approved")
|
|
|
+ if profile.get("runtime_enabled") is not True:
|
|
|
+ _fail(findings, "v4_score_weight_profile_runtime_invalid", f"{rule_pack_id}/{profile_id} runtime_enabled must be true")
|
|
|
+ weights = profile.get("weights")
|
|
|
+ if not isinstance(weights, dict) or not weights:
|
|
|
+ _fail(findings, "v4_score_weight_profile_invalid", f"{rule_pack_id}/{profile_id} weights must be non-empty object")
|
|
|
+ continue
|
|
|
+ total = 0.0
|
|
|
+ for key, weight in weights.items():
|
|
|
+ if key not in dimension_keys:
|
|
|
+ _fail(
|
|
|
+ findings,
|
|
|
+ "v4_score_weight_profile_dimension_unknown",
|
|
|
+ f"{rule_pack_id}/{profile_id} unknown dimension: {key}",
|
|
|
+ )
|
|
|
+ if not _positive_number(weight):
|
|
|
+ _fail(
|
|
|
+ findings,
|
|
|
+ "v4_score_weight_profile_weight_invalid",
|
|
|
+ f"{rule_pack_id}/{profile_id}/{key} weight must be positive",
|
|
|
+ )
|
|
|
+ else:
|
|
|
+ total += float(weight)
|
|
|
+ normalization = profile.get("normalization_policy") or "sum_100"
|
|
|
+ if normalization == "raw_sum":
|
|
|
+ if not profile.get("normalization_reason"):
|
|
|
+ _fail(
|
|
|
+ findings,
|
|
|
+ "v4_score_weight_profile_normalization_invalid",
|
|
|
+ f"{rule_pack_id}/{profile_id} raw_sum requires normalization_reason",
|
|
|
+ )
|
|
|
+ elif round(total, 6) != 100:
|
|
|
+ _fail(
|
|
|
+ findings,
|
|
|
+ "v4_score_weight_profile_total_invalid",
|
|
|
+ f"{rule_pack_id}/{profile_id} weights must sum to 100",
|
|
|
+ )
|
|
|
+ group = (
|
|
|
+ tuple(profile.get("platform_scope") or []),
|
|
|
+ profile.get("content_format") or "*",
|
|
|
+ profile.get("applies_when") or "always",
|
|
|
+ int(profile.get("priority") or 0),
|
|
|
+ )
|
|
|
+ priority_groups.setdefault(group, []).append(str(profile_id))
|
|
|
+ missing = sorted(V4_REQUIRED_SCORE_WEIGHT_PROFILES - aliases)
|
|
|
+ if missing:
|
|
|
+ _fail(findings, "v4_score_weight_profiles_invalid", f"{rule_pack_id} missing historical aliases: {missing}")
|
|
|
+ for group, ids in priority_groups.items():
|
|
|
+ if len(ids) > 1:
|
|
|
+ _fail(findings, "v4_score_weight_profile_priority_invalid", f"{rule_pack_id} priority conflict {group}: {ids}")
|
|
|
+
|
|
|
+
|
|
|
+def _check_v4_scorecard_rule_refs(
|
|
|
+ findings: list[dict[str, str]],
|
|
|
+ rule_pack_id: Any,
|
|
|
+ scorecard: dict[str, Any],
|
|
|
+) -> None:
|
|
|
+ dimension_keys = {
|
|
|
+ row.get("key")
|
|
|
+ for row in scorecard.get("dimensions", [])
|
|
|
+ if isinstance(row, dict) and row.get("key")
|
|
|
+ }
|
|
|
+ for rule in scorecard.get("scoring_rules", []):
|
|
|
+ if not isinstance(rule, dict):
|
|
|
+ _fail(findings, "v4_scoring_rule_invalid", f"{rule_pack_id} scoring rule must be object")
|
|
|
+ continue
|
|
|
+ if rule.get("dimension_key") not in dimension_keys:
|
|
|
+ _fail(
|
|
|
+ findings,
|
|
|
+ "v4_scoring_rule_dimension_unknown",
|
|
|
+ f"{rule_pack_id}/{rule.get('scoring_rule_id')} unknown dimension_key: {rule.get('dimension_key')}",
|
|
|
+ )
|
|
|
+
|
|
|
+
|
|
|
+def _positive_number(value: Any) -> bool:
|
|
|
+ return isinstance(value, (int, float)) and not isinstance(value, bool) and value > 0
|
|
|
+
|
|
|
+
|
|
|
def _fail(findings: list[dict[str, str]], check_id: str, message: str) -> None:
|
|
|
findings.append({"level": "fail", "check_id": check_id, "message": message})
|
|
|
|