forked from baskbuddy/zhangshun
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
224 lines
9.2 KiB
224 lines
9.2 KiB
# -*- coding: utf-8 -*-
|
|
"""
|
|
V36 四维能力测评 题库权威校验脚本
|
|
|
|
两个目标:
|
|
1) 独立校验权威题库 ability-question-bank-v0.1.json 满足全部 14 项硬性约束。
|
|
2) 将 V35__ability_assessment.sql 中写入的 48 道题与权威 JSON 逐题对账,
|
|
确保题干/选项/答案键/分值/解析/难度/版本/position 语义完全一致。
|
|
|
|
任一项校验失败 => 打印 FAIL 并退出码 1(不自动修复题库内容)。
|
|
全部通过 => 打印 PASS(针对每道题)并退出码 0。
|
|
"""
|
|
import json
|
|
import re
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
BANK_JSON = ROOT / "docs" / "ability-question-bank-v0.1.json"
|
|
V35_SQL = ROOT / "mydreamjob-server" / "src" / "main" / "resources" / "db" / "migration" / "V35__ability_assessment.sql"
|
|
|
|
DIMENSIONS = ["LOGIC", "COMMUNICATION", "EXECUTION", "LEARNING"]
|
|
COMPETENCY_TAGS = {
|
|
"LOGIC": ["INFORMATION_ANALYSIS", "CAUSAL_REASONING", "PROBLEM_SOLVING"],
|
|
"COMMUNICATION": ["LISTENING_UNDERSTANDING", "STRUCTURED_EXPRESSION", "COLLABORATION_CONFLICT"],
|
|
"EXECUTION": ["PLANNING_PRIORITIZATION", "RISK_FOLLOWUP", "DELIVERY_REVIEW"],
|
|
"LEARNING": ["INFORMATION_ACQUISITION", "FEEDBACK_REFLECTION", "TRANSFER_ADAPTATION"],
|
|
}
|
|
SCORES = {0, 1, 2, 4}
|
|
OPTION_IDS = ["A", "B", "C", "D"]
|
|
|
|
|
|
def fail(msg):
|
|
print(f"FAIL: {msg}")
|
|
sys.exit(1)
|
|
|
|
|
|
def check(cond, msg):
|
|
if not cond:
|
|
fail(msg)
|
|
print(f" PASS: {msg}")
|
|
|
|
|
|
def main():
|
|
print("=== 第 1 步:校验权威 JSON 文件 ===")
|
|
bank = json.loads(BANK_JSON.read_text(encoding='utf-8'))
|
|
if bank.get("bankCode") != "WORKPLACE_ABILITY":
|
|
fail("bankCode != WORKPLACE_ABILITY")
|
|
if bank.get("bankVersion") != "ability-v0.1":
|
|
fail("bankVersion != ability-v0.1")
|
|
|
|
qs = bank.get("questions", [])
|
|
check(len(qs) == 48, f"总数 = 48(实际 {len(qs)})")
|
|
|
|
# 1) 四维各 12
|
|
dim_count = {d: 0 for d in DIMENSIONS}
|
|
pos_set = set()
|
|
tag_count = {}
|
|
key_count = {}
|
|
dif_count = {}
|
|
for q in qs:
|
|
dim_count[q["dimension"]] = dim_count.get(q["dimension"], 0) + 1
|
|
tag_count[q["competencyTag"]] = tag_count.get(q["competencyTag"], 0) + 1
|
|
key_count[q["answerKey"]] = key_count.get(q["answerKey"], 0) + 1
|
|
dif_count[q["difficulty"]] = dif_count.get(q["difficulty"], 0) + 1
|
|
pos_set.add(q["position"])
|
|
for d in DIMENSIONS:
|
|
check(dim_count.get(d) == 12, f"维度 {d} = 12(实际 {dim_count.get(d)})")
|
|
for tag in sum(COMPETENCY_TAGS.values(), []):
|
|
check(tag_count.get(tag) == 4, f"competencyTag {tag} = 4(实际 {tag_count.get(tag)})")
|
|
for k in OPTION_IDS:
|
|
check(key_count.get(k) == 12, f"answerKey {k} = 12(实际 {key_count.get(k)})")
|
|
for d in ["EASY", "MEDIUM", "HARD"]:
|
|
check(dif_count.get(d) == 16, f"difficulty {d} = 16(实际 {dif_count.get(d)})")
|
|
check(len(pos_set) == 48 and pos_set == set(range(1, 49)),
|
|
"position 1-48 唯一且连续")
|
|
|
|
# 2) 逐题约束
|
|
for q in qs:
|
|
code = q["questionCode"]
|
|
opts = q["options"]
|
|
# 每题四选一
|
|
check(len(opts) == 4, f"{code} 含 4 个选项")
|
|
oids = [o["optionId"] for o in opts]
|
|
check(oids == OPTION_IDS, f"{code} optionId 集合 = A/B/C/D")
|
|
for o in opts:
|
|
if not o.get("text") or not str(o["text"]).strip():
|
|
fail(f"{code} 存在空选项文本")
|
|
# 分值集合严格等于 {0,1,2,4}
|
|
rule = q["scoringRule"]
|
|
check(sorted(rule.keys()) == OPTION_IDS, f"{code} scoringRule 键 = A/B/C/D")
|
|
check(set(rule.values()) == SCORES, f"{code} 分值集合 = {{0,1,2,4}}(实际 {sorted(set(rule.values()))})")
|
|
check(len(set(rule.values())) == 4, f"{code} 分值互不相同(每题各档唯一)")
|
|
# answerKey 对应 4 分
|
|
check(rule.get(q["answerKey"]) == 4, f"{code} answerKey={q['answerKey']} 对应 4 分")
|
|
# explanation 非空
|
|
check(bool(q.get("explanation")) and str(q["explanation"]).strip(), f"{code} explanation 非空")
|
|
# 版本/来源/启用
|
|
check(q["questionVersion"] == "ability-v0.1", f"{code} questionVersion=ability-v0.1")
|
|
check(q["source"] == "SELF_AUTHORED", f"{code} source=SELF_AUTHORED")
|
|
check(q["active"] is True, f"{code} active=true")
|
|
check(q["dimension"] in DIMENSIONS, f"{code} dimension 合法")
|
|
check(q["competencyTag"] in COMPETENCY_TAGS.get(q["dimension"], []), f"{code} competencyTag 归属该维度")
|
|
check(q["itemType"] in ("SITUATIONAL_JUDGMENT", "SEQUENCE_JUDGMENT"), f"{code} itemType 合法")
|
|
|
|
print("=== 第 2 步:V35 SQL 与权威 JSON 逐题对账 ===")
|
|
sql = V35_SQL.read_text(encoding='utf-8')
|
|
# 抓取 definition INSERT 中 WORKPLACE_ABILITY 的系数行
|
|
defn_pat = ("'WORKPLACE_ABILITY'[\\s\\S]*?'ACTIVE',[\\s\\S]*?"
|
|
"48,\\s*24,\\s*6,\\s*30,\\s*7,\\s*90,\\s*3,"
|
|
"[\\s\\S]*?'ability-v0.1'[\\s\\S]*?'ability-scoring-v0.1'\\s*,?\\s*\\)")
|
|
defn_m = re.search(defn_pat, sql)
|
|
if not defn_m:
|
|
fail("V35 中缺少 WORKPLACE_ABILITY 定义种子(48/24/6/30/7/90/3)")
|
|
print(" PASS: WORKPLACE_ABILITY 定义种子存在且参数一致")
|
|
|
|
# 抓取 ability_questions 的整段 VALUES(一直抓到文件结尾)
|
|
values_m = re.search(r"INSERT INTO ability_questions[\s\S]*?VALUES\s*(.*)$", sql, re.S)
|
|
if not values_m:
|
|
fail("V35 中缺少 ability_questions INSERT")
|
|
body = values_m.group(1)
|
|
# 每个 VALUES 行以 "('XXNN'" 起始,且独占一行;逐行拆分
|
|
rows = []
|
|
for line in body.splitlines():
|
|
m = re.match(r"\s*\(\s*'([A-Z]{2}\d{2})'(.+)\)\s*,?;?\s*$", line)
|
|
if m:
|
|
rows.append((m.group(1), m.group(2)))
|
|
|
|
def clean(v):
|
|
v = v.strip()
|
|
if v.startswith("'") and v.endswith("'"):
|
|
v = v[1:-1]
|
|
return v.replace("''", "'")
|
|
|
|
def parse_json(v):
|
|
return json.loads(clean(v))
|
|
|
|
if len(rows) != 48:
|
|
fail(f"V35 中题目行数 = {len(rows)}(应为 48)")
|
|
# 拆分字段:code 单独;其余依次为
|
|
# def_subquery, dimension, competency_tag, item_type, stem,
|
|
# options_json, answer_key_json, scoring_rule_json, explanation,
|
|
# difficulty, discrimination, question_version, source, active, position
|
|
by_pos = {}
|
|
for code, rest in rows:
|
|
parts = split_row(rest)
|
|
# rest 以 "(SELECT..." 起始,首字段为 definition_id 子查询,且行首逗号会产生一个空 token;
|
|
# 去除空 token 后即为 [def_subquery, dimension, ..., position]
|
|
if parts and parts[0] == "":
|
|
parts = parts[1:]
|
|
if len(parts) != 15:
|
|
fail(f"{code} 解析出 {len(parts)} 个字段(应为 15)")
|
|
by_pos[int(parts[14])] = (code, parts)
|
|
if set(by_pos.keys()) != set(range(1, 49)):
|
|
fail("V35 中 position 不覆盖 1-48")
|
|
|
|
for q in qs:
|
|
code = q["questionCode"]
|
|
pos = q["position"]
|
|
row = by_pos.get(pos)
|
|
if row is None:
|
|
fail(f"position {pos}({code})在 V35 中缺失")
|
|
sql_code, parts = row
|
|
if sql_code != code:
|
|
fail(f"position {pos}:V35 questionCode={sql_code},权威 JSON={code}")
|
|
|
|
# definition_id(parts[0])为子查询,跳过比较
|
|
sql_dim, sql_tag, sql_item, sql_stem = parts[1], parts[2], parts[3], parts[4]
|
|
sql_opts, sql_key, sql_rule, sql_expl = parts[5], parts[6], parts[7], parts[8]
|
|
sql_dif = parts[9]
|
|
sql_ver, sql_src, sql_active, sql_pos = parts[11], parts[12], parts[13], parts[14]
|
|
|
|
def eq_a(actual, expect, label):
|
|
if actual != expect:
|
|
fail(f"{code} {label} 不一致")
|
|
|
|
eq_a(clean(sql_dim), q["dimension"], "dimension")
|
|
eq_a(clean(sql_tag), q["competencyTag"], "competencyTag")
|
|
eq_a(clean(sql_item), q["itemType"], "itemType")
|
|
eq_a(clean(sql_stem), q["stem"], "stem")
|
|
eq_a(parse_json(sql_opts), q["options"], "options_json")
|
|
eq_a(parse_json(sql_key), dict(answerKey=q["answerKey"]), "answer_key_json")
|
|
eq_a(parse_json(sql_rule), q["scoringRule"], "scoring_rule_json")
|
|
eq_a(clean(sql_expl), q["explanation"], "explanation")
|
|
eq_a(clean(sql_dif), q["difficulty"], "difficulty")
|
|
eq_a(clean(sql_ver), q["questionVersion"], "question_version")
|
|
eq_a(clean(sql_src), q["source"], "source")
|
|
eq_a(clean(sql_active), "1", "active")
|
|
eq_a(clean(sql_pos), str(q["position"]), "position")
|
|
print(f" PASS: {code} 与权威 JSON 完全一致")
|
|
|
|
print("ALL PASS: 题库 14 项硬性约束 + V35 SQL 逐题对账全部通过")
|
|
sys.exit(0)
|
|
|
|
|
|
def split_row(rest):
|
|
"""把一行剩余部分按顶层逗号切分(忽略引号内的逗号)。"""
|
|
out = []
|
|
buf = []
|
|
in_sq = False
|
|
i = 0
|
|
body = rest
|
|
while i < len(body):
|
|
c = body[i]
|
|
if c == "'":
|
|
if in_sq and i + 1 < len(body) and body[i + 1] == "'":
|
|
buf.append("''")
|
|
i += 2
|
|
continue
|
|
in_sq = not in_sq
|
|
buf.append(c)
|
|
elif c == "," and not in_sq:
|
|
out.append("".join(buf).strip())
|
|
buf = []
|
|
else:
|
|
buf.append(c)
|
|
i += 1
|
|
if buf:
|
|
out.append("".join(buf).strip())
|
|
return out
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|