cert-study-app / cert_study_app /services /quality_gate_service.py
github-actions
Sync from GitHub d2682fe6d3fcffe93aa302c286320962009f6436
9381502
Raw
History Blame Contribute Delete
5.28 kB
from __future__ import annotations
import json
from pathlib import Path
from typing import Any
from cert_study_app.services.parse_quality_service import load_parsed_questions
BLOCKING_ISSUES = {
"missing_stem",
"number_duplicate",
"embedded_next_question",
"page_regression",
"duplicate_raw_text",
}
REPARSE_ISSUES = {"number_gap", "embedded_next_question", "chunk_too_long", "duplicate_raw_text"}
REVIEW_ISSUES = {
"short_stem",
"missing_answer",
"missing_options",
"option_label_gap",
"answer_not_in_options",
"answer_leaked_to_stem",
"chunk_too_short",
"image_missing",
}
def default_gate_report_path(output_json: str | Path) -> str:
path = Path(output_json)
return path.with_name(f"{path.stem}.gate.json").as_posix()
def default_quarantine_path(output_json: str | Path) -> str:
path = Path(output_json)
return path.with_name(f"{path.stem}.quarantine.json").as_posix()
def apply_quality_gate(
parsed_json_path: str | Path,
quality_report: dict[str, Any],
*,
gate_report_path: str | Path | None = None,
quarantine_path: str | Path | None = None,
pass_score: int = 85,
warn_score: int = 70,
) -> dict[str, Any]:
questions = load_parsed_questions(parsed_json_path)
issues_by_index = {
int(row["index"]): row.get("issues") or []
for row in quality_report.get("question_issues") or []
if isinstance(row, dict) and str(row.get("index", "")).isdigit()
}
issue_counts = quality_report.get("issue_counts") or {}
blocking_count = sum(int(issue_counts.get(code, 0) or 0) for code in BLOCKING_ISSUES)
reparse_count = sum(int(issue_counts.get(code, 0) or 0) for code in REPARSE_ISSUES)
score = int(quality_report.get("score") or 0)
question_count = int(quality_report.get("question_count") or len(questions))
if question_count <= 0:
action = "hold"
status = "empty"
reason = "ํŒŒ์‹ฑ๋œ ๋ฌธํ•ญ์ด ์—†์Šต๋‹ˆ๋‹ค."
elif blocking_count:
action = "hold"
status = "reparse_needed"
reason = "์ฒญํ‚น/๋ฒˆํ˜ธ/์ค‘๋ณต ๊ด€๋ จ ์ฐจ๋‹จ ์ด์Šˆ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค."
elif score >= pass_score:
action = "proceed"
status = "pass"
reason = "์ž๋™ ์ ์žฌ ๊ฐ€๋Šฅํ•œ ํ’ˆ์งˆ์ž…๋‹ˆ๋‹ค."
elif score >= warn_score:
action = "proceed_with_review"
status = "warning"
reason = "์ ์žฌ๋Š” ๊ฐ€๋Šฅํ•˜์ง€๋งŒ ์ผ๋ถ€ ๋ฌธํ•ญ์€ ์ž๋™ ๊ฒ€์ˆ˜ ๋Œ€์ƒ์œผ๋กœ ํ‘œ์‹œํ•ฉ๋‹ˆ๋‹ค."
else:
action = "hold"
status = "low_quality"
reason = "ํ’ˆ์งˆ ์ ์ˆ˜๊ฐ€ ๋‚ฎ์•„ DB ์ ์žฌ๋ฅผ ๋ณด๋ฅ˜ํ•ฉ๋‹ˆ๋‹ค."
annotated = []
status_counts: dict[str, int] = {}
for index, item in enumerate(questions):
row = dict(item)
issues = issues_by_index.get(index, [])
codes = [str(issue.get("code")) for issue in issues if isinstance(issue, dict)]
if action == "hold":
parse_status = "needs_reparse" if set(codes) & REPARSE_ISSUES else "needs_review"
quality_status = "blocked"
elif set(codes) & REPARSE_ISSUES:
parse_status = "needs_reparse"
quality_status = "reparse_candidate"
elif set(codes) & REVIEW_ISSUES:
parse_status = "needs_review"
quality_status = "review"
else:
parse_status = row.get("parse_status") or "draft"
quality_status = "pass"
row["parse_status"] = parse_status
row["quality_status"] = quality_status
row["quality_issues"] = issues
row["quality_score"] = score
annotated.append(row)
status_counts[quality_status] = status_counts.get(quality_status, 0) + 1
Path(parsed_json_path).write_text(json.dumps(annotated, ensure_ascii=False, indent=2), encoding="utf-8")
quarantine = None
if action == "hold":
quarantine = str(quarantine_path or default_quarantine_path(parsed_json_path))
Path(quarantine).parent.mkdir(parents=True, exist_ok=True)
Path(quarantine).write_text(json.dumps(annotated, ensure_ascii=False, indent=2), encoding="utf-8")
gate = {
"schema_version": 1,
"source_json": Path(parsed_json_path).as_posix(),
"action": action,
"status": status,
"reason": reason,
"score": score,
"pass_score": pass_score,
"warn_score": warn_score,
"question_count": question_count,
"blocking_count": blocking_count,
"reparse_count": reparse_count,
"status_counts": status_counts,
"quarantine_json": quarantine,
"llm_review_policy": {
"enabled_by_default": False,
"recommended_only_for": sorted(REPARSE_ISSUES | {"answer_not_in_options", "missing_answer"}),
},
}
if gate_report_path:
Path(gate_report_path).parent.mkdir(parents=True, exist_ok=True)
Path(gate_report_path).write_text(json.dumps(gate, ensure_ascii=False, indent=2), encoding="utf-8")
return gate
def summarize_quality_gate(gate: dict[str, Any]) -> str:
return (
f"ํ’ˆ์งˆ ๊ฒŒ์ดํŠธ {gate.get('status')} ยท {gate.get('action')} ยท "
f"์ฐจ๋‹จ {gate.get('blocking_count', 0)}๊ฐœ ยท ์žฌํŒŒ์‹ฑ ํ›„๋ณด {gate.get('reparse_count', 0)}๊ฐœ"
)