484 lines
24 KiB
Python
484 lines
24 KiB
Python
"""往规矩的投标文件稿里注入问题,写出马虎的投标文件稿和响应清单(evals/check/GUIDELINES.md §3、§6.2)。
|
||
|
||
cd server && ../.venv/bin/python ../evals/check/mutate.py <规矩的投标文件> <输出的投标文件> --split dev [--seed 1] [--density high|low]
|
||
cd server && ../.venv/bin/python ../evals/check/mutate.py <规矩的投标文件> --clean --split dev # 只给规矩的投标文件写响应清单
|
||
|
||
- 词库:开发集用 evals/check/vocab-dev.json;留出集用封存的 evals/out/check/_sealed/vocab-holdout.json;
|
||
- 注入是确定性的(同一个 seed 结果相同),每个问题记 defect、variant、granularity、worded;
|
||
- 问题只挂在标注单元上;挂上的单元先记 reviewed: false,复核(决策 E1)后改成 true,评分只算复核过的;
|
||
- 响应方式按单元在投标文件里的响应位置推出来:逐条响应表 → explicit,只列偏离的表 → implicit,附件 → evidence,格式文件 → presence。
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import argparse
|
||
import copy
|
||
import json
|
||
import pathlib
|
||
import random
|
||
import re
|
||
import sys
|
||
from decimal import Decimal
|
||
from typing import Any
|
||
|
||
EVALS = pathlib.Path(__file__).resolve().parents[1]
|
||
sys.path.insert(0, str(EVALS))
|
||
sys.path.insert(0, str(EVALS / "check"))
|
||
import gold # noqa: E402
|
||
from build import rmb_upper # noqa: E402
|
||
|
||
OUT = EVALS / "out" / "check"
|
||
VOCAB = {"dev": EVALS / "check" / "vocab-dev.json", "holdout": OUT / "_sealed" / "vocab-holdout.json"}
|
||
# 「暂不支持」只对功能类要求说得通;证书、清单、服务义务、文档交付、改造范围这类不是功能(复核两批共拒掉 12 条)
|
||
FUNCTIONAL = re.compile(r"支持|功能|具备|实现|可对|能够|可以|兼容|提供.{0,6}接口")
|
||
NON_FUNCTIONAL = re.compile(r"证书|认证|清单|服务|培训|文档|资料|方案|范围|记录|报告|期限|质保|交付|人员|工期|成果")
|
||
# 条件性要求:只对特定投标人适用(「如依法免税则须提供……」),规矩的投标人不适用,不能当作必须提供的材料
|
||
CONDITIONAL = re.compile(r"(如|若|对于)[^,。;]{0,12}(免税|不需|无需|依法|属于|是)")
|
||
DOC_REQ = re.compile(r"提供.{0,12}(方案|证明|报告|文件|承诺|截图|复印件|证书)")
|
||
UNIT_DOWN = {"TB": ("GB", 1024), "GB": ("MB", 1024), "Gbps": ("Mbps", 1000), "s": ("ms", 1000), "秒": ("毫秒", 1000)}
|
||
|
||
|
||
class Mutator:
|
||
def __init__(self, spec: dict[str, Any], vocab: dict[str, Any], rng: random.Random) -> None:
|
||
self.spec, self.vocab, self.rng = spec, vocab, rng
|
||
self.unit_defects: dict[str, dict[str, Any]] = {}
|
||
self.rule_defects: list[dict[str, Any]] = []
|
||
self.umbrella: set[str] = set()
|
||
self.vols = spec["volumes"]
|
||
|
||
@property
|
||
def blocks(self) -> list[dict[str, Any]]:
|
||
"""所有分册的块(只读视图);增删用 remove / insert_after。"""
|
||
return [b for v in self.vols for b in v["blocks"]]
|
||
|
||
def remove(self, b: dict[str, Any]) -> None:
|
||
for v in self.vols:
|
||
if b in v["blocks"]:
|
||
v["blocks"].remove(b)
|
||
|
||
def insert_after(self, anchor: dict[str, Any], new: dict[str, Any]) -> None:
|
||
for v in self.vols:
|
||
if anchor in v["blocks"]:
|
||
v["blocks"].insert(v["blocks"].index(anchor) + 1, new)
|
||
return
|
||
|
||
# ---------- 定位 ----------
|
||
def tables(self, *, explicit: bool | None = None, fixed: bool = False) -> list[dict[str, Any]]:
|
||
out = []
|
||
for b in self.blocks:
|
||
if b.get("type") != "table" or not b.get("form"):
|
||
continue
|
||
if fixed:
|
||
if b.get("fixed"):
|
||
out.append(b)
|
||
continue
|
||
if b.get("fixed") or "only_deviations" not in b:
|
||
continue
|
||
mode = table_mode(b)
|
||
if explicit is None or (explicit and mode == "explicit") or (explicit is False and mode == "implicit"):
|
||
out.append(b)
|
||
return out
|
||
|
||
def free(self, units: list[str]) -> bool:
|
||
return bool(units) and not any(u in self.unit_defects for u in units)
|
||
|
||
def mark(
|
||
self, units: list[str], defect: str, variant: str, granularity: str = "single", worded: bool = True
|
||
) -> None:
|
||
for u in units:
|
||
self.unit_defects[u] = {"defect": defect, "variant": variant, "granularity": granularity, "worded": worded}
|
||
|
||
def phrase(self, key: str, field: str) -> str:
|
||
return str(self.rng.choice(self.vocab[key][field]))
|
||
|
||
# ---------- 要求项问题 ----------
|
||
def d01_block(self) -> None:
|
||
"""连续漏掉 5~10 行(逐条响应表)。"""
|
||
for t in self.rng.sample(self.tables(explicit=True), k=len(self.tables(explicit=True))):
|
||
rows = t["rows"]
|
||
if len(rows) < 12:
|
||
continue
|
||
n = self.rng.randint(5, 10)
|
||
start = self.rng.randint(0, len(rows) - n)
|
||
chunk = rows[start : start + n]
|
||
if not all(self.free(r.get("responds") or []) for r in chunk if r.get("responds")):
|
||
continue
|
||
for r in chunk:
|
||
self.mark(r.get("responds") or [], "D01", "删连续一段", "block", worded=False)
|
||
del rows[start : start + n]
|
||
self.drop_everywhere([u for r in chunk for u in r.get("responds") or []])
|
||
return
|
||
|
||
def row_ops(self, n: int) -> None:
|
||
"""逐条响应表里的单点问题:漏行、数值不满足、写明不支持、负偏离未说明、只写满足、对象错位。"""
|
||
cands = [(t, r) for t in self.tables(explicit=True) for r in t["rows"] if self.free(r.get("responds") or [])]
|
||
self.rng.shuffle(cands)
|
||
ops = ["D01", "D03", "D03", "D05", "D06", "D08", "D09"]
|
||
done = 0
|
||
for t, r in cands:
|
||
if done >= n:
|
||
break
|
||
if r not in t["rows"] or not self.free(r.get("responds") or []):
|
||
continue
|
||
# 从轮到的类型开始,这一行做不了就换下一种(软件类项目几乎没有数值参数,D03 常常做不了)
|
||
for k in range(len(ops)):
|
||
if self._apply(ops[(done + k) % len(ops)], t, r, cands):
|
||
done += 1
|
||
break
|
||
|
||
def drop_everywhere(self, units: list[str]) -> None:
|
||
"""漏响应要在所有表里都漏:S04 的技术参数同时列在全量偏离表和 ★ 表里,只删一张表等于没漏。"""
|
||
for t in self.tables():
|
||
t["rows"] = [r for r in t["rows"] if not set(r.get("responds") or []) & set(units)]
|
||
|
||
@staticmethod
|
||
def col(t: dict[str, Any], field: str) -> int | None:
|
||
cols = t.get("columns") or ["no", "requirement", "answer", "deviation", "note"]
|
||
return cols.index(field) if field in cols else None
|
||
|
||
def _apply(self, op: str, t: dict[str, Any], r: dict[str, Any], cands: list[Any]) -> bool:
|
||
ans, dev, note, req = (self.col(t, f) for f in ("answer", "deviation", "note", "requirement"))
|
||
if ans is None:
|
||
return False
|
||
tp = r.get("tp") or {}
|
||
units = r.get("responds") or []
|
||
req_text = r["cells"][req] if req is not None else ""
|
||
if DOC_REQ.search(req_text): # 「提供……方案 / 证明 / 报告」是文件要在(presence),不注入参数类问题
|
||
return False
|
||
numeric = tp.get("op") in (">=", "<=", "=") and tp.get("value")
|
||
if op == "D05" and (numeric or not FUNCTIONAL.search(req_text) or NON_FUNCTIONAL.search(req_text)):
|
||
return False # 「暂不支持」只套在功能类要求上(数值参数、证书、清单、服务义务不会这么写)
|
||
if op == "D01":
|
||
t["rows"].remove(r)
|
||
self.drop_everywhere(units)
|
||
self.mark(units, "D01", "删一行", worded=False)
|
||
elif op == "D03":
|
||
return self._unmet(r, tp, ans, dev)
|
||
elif op == "D05":
|
||
param = tp.get("param") or (r["cells"][req][:12] if req is not None else "该功能")
|
||
r["cells"][ans] = self.phrase("D05", "phrases").format(param=param)
|
||
self.mark(units, "D05", r["cells"][ans])
|
||
elif op == "D06":
|
||
if dev is None:
|
||
return False
|
||
r["cells"][dev] = self.phrase("D06", "deviations")
|
||
if note is not None:
|
||
r["cells"][note] = ""
|
||
self.mark(units, "D06", r["cells"][dev])
|
||
elif op == "D08":
|
||
if not tp.get("value"):
|
||
return False
|
||
r["cells"][ans] = self.phrase("D08", "phrases")
|
||
self.mark(units, "D08", r["cells"][ans], worded=False)
|
||
elif op == "D09":
|
||
others = [
|
||
x
|
||
for _, x in cands
|
||
if x is not r
|
||
and (x.get("tp") or {}).get("param")
|
||
and x["cells"][x.get("anchor", ans)] != r["cells"][ans]
|
||
]
|
||
if not others:
|
||
return False
|
||
o = self.rng.choice(others)
|
||
r["cells"][ans] = o["cells"][o.get("anchor", ans)]
|
||
self.mark(units, "D09", "另一条参数的响应", worded=False)
|
||
return True
|
||
|
||
def _unmet(self, r: dict[str, Any], tp: dict[str, Any], ans: int, dev: int | None) -> bool:
|
||
"""把响应值改成不满足;一部分换成更小的单位(换算后不满足)。不带标志词。"""
|
||
op, unit = tp.get("op"), tp.get("unit") or ""
|
||
try:
|
||
v = Decimal(str(tp.get("value")))
|
||
except Exception: # noqa: BLE001
|
||
return False
|
||
if op not in (">=", "<=") or v == 0:
|
||
return False
|
||
bad = (v / 2) if op == ">=" else (v * Decimal("1.6"))
|
||
variant = "低于下限" if op == ">=" else "高于上限"
|
||
if unit in UNIT_DOWN and self.rng.random() < 0.4:
|
||
small, k = UNIT_DOWN[unit]
|
||
text, variant = f"{(bad * k).normalize():f}{small}", "换算后不满足"
|
||
else:
|
||
text = f"{bad.normalize():f}{unit}"
|
||
r["cells"][ans] = f"{tp.get('param') or ''}:{text}"
|
||
if dev is not None and self.rng.random() < 0.5:
|
||
r["cells"][dev] = "无偏离" # 一半在偏离栏仍写「无偏离」(另一半保持原样,也是无偏离或正偏离)
|
||
self.mark(r.get("responds") or [], "D03", variant, worded=False)
|
||
return True
|
||
|
||
def implicit_deviation(self, n: int) -> None:
|
||
"""只列偏离的表:挑默认接受的要求写成偏离(D06 负偏离未说明 / D05 写明不支持)。"""
|
||
for t in self.tables(explicit=False):
|
||
req_col = self.col(t, "requirement")
|
||
pool = [
|
||
r
|
||
for r in t.get("pool") or []
|
||
if self.free(r.get("responds") or [])
|
||
and not (req_col is not None and DOC_REQ.search(r["cells"][req_col]))
|
||
]
|
||
self.rng.shuffle(pool)
|
||
ans, dev, note, req = (self.col(t, f) for f in ("answer", "deviation", "note", "requirement"))
|
||
if ans is None or dev is None:
|
||
continue
|
||
for k, r in enumerate(pool[:n]):
|
||
row = copy.deepcopy(r)
|
||
if k % 2 == 0:
|
||
row["cells"][dev] = self.phrase("D06", "deviations")
|
||
if note is not None:
|
||
row["cells"][note] = ""
|
||
self.mark(row["responds"], "D06", row["cells"][dev])
|
||
elif (tp := row.get("tp") or {}).get("op") in (">=", "<=") and tp.get("value"):
|
||
# 数值参数不会写「暂不支持」:写一个不满足的值并标负偏离
|
||
v = Decimal(str(tp["value"]))
|
||
bad = v / 2 if tp["op"] == ">=" else v * Decimal("1.6")
|
||
row["cells"][ans] = f"{tp.get('param') or ''}:{bad.normalize():f}{tp.get('unit') or ''}"
|
||
row["cells"][dev] = "负偏离"
|
||
self.mark(row["responds"], "D06", "数值不满足并写负偏离")
|
||
else:
|
||
req_text = row["cells"][req] if req is not None else ""
|
||
if not FUNCTIONAL.search(req_text) or NON_FUNCTIONAL.search(req_text):
|
||
row["cells"][dev] = self.phrase("D06", "deviations") # 非功能要求:写负偏离而不是「不支持」
|
||
if note is not None:
|
||
row["cells"][note] = ""
|
||
self.mark(row["responds"], "D06", row["cells"][dev])
|
||
if t["rows"] and "—" in t["rows"][0]["cells"]:
|
||
t["rows"].clear()
|
||
if self.col(t, "no") is not None:
|
||
row["cells"][self.col(t, "no")] = str(len(t["rows"]) + 1)
|
||
t["rows"].append(row)
|
||
continue
|
||
param = tp.get("param") or (req_text[:12] or "该功能")
|
||
row["cells"][ans] = self.phrase("D05", "phrases").format(param=param)
|
||
row["cells"][dev] = "负偏离"
|
||
self.mark(row["responds"], "D05", row["cells"][ans])
|
||
if t["rows"] and "—" in t["rows"][0]["cells"]:
|
||
t["rows"].clear()
|
||
if self.col(t, "no") is not None:
|
||
row["cells"][self.col(t, "no")] = str(len(t["rows"]) + 1)
|
||
t["rows"].append(row)
|
||
|
||
def attachments(self, n: int, cats: dict[str, str], texts: dict[str, str]) -> None:
|
||
"""删掉附件:资格条件 → D11,评分项 → D13,其他 → D02。
|
||
伞状单元(「第二十二条规定的条件」这类总述,挂在 3 份以上附件上)不挂问题:查标报的是具体条目,和总述对不上;
|
||
条件性要求(「如依法免税则须提供」)规矩的投标人不适用,它的附件不删。"""
|
||
uses: dict[str, int] = {}
|
||
for b in self.blocks:
|
||
if b.get("attach"):
|
||
for u in b.get("responds") or []:
|
||
uses[u] = uses.get(u, 0) + 1
|
||
self.umbrella = {u for u, k in uses.items() if k >= 3}
|
||
cands = [
|
||
b
|
||
for b in self.blocks
|
||
if b.get("attach")
|
||
and self.free([u for u in b.get("responds") or [] if u not in self.umbrella])
|
||
and not any(CONDITIONAL.search(texts.get(u, "")) for u in b.get("responds") or [])
|
||
]
|
||
self.rng.shuffle(cands)
|
||
for b in cands[:n]:
|
||
units = [u for u in b.get("responds") or [] if u not in self.umbrella]
|
||
kinds = {cats.get(u) for u in units}
|
||
defect = "D11" if "qualification" in kinds else "D13" if "scoring" in kinds else "D02"
|
||
self.mark(units, defect, "附件缺失", worded=False)
|
||
self.remove(b)
|
||
|
||
def compliance(self) -> None:
|
||
for t in self.tables(fixed=True):
|
||
rows = [r for r in t["rows"] if self.free(r.get("responds") or [])]
|
||
if not rows:
|
||
continue
|
||
r = self.rng.choice(rows)
|
||
at = r.get("anchor", len(r["cells"]) - 2)
|
||
r["cells"][at] = self.phrase("D10", "cells")
|
||
# 符合性条件响应表的每一行没法对到单个标注单元(整张表共用一组),按表级问题记一个
|
||
self.rule("D10", r["cells"][at] or "空着", r["cells"][1] if len(r["cells"]) > 1 else "")
|
||
return
|
||
|
||
# ---------- 规则问题 ----------
|
||
def rule(self, kind: str, variant: str, snippet: str) -> None:
|
||
self.rule_defects.append(
|
||
{"id": f"{kind}-{len(self.rule_defects) + 1}", "type": kind, "variant": variant, "t": snippet}
|
||
)
|
||
|
||
def price_table(self) -> dict[str, Any] | None:
|
||
return next((b for b in self.blocks if b.get("price_summary")), None)
|
||
|
||
def p04(self) -> None:
|
||
t = self.price_table()
|
||
if not t or len(t["rows"][0]["cells"]) < 3: # 开标一览表没有大写金额栏(S04):做不了
|
||
return
|
||
variant = self.phrase("P04", "variants")
|
||
total = Decimal(self.spec["price"]["total"])
|
||
wrong = {"drop_digit": total / 10, "extra_shi": total + 10, "decimal_shift": total / 100}.get(
|
||
variant, total + 1
|
||
)
|
||
t["rows"][0]["cells"][2] = rmb_upper(wrong.quantize(Decimal("0.01")))
|
||
self.rule("P04", variant, t["rows"][0]["cells"][2])
|
||
|
||
def p05(self) -> None:
|
||
t = next((b for b in self.blocks if b.get("price_items")), None)
|
||
if not t or not t["rows"]:
|
||
return
|
||
r = self.rng.choice(t["rows"])
|
||
r["cells"][-1] = f"{Decimal(r['cells'][-1].replace(',', '')) + 1000:,.2f}"
|
||
self.rule("P05", "分项总价多 1000 元", r["cells"][-1])
|
||
|
||
def p06(self) -> None:
|
||
fp = self.rng.choice(self.vocab["P06"]["foreign_projects"])
|
||
self.vols[-1]["header"] = fp["name"] # 技术分册照搬旧标书最常见
|
||
self.rule("P06", "页眉留旧项目名", fp["name"])
|
||
|
||
def text_paras(self, form_hint: str) -> list[dict[str, Any]]:
|
||
return [b for b in self.blocks if b.get("type") == "para" and form_hint in str(b.get("form") or "")]
|
||
|
||
def p07(self) -> None:
|
||
bidder = self.spec["bidder"]["bidder"]
|
||
paras = [b for b in self.blocks if b.get("type") == "para" and b.get("form") and bidder in b["text"]]
|
||
if paras:
|
||
b = self.rng.choice(paras)
|
||
blank = self.phrase("P07", "blanks")
|
||
b["text"] = b["text"].replace(bidder, blank, 1)
|
||
self.rule("P07", "投标人名称空", b["text"][:24])
|
||
|
||
def p08(self) -> None:
|
||
signs = [b for b in self.blocks if b.get("type") == "sign"]
|
||
if signs:
|
||
date = self.phrase("P08", "dates")
|
||
b = self.rng.choice(signs)
|
||
b["lines"] = [re.sub(r"日期:.*", f"日期:{date}", x) for x in b["lines"]]
|
||
self.rule("P08", date, date)
|
||
|
||
def p09(self) -> None:
|
||
days = str(self.spec.get("validity_days") or 90)
|
||
for b in self.blocks:
|
||
if b.get("type") == "para" and f"{days}天" in b["text"].replace(" ", ""):
|
||
new = str(self.phrase("P09", "days"))
|
||
b["text"] = b["text"].replace(f"{days}天", f"{new}天").replace(f"{days} 天", f"{new}天")
|
||
self.rule("P09", f"{days} → {new} 天", f"{new}天")
|
||
return
|
||
|
||
def p10(self) -> None:
|
||
letter = next((i for i, b in enumerate(self.blocks) if b.get("type") == "para" and b.get("form")), None)
|
||
if letter is not None:
|
||
text = self.phrase("P10", "phrases")
|
||
self.insert_after(self.blocks[letter], {"type": "para", "text": text, "form": self.blocks[letter]["form"]})
|
||
self.rule("P10", text, text)
|
||
|
||
def p12(self) -> None:
|
||
t = self.price_table()
|
||
if t:
|
||
alt = f"{Decimal(self.spec['price']['total']) * Decimal('0.97'):,.2f}"
|
||
text = self.phrase("P12", "phrases").format(alt=alt)
|
||
self.insert_after(t, {"type": "para", "text": text})
|
||
self.rule("P12", text, text)
|
||
|
||
|
||
def table_mode(b: dict[str, Any]) -> str:
|
||
"""表格的响应方式:档案里表单写了 mode 的以它为准(如 S02 评分要的技术规格表是 presence,不是逐条响应的机制)。"""
|
||
return str(b.get("mode") or ("implicit" if b.get("only_deviations") else "explicit"))
|
||
|
||
|
||
def unit_modes(spec: dict[str, Any]) -> dict[str, str]:
|
||
"""单元的响应方式:逐条响应表里有 → explicit;只在只列偏离的表里 → implicit;附件 → evidence;格式文件 → presence。"""
|
||
modes: dict[str, str] = {}
|
||
rank = {"explicit": 0, "implicit": 1, "evidence": 2, "presence": 3}
|
||
blocks = [b for v in spec["volumes"] for b in v["blocks"]]
|
||
for b in blocks:
|
||
if b.get("type") == "table" and "only_deviations" in b:
|
||
mode = table_mode(b)
|
||
for r in [*b["rows"], *(b.get("pool") or [])]:
|
||
for u in r.get("responds") or []:
|
||
if u not in modes or rank.get(mode, 9) < rank.get(modes[u], 9):
|
||
modes[u] = mode
|
||
elif b.get("attach"):
|
||
for u in b.get("responds") or []:
|
||
modes.setdefault(u, "evidence")
|
||
elif b.get("form"):
|
||
for r in b.get("rows") or [b]:
|
||
for u in r.get("responds") or []:
|
||
modes.setdefault(u, "presence")
|
||
return modes
|
||
|
||
|
||
def main() -> None:
|
||
ap = argparse.ArgumentParser()
|
||
ap.add_argument("clean")
|
||
ap.add_argument("out", nargs="?")
|
||
ap.add_argument("--clean", dest="manifest_only", action="store_true", help="只给规矩的投标文件写响应清单")
|
||
ap.add_argument("--split", required=True, choices=["dev", "holdout"])
|
||
ap.add_argument("--seed", type=int, default=1)
|
||
ap.add_argument("--density", default="high", choices=["high", "low"])
|
||
a = ap.parse_args()
|
||
spec = json.loads((OUT / a.clean / "spec.json").read_text())
|
||
tender = spec["tender"]
|
||
units, _ = gold.units(tender)
|
||
cats = {u.id: u.cat for u in units}
|
||
texts = {
|
||
u.id: "".join(str(x.get("t", "")) for x in u.raw.get("anchors") or []) + str(u.raw.get("note") or "")
|
||
for u in units
|
||
}
|
||
optional = {u.id for u in units if u.optional}
|
||
modes = unit_modes(spec) # 注入前算:被删掉的行也要知道它本来该怎么响应
|
||
m = Mutator(spec, json.loads(VOCAB[a.split].read_text()), random.Random(a.seed)) # noqa: S311 评测数据,不用于安全
|
||
if not a.manifest_only:
|
||
if a.density == "high":
|
||
m.d01_block()
|
||
m.row_ops(18)
|
||
m.implicit_deviation(4)
|
||
m.attachments(4, cats, texts)
|
||
m.compliance()
|
||
for op in (m.p04, m.p05, m.p06, m.p07, m.p08, m.p09, m.p10, m.p12):
|
||
op()
|
||
else:
|
||
m.row_ops(4)
|
||
m.attachments(1, cats, texts)
|
||
m.p04()
|
||
m.p07()
|
||
bid = a.clean if a.manifest_only else a.out
|
||
assert bid
|
||
manifest_units: dict[str, Any] = {}
|
||
na = set(spec.get("na_units") or [])
|
||
for u in units:
|
||
mode = modes.get(u.id) or ("na" if u.id in na else "unknown")
|
||
entry: dict[str, Any] = {"cat": u.cat, "mode": mode, "optional": u.id in optional}
|
||
if u.id in m.umbrella and not a.manifest_only:
|
||
entry["expect"] = "unknown" # 伞状单元:附件删了一部分,算不算问题看具体条目,评分不算
|
||
manifest_units[u.id] = entry
|
||
continue
|
||
if u.id in m.unit_defects:
|
||
entry.update({"expect": "defect", "reviewed": False, **m.unit_defects[u.id]})
|
||
elif mode == "implicit":
|
||
entry.update({"expect": "ok", "trap": "T06"})
|
||
elif u.id in (spec.get("traps") or {}):
|
||
entry.update({"expect": "ok", "trap": spec["traps"][u.id]})
|
||
else:
|
||
entry["expect"] = {"na": "na", "unknown": "unknown"}.get(mode, "ok")
|
||
manifest_units[u.id] = entry
|
||
manifest = {
|
||
"bid": bid,
|
||
"tender": tender,
|
||
"style": spec["style"],
|
||
"split": a.split,
|
||
"kind": "clean" if a.manifest_only else "sloppy",
|
||
"seed": a.seed,
|
||
"density": None if a.manifest_only else a.density,
|
||
"units": manifest_units,
|
||
"rule_defects": m.rule_defects,
|
||
}
|
||
d = OUT / bid
|
||
d.mkdir(parents=True, exist_ok=True)
|
||
spec["bid"] = bid
|
||
(d / "spec.json").write_text(json.dumps(spec, ensure_ascii=False, indent=1))
|
||
(d / "manifest.json").write_text(json.dumps(manifest, ensure_ascii=False, indent=1))
|
||
n_def = sum(1 for e in manifest_units.values() if e.get("expect") == "defect")
|
||
by: dict[str, int] = {}
|
||
for e in manifest_units.values():
|
||
if e.get("expect") == "defect":
|
||
by[e["defect"]] = by.get(e["defect"], 0) + 1
|
||
print(f"{bid}:要求项问题 {n_def} 个 {by},规则问题 {len(m.rule_defects)} 个")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|