mirror of
https://github.com/ThearchyHelios/AgentLab.git
synced 2026-11-15 22:31:59 +00:00
2d5e50fa88
配方语言(agentlab-recipe/2,新 recipe_types、recipe) - 封闭、不含正则、保存时静态校验:常量只能从表里的候选文字里选,占位符只能映射成空值,单位从固定词表里取 - 交叉表和列表两种版式;分段各自定位、不依赖先后顺序;合计按标签语义区间核对,关系恒等式、口径不同的声明都写在配方里 - 固定的解析器集合(parsers/2):全角、各类横线、千分位、月日写法宽容归一,规范写法存库、原文另记 执行器(新 xlsx_cells、recipe_engine) - 两遍读取逐行对账,每个非空格都要有去向(格子账);交叉表逆透视成长表,按统计期补年份并断言覆盖整期 - 合计行改作核对并另存一张表,不和明细混在一列;区域外文字、隐藏行列、合并单元格、空行之后的文字都单独报出 - STRICT 加主键写临时库;按锚点段记溯源,供后续证据下钻用;大表流式读取,扫描在 sheetData 处停止 校验与说明(新 recipe_checks、recipe_notes) - SQL 核对分通过、不一致、无法核对三种结果,失败分结构、数据质量、需确认三类;结构类直接拒收 - 数据质量类可以写理由接受,接受不改库、只改状态和说明 - 表的说明按核对结果生成、不含数字,经过数字检查;没核对过的不写「已核对」 起草(新 recipe_suggest、recipe_ai) - 先用规则起草:建议卡片加待确认问题(关系要不要登记、占位符表示什么) - 规则起草不完整时才提示 AI 兜底:界面先展示要发出的全部文字,用户同意才发;只发结构,数字、隐藏行列、遮罩列不发,公式先脱敏;同意绑定模型和服务方;AI 只写配方,有限次修订,记 token 和费用;截断单独识别 - 无论规则还是 AI 起草,都必须人工确认才能启用 导入流程(新 recipe_imports、recipe_confirm、recipe_diff、api/table_imports) - 暂存 → 起草 → 回答问题或改配方 → 试运行 → 勾确认项、写接受理由 → 确认并启用;接入期 1 的构建、导入记录、快照和运行固定版本 - 上传新一期按已确认的配方重放,不调用模型,出回执和差异卡,手动启用 - 改配方报破坏性变更(breaking:*)、单位变化、工作表改名的兜底,都要逐项勾选确认 前端导入向导(新 pages/import) - 原始网格(行列号、去向着色)、建议卡片、配方面板(表单,不露正则)、试运行回执、确认清单、差异卡、AI 起草的同意对话框
142 lines
5.8 KiB
Python
142 lines
5.8 KiB
Python
"""9.4 的数据变异:在 XML 层改 D00 的字节(flow_workbook(2026-08-01, 31)),保留缓存值。
|
||
|
||
做法同 xlsx-lab 评审时的 mutate.py,但输入是合成夹具。openpyxl 写不出来的形态(去掉公式留缓存、陈旧的
|
||
<dimension>、右侧多出的格子不进 <dimension>)只能这么造。每种变异只改预定的格子,其余字节级不动
|
||
(test_fixtures_xlsx 逐格比对)。
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import re
|
||
|
||
from . import patch_parts, sheet_part
|
||
from .flow import SHEET
|
||
|
||
#: 代号 → (改法, 期望的结果, 期望出现的问题 code 或核对 id)。照 P2-SPEC 9.4 抄录,WP-7 验收时逐项比
|
||
MUTATIONS: dict[str, tuple[str, str, tuple[str, ...]]] = {
|
||
"P1": ("第 28–30 行去掉公式、保留缓存值", "passed", ()),
|
||
"P1b": ("第 28–30 行保留公式、去掉缓存值", "needs_decision", ("K1",)),
|
||
"P2": ("D15 改成空", "rejected", ("value_blank",)),
|
||
"P6": ("R4 改成与 Q4 相同的日期", "rejected", ("axis_duplicate",)),
|
||
"P9": ("C28 改成 =SUM(C22:C24),缓存值自洽", "rejected", ("K1", "G1")),
|
||
"P20": ("第 32 行加「备用分区(人次)」和两个数,<dimension> 不更新", "rejected", ("row_unclaimed",)),
|
||
"P23": ("<dimension> 截到第 27 行", "passed", ()),
|
||
"P24": ("<dimension> 截到倒数第二个日期列", "passed", ()),
|
||
"P25": ("右侧多一列:表头「合计」、各数据行 999,<dimension> 不更新", "rejected", ("axis_extra_cells",)),
|
||
"P26": ("删掉最后一个日期列的所有格,<dimension> 跟着改,B2 仍写到月末", "rejected", ("axis_coverage",)),
|
||
# 9.4「外加」:同一分段里一行写 8-9、另一行写 8-9(全角横线)
|
||
"PKDUP": ("B12(9-10)改成「8-9」,与 B11 的「8-9」规范写法相同", "rejected", ("pk_duplicate", "label_duplicate")),
|
||
}
|
||
|
||
#: D00 的数据行(指标、日间、夜间、合计)
|
||
DATA_ROWS = (5, 6, 7, *range(10, 21), *range(22, 31))
|
||
_TOTAL_ROWS = "(?:28|29|30)"
|
||
|
||
|
||
def _cell_span(xml: str, ref: str) -> re.Match[str]:
|
||
m = re.search(rf'<c r="{ref}"(?:[^>]*/>|[^>]*>.*?</c>)', xml)
|
||
if m is None:
|
||
raise AssertionError(f"没有格子 {ref}")
|
||
return m
|
||
|
||
|
||
def _replace_cell(xml: str, ref: str, new: str) -> str:
|
||
m = _cell_span(xml, ref)
|
||
return xml[:m.start()] + new + xml[m.end():]
|
||
|
||
|
||
def _number(xml: str, ref: str) -> float:
|
||
m = re.search(r"<v>([^<]*)</v>", _cell_span(xml, ref).group(0))
|
||
if m is None:
|
||
raise AssertionError(f"格子 {ref} 没有值")
|
||
return float(m.group(1))
|
||
|
||
|
||
def _text(xml: str, ref: str) -> str:
|
||
m = re.search(r"<t[^>]*>([^<]*)</t>", _cell_span(xml, ref).group(0))
|
||
if m is None:
|
||
raise AssertionError(f"格子 {ref} 不是文字")
|
||
return m.group(1)
|
||
|
||
|
||
def _inline(ref: str, text: str) -> str:
|
||
return f'<c r="{ref}" t="inlineStr"><is><t>{text}</t></is></c>'
|
||
|
||
|
||
def _num(ref: str, v: float | int) -> str:
|
||
return f'<c r="{ref}" t="n"><v>{v}</v></c>'
|
||
|
||
|
||
def _append_to_row(xml: str, row: int, cell: str) -> str:
|
||
xml, n = re.subn(rf'(<row r="{row}"[^>]*>.*?)(</row>)', lambda m: m.group(1) + cell + m.group(2), xml,
|
||
count=1, flags=re.S)
|
||
if n != 1:
|
||
raise AssertionError(f"没有第 {row} 行")
|
||
return xml
|
||
|
||
|
||
def _dimension(xml: str) -> tuple[str, int, str, int]:
|
||
m = re.search(r'<dimension ref="([A-Z]+)(\d+):([A-Z]+)(\d+)"/>', xml)
|
||
if m is None:
|
||
raise AssertionError("没有 <dimension>")
|
||
return m.group(1), int(m.group(2)), m.group(3), int(m.group(4))
|
||
|
||
|
||
def _set_dimension(xml: str, ref: str) -> str:
|
||
return re.sub(r'<dimension ref="[^"]*"/>', f'<dimension ref="{ref}"/>', xml, count=1)
|
||
|
||
|
||
def _col_shift(letters: str, delta: int) -> str:
|
||
n = 0
|
||
for ch in letters:
|
||
n = n * 26 + ord(ch) - 64
|
||
n += delta
|
||
out = ""
|
||
while n:
|
||
n, rem = divmod(n - 1, 26)
|
||
out = chr(65 + rem) + out
|
||
return out
|
||
|
||
|
||
def _mutate_sheet(xml: str, case: str) -> str:
|
||
c1, r1, last, r2 = _dimension(xml)
|
||
if (c1, r1, r2) != ("B", 2, 30):
|
||
raise ValueError(f"mutate 只接受 D00 的版式(<dimension> 是 B2:…30),这份是 {c1}{r1}:{last}{r2}")
|
||
if case == "P1":
|
||
return re.sub(rf'(<c r="[A-Z]+{_TOTAL_ROWS}"[^>]*>)<f>[^<]*</f>', r"\1", xml)
|
||
if case == "P1b":
|
||
return re.sub(rf'(<c r="[A-Z]+{_TOTAL_ROWS}"[^>]*><f>[^<]*</f>)(?:<v>[^<]*</v>|<v\s*/>)', r"\1", xml)
|
||
if case == "P2":
|
||
return _replace_cell(xml, "D15", "")
|
||
if case == "P6":
|
||
return _replace_cell(xml, "R4", _inline("R4", _text(xml, "Q4")))
|
||
if case == "P9":
|
||
v = sum(_number(xml, f"C{r}") for r in (22, 23, 24))
|
||
return _replace_cell(xml, "C28", f'<c r="C28"><f>SUM(C22:C24)</f><v>{int(v)}</v></c>')
|
||
if case == "P20":
|
||
row = ('<row r="32">' + _inline("B32", "备用分区(人次)") + _num("C32", 12) + _num("D32", 15) + "</row>")
|
||
return xml.replace("</sheetData>", row + "</sheetData>", 1)
|
||
if case == "P23":
|
||
return _set_dimension(xml, f"B2:{last}27")
|
||
if case == "P24":
|
||
return _set_dimension(xml, f"B2:{_col_shift(last, -1)}30")
|
||
if case == "P25":
|
||
extra = _col_shift(last, 1)
|
||
xml = _append_to_row(xml, 4, _inline(f"{extra}4", "合计"))
|
||
for r in DATA_ROWS:
|
||
xml = _append_to_row(xml, r, _num(f"{extra}{r}", 999))
|
||
return xml
|
||
if case == "P26":
|
||
xml = re.sub(rf'<c r="{last}\d+"(?:[^>]*/>|[^>]*>.*?</c>)', "", xml)
|
||
return _set_dimension(xml, f"B2:{_col_shift(last, -1)}30")
|
||
if case == "PKDUP":
|
||
return _replace_cell(xml, "B12", _inline("B12", "8-9"))
|
||
raise ValueError(f"不认识的变异:{case}")
|
||
|
||
|
||
def mutate(xlsx: bytes, case: str) -> bytes:
|
||
"""在 XML 层改 D00 的字节。case 见 MUTATIONS。"""
|
||
if case not in MUTATIONS:
|
||
raise ValueError(f"不认识的变异:{case}")
|
||
part = sheet_part(xlsx, SHEET)
|
||
return patch_parts(xlsx, {part: lambda t: _mutate_sheet(t, case)})
|